add documents provider
This commit is contained in:
10 files changed
+391
-10
No files matched your search
@@ -0,0 +1,40 @@
|
||||
package com.github.kr328.clash.common.utils
|
||||
|
||||
data class PrefixTree<T>(val separator: String) {
|
||||
companion object {
|
||||
const val WILDCARD = "*"
|
||||
}
|
||||
|
||||
private data class Node<T>(val children: MutableMap<String, Node<T>>, var data: T?)
|
||||
private val root = Node<T>(mutableMapOf(), null)
|
||||
|
||||
fun insert(path: String, data: T) {
|
||||
val segment = path.split(separator).filter(String::isNotBlank)
|
||||
var current = root
|
||||
|
||||
for ( s in segment ) {
|
||||
current = current.children.getOrPut(s) {
|
||||
Node(mutableMapOf(), null)
|
||||
}
|
||||
}
|
||||
|
||||
current.data = data
|
||||
}
|
||||
|
||||
fun find(path: String): T? {
|
||||
val segment = path.split(separator).filter(String::isNotBlank)
|
||||
var current = root
|
||||
|
||||
for (s in segment) {
|
||||
current = when {
|
||||
current.children.containsKey(s) ->
|
||||
current.children[s]!!
|
||||
current.children.containsKey(WILDCARD) ->
|
||||
current.children[WILDCARD]!!
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
||||
return current.data
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,17 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/profile_provider" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name=".ProfileDocumentProvider"
|
||||
android:authorities="${applicationId}.documents"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS"
|
||||
android:process=":background">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
<provider
|
||||
android:name=".ServiceStatusProvider"
|
||||
android:authorities="${applicationId}.status"
|
||||
|
||||
@@ -4,24 +4,52 @@ import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.os.CancellationSignal
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.DocumentsContract.Document
|
||||
import android.provider.DocumentsContract.Root
|
||||
import android.provider.DocumentsProvider
|
||||
import com.github.kr328.clash.core.Global
|
||||
import com.github.kr328.clash.service.data.ClashDatabase
|
||||
import com.github.kr328.clash.service.files.ProfilesResolver
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class ProfileDocumentProvider : DocumentsProvider() {
|
||||
companion object {
|
||||
private const val DEFAULT_ROOT_ID = 0
|
||||
private const val DEFAULT_ROOT_ID = "0"
|
||||
private val DEFAULT_DOCUMENT_COLUMNS = arrayOf(
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_LAST_MODIFIED,
|
||||
Document.COLUMN_SIZE,
|
||||
Document.COLUMN_FLAGS
|
||||
)
|
||||
private val DEFAULT_ROOT_COLUMNS = arrayOf(
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_ICON,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_SUMMARY,
|
||||
Root.COLUMN_DOCUMENT_ID
|
||||
)
|
||||
}
|
||||
|
||||
val database = ClashDatabase.getInstance(Global.application)
|
||||
private val resolver: ProfilesResolver by lazy {
|
||||
ProfilesResolver(context!!, ClashDatabase.getInstance(context!!))
|
||||
}
|
||||
|
||||
override fun openDocument(
|
||||
documentId: String?,
|
||||
mode: String?,
|
||||
signal: CancellationSignal?
|
||||
): ParcelFileDescriptor {
|
||||
TODO("Not yet implemented")
|
||||
val m = ParcelFileDescriptor.parseMode(mode)
|
||||
|
||||
if (m and ParcelFileDescriptor.MODE_READ_ONLY == 0)
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
return runBlocking {
|
||||
val file = resolver.resolve(resolvePath(documentId ?: ""))
|
||||
file.openFile(ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryChildDocuments(
|
||||
@@ -29,11 +57,54 @@ class ProfileDocumentProvider : DocumentsProvider() {
|
||||
projection: Array<out String>?,
|
||||
sortOrder: String?
|
||||
): Cursor {
|
||||
TODO("Not yet implemented")
|
||||
return runBlocking {
|
||||
try {
|
||||
val documentPath = parentDocumentId ?: "/"
|
||||
val paths = resolvePath(documentPath)
|
||||
val file = resolver.resolve(paths)
|
||||
|
||||
MatrixCursor(resolveDocumentProjection(projection)).apply {
|
||||
file.listFiles().forEach {
|
||||
val childPaths = paths + it
|
||||
val child = resolver.resolve(paths + it)
|
||||
|
||||
newRow().apply {
|
||||
add(Document.COLUMN_DOCUMENT_ID, childPaths.joinToString("/"))
|
||||
add(Document.COLUMN_DISPLAY_NAME, child.name())
|
||||
add(Document.COLUMN_MIME_TYPE, child.mimeType())
|
||||
add(Document.COLUMN_LAST_MODIFIED, child.lastModified())
|
||||
add(Document.COLUMN_SIZE, child.size())
|
||||
add(Document.COLUMN_FLAGS, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
MatrixCursor(resolveDocumentProjection(projection))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
|
||||
TODO("Not yet implemented")
|
||||
return runBlocking {
|
||||
try {
|
||||
val documentPath = documentId ?: "/"
|
||||
val paths = resolvePath(documentPath)
|
||||
val file = resolver.resolve(paths)
|
||||
|
||||
MatrixCursor(resolveDocumentProjection(projection)).apply {
|
||||
newRow().apply {
|
||||
add(Document.COLUMN_DOCUMENT_ID, documentPath)
|
||||
add(Document.COLUMN_DISPLAY_NAME, file.name())
|
||||
add(Document.COLUMN_MIME_TYPE, file.mimeType())
|
||||
add(Document.COLUMN_LAST_MODIFIED, file.lastModified())
|
||||
add(Document.COLUMN_SIZE, file.size())
|
||||
add(Document.COLUMN_FLAGS, 0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
MatrixCursor(resolveDocumentProjection(projection))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
@@ -41,11 +112,33 @@ class ProfileDocumentProvider : DocumentsProvider() {
|
||||
}
|
||||
|
||||
override fun queryRoots(projection: Array<out String>?): Cursor {
|
||||
return MatrixCursor(projection).apply {
|
||||
newRow().apply {
|
||||
add(DocumentsContract.Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
|
||||
val flags = Root.FLAG_LOCAL_ONLY or Root.FLAG_SUPPORTS_IS_CHILD
|
||||
|
||||
return MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply {
|
||||
newRow().apply {
|
||||
add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
|
||||
add(Root.COLUMN_FLAGS, flags)
|
||||
add(Root.COLUMN_ICON, R.drawable.ic_icon)
|
||||
add(Root.COLUMN_TITLE, context!!.getString(R.string.clash_for_android))
|
||||
add(Root.COLUMN_SUMMARY, context!!.getString(R.string.profiles_and_providers))
|
||||
add(Root.COLUMN_DOCUMENT_ID, "/")
|
||||
add(Root.COLUMN_MIME_TYPES, Document.MIME_TYPE_DIR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean {
|
||||
if (parentDocumentId == null || documentId == null)
|
||||
return false
|
||||
|
||||
return documentId.startsWith(parentDocumentId)
|
||||
}
|
||||
|
||||
private fun resolveDocumentProjection(projection: Array<out String>?): Array<out String> {
|
||||
return projection ?: DEFAULT_DOCUMENT_COLUMNS
|
||||
}
|
||||
|
||||
private fun resolvePath(path: String): List<String> {
|
||||
return path.split("/").filter(String::isNotBlank)
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.github.kr328.clash.service.files
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract
|
||||
import com.github.kr328.clash.service.R
|
||||
import com.github.kr328.clash.service.util.resolveBase
|
||||
import com.github.kr328.clash.service.util.resolveProfile
|
||||
import java.io.FileNotFoundException
|
||||
import java.lang.UnsupportedOperationException
|
||||
|
||||
class ProfileDirectoryResolver(private val context: Context) {
|
||||
companion object {
|
||||
const val FILE_NAME_CONFIG = "config.yaml"
|
||||
const val FILE_NAME_PROVIDER = "providers"
|
||||
}
|
||||
|
||||
private val nextResolver = ProviderResolver(context)
|
||||
|
||||
fun resolve(id: Long, paths: List<String>): VirtualFile {
|
||||
if (paths.size == 1) {
|
||||
return object : VirtualFile {
|
||||
override fun name(): String {
|
||||
return when (paths[0]) {
|
||||
FILE_NAME_CONFIG ->
|
||||
context.getString(R.string.profile_yaml)
|
||||
FILE_NAME_PROVIDER ->
|
||||
context.getString(R.string.provider_files)
|
||||
else ->
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun lastModified(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
return resolveProfile(id).length()
|
||||
}
|
||||
|
||||
override fun mimeType(): String {
|
||||
return when (paths[0]) {
|
||||
FILE_NAME_CONFIG ->
|
||||
"text/plain"
|
||||
FILE_NAME_PROVIDER ->
|
||||
DocumentsContract.Document.MIME_TYPE_DIR
|
||||
else ->
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun listFiles(): List<String> {
|
||||
if (paths[0] == FILE_NAME_PROVIDER) {
|
||||
return resolveBase(id).list()?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun openFile(mode: Int): ParcelFileDescriptor {
|
||||
return if (paths[0] == FILE_NAME_CONFIG)
|
||||
ParcelFileDescriptor.open(resolveProfile(id), mode)
|
||||
else
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
} else if (paths.size == 2) {
|
||||
return nextResolver.resolve(id, paths[1])
|
||||
}
|
||||
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.github.kr328.clash.service.files
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract
|
||||
import com.github.kr328.clash.service.R
|
||||
import com.github.kr328.clash.service.data.ClashDatabase
|
||||
import com.github.kr328.clash.service.data.ClashProfileEntity
|
||||
import java.io.FileNotFoundException
|
||||
import java.lang.UnsupportedOperationException
|
||||
|
||||
class ProfilesResolver(private val context: Context, private val database: ClashDatabase) {
|
||||
private val nextResolver = ProfileDirectoryResolver(context)
|
||||
|
||||
suspend fun resolve(paths: List<String>): VirtualFile {
|
||||
return when (paths.size) {
|
||||
0 -> {
|
||||
val files = database.openClashProfileDao()
|
||||
.queryProfiles()
|
||||
.map(ClashProfileEntity::id)
|
||||
.map(Long::toString)
|
||||
|
||||
object : VirtualFile {
|
||||
override fun name(): String {
|
||||
return context.getString(R.string.clash_for_android)
|
||||
}
|
||||
|
||||
override fun lastModified(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun mimeType(): String {
|
||||
return DocumentsContract.Document.MIME_TYPE_DIR
|
||||
}
|
||||
|
||||
override fun listFiles(): List<String> {
|
||||
return files
|
||||
}
|
||||
|
||||
override fun openFile(mode: Int): ParcelFileDescriptor {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
val profile = paths[0].toLongOrNull()?.let {
|
||||
database.openClashProfileDao().queryProfileById(it)
|
||||
} ?: throw FileNotFoundException()
|
||||
|
||||
object : VirtualFile {
|
||||
override fun name(): String {
|
||||
return profile.name
|
||||
}
|
||||
|
||||
override fun lastModified(): Long {
|
||||
return profile.lastUpdate
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun mimeType(): String {
|
||||
return DocumentsContract.Document.MIME_TYPE_DIR
|
||||
}
|
||||
|
||||
override fun listFiles(): List<String> {
|
||||
return listOf(
|
||||
ProfileDirectoryResolver.FILE_NAME_CONFIG,
|
||||
ProfileDirectoryResolver.FILE_NAME_PROVIDER
|
||||
)
|
||||
}
|
||||
|
||||
override fun openFile(mode: Int): ParcelFileDescriptor {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val id = paths[0].toLongOrNull() ?: throw FileNotFoundException()
|
||||
|
||||
nextResolver.resolve(id, paths.subList(1, paths.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.github.kr328.clash.service.files
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.github.kr328.clash.service.util.resolveBase
|
||||
import java.io.FileNotFoundException
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.net.URLDecoder
|
||||
|
||||
class ProviderResolver(private val context: Context) {
|
||||
fun resolve(id: Long, fileName: String): VirtualFile {
|
||||
val file = resolveBase(id).resolve(fileName)
|
||||
if (!file.exists())
|
||||
throw FileNotFoundException()
|
||||
|
||||
return object : VirtualFile {
|
||||
override fun name(): String {
|
||||
return URLDecoder.decode(file.name, "utf-8")
|
||||
}
|
||||
|
||||
override fun lastModified(): Long {
|
||||
return file.lastModified()
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
return file.length()
|
||||
}
|
||||
|
||||
override fun mimeType(): String {
|
||||
return "text/plain"
|
||||
}
|
||||
|
||||
override fun listFiles(): List<String> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun openFile(mode: Int): ParcelFileDescriptor {
|
||||
return ParcelFileDescriptor.open(file, mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.github.kr328.clash.service.files
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
|
||||
interface VirtualFile {
|
||||
fun name(): String
|
||||
fun lastModified(): Long
|
||||
fun size(): Long
|
||||
fun mimeType(): String
|
||||
fun listFiles(): List<String>
|
||||
fun openFile(mode: Int): ParcelFileDescriptor
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="200">
|
||||
<path
|
||||
android:fillColor="#1E4376"
|
||||
android:pathData="M47.211,168.128C70.531,-34.962 67.471,13.788 94.071,43.818c13.45,-1.52 27.24,-3.47 40.82,-0.67c2.64,0.13 5.42,1.86 7.71,0.18c4.12,-6.27 7.35,-13.54 11.35,-20c12.19,-24.44 12.85,19.54 15.48,26.52c5.23,32.99 10.89,64.46 14.67,97.59c0.31,10.72 5.74,32.92 1.08,33.56c-49.36,5.23 -147.71,3.91 -160.84,-6.3c-15.85,-10.5 -15.18,-35.33 2.03,-43.72c3.63,-2.03 10.68,-3.72 11.94,0.7c-2.41,4.99 -8.79,5.77 -12.12,11.17C16.621,158.948 33.111,168.888 47.211,168.128zM87.841,74.008c-10.42,0.52 -9.59,14.89 -0.07,15.18C98.191,88.668 97.361,74.298 87.841,74.008zM149.121,89.188c10.46,-0.34 9.85,-14.71 0.38,-15.18C139.031,74.348 139.651,88.718 149.121,89.188zM107.871,99.228c2.16,3.48 5.28,3.29 9.79,0.16c3.81,3.17 8.06,3.28 9.18,-0.19c-3.78,1.17 -7.04,0.79 -9.4,-3.49C115.371,100.108 112.071,100.428 107.871,99.228z"
|
||||
tools:ignore="VectorPath" />
|
||||
</vector>
|
||||
@@ -11,4 +11,8 @@
|
||||
<string name="processing_profiles">正在处理配置文件</string>
|
||||
<string name="recycling_resources">正在回收资源</string>
|
||||
<string name="destroying">销毁中</string>
|
||||
<string name="clash_for_android">Clash for Android</string>
|
||||
<string name="profiles_and_providers">配置文件和外部引用</string>
|
||||
<string name="profile_yaml">配置文件.yaml</string>
|
||||
<string name="provider_files">外部引用文件</string>
|
||||
</resources>
|
||||
@@ -13,4 +13,8 @@
|
||||
<string name="running">Running</string>
|
||||
<string name="destroying">Destroying</string>
|
||||
<string name="recycling_resources">Recycling resources</string>
|
||||
<string name="clash_for_android">Clash for Android</string>
|
||||
<string name="profiles_and_providers">Profiles and Providers</string>
|
||||
<string name="profile_yaml">Profile.yaml</string>
|
||||
<string name="provider_files">Provider Files</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user