From 606a21dff0040b6a2f866139aa9cd7ec92df1805 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 9 Apr 2020 14:39:57 +0800 Subject: [PATCH] add documents provider --- .../kr328/clash/common/utils/PrefixTree.kt | 40 +++++++ service/src/main/AndroidManifest.xml | 11 ++ .../clash/service/ProfileDocumentProvider.kt | 113 ++++++++++++++++-- .../service/files/ProfileDirectoryResolver.kt | 74 ++++++++++++ .../clash/service/files/ProfilesResolver.kt | 90 ++++++++++++++ .../clash/service/files/ProviderResolver.kt | 42 +++++++ .../kr328/clash/service/files/VirtualFile.kt | 12 ++ service/src/main/res/drawable/ic_icon.xml | 11 ++ service/src/main/res/values-zh/strings.xml | 4 + service/src/main/res/values/strings.xml | 4 + 10 files changed, 391 insertions(+), 10 deletions(-) create mode 100644 common/src/main/java/com/github/kr328/clash/common/utils/PrefixTree.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/files/ProfileDirectoryResolver.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/files/ProfilesResolver.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/files/ProviderResolver.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/files/VirtualFile.kt create mode 100644 service/src/main/res/drawable/ic_icon.xml diff --git a/common/src/main/java/com/github/kr328/clash/common/utils/PrefixTree.kt b/common/src/main/java/com/github/kr328/clash/common/utils/PrefixTree.kt new file mode 100644 index 0000000..01c4e51 --- /dev/null +++ b/common/src/main/java/com/github/kr328/clash/common/utils/PrefixTree.kt @@ -0,0 +1,40 @@ +package com.github.kr328.clash.common.utils + +data class PrefixTree(val separator: String) { + companion object { + const val WILDCARD = "*" + } + + private data class Node(val children: MutableMap>, var data: T?) + private val root = Node(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 + } +} \ No newline at end of file diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 2306207..1290447 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -45,6 +45,17 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/profile_provider" /> + + + + + ?, 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?): 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?): 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?): Array { + return projection ?: DEFAULT_DOCUMENT_COLUMNS + } + + private fun resolvePath(path: String): List { + return path.split("/").filter(String::isNotBlank) + } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/files/ProfileDirectoryResolver.kt b/service/src/main/java/com/github/kr328/clash/service/files/ProfileDirectoryResolver.kt new file mode 100644 index 0000000..ce1c8d0 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/files/ProfileDirectoryResolver.kt @@ -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): 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 { + 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() + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/files/ProfilesResolver.kt b/service/src/main/java/com/github/kr328/clash/service/files/ProfilesResolver.kt new file mode 100644 index 0000000..fef6955 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/files/ProfilesResolver.kt @@ -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): 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 { + 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 { + 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)) + } + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/files/ProviderResolver.kt b/service/src/main/java/com/github/kr328/clash/service/files/ProviderResolver.kt new file mode 100644 index 0000000..122d729 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/files/ProviderResolver.kt @@ -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 { + return emptyList() + } + + override fun openFile(mode: Int): ParcelFileDescriptor { + return ParcelFileDescriptor.open(file, mode) + } + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/files/VirtualFile.kt b/service/src/main/java/com/github/kr328/clash/service/files/VirtualFile.kt new file mode 100644 index 0000000..688174f --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/files/VirtualFile.kt @@ -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 + fun openFile(mode: Int): ParcelFileDescriptor +} \ No newline at end of file diff --git a/service/src/main/res/drawable/ic_icon.xml b/service/src/main/res/drawable/ic_icon.xml new file mode 100644 index 0000000..e20fae3 --- /dev/null +++ b/service/src/main/res/drawable/ic_icon.xml @@ -0,0 +1,11 @@ + + + diff --git a/service/src/main/res/values-zh/strings.xml b/service/src/main/res/values-zh/strings.xml index 8adba9a..c78525a 100644 --- a/service/src/main/res/values-zh/strings.xml +++ b/service/src/main/res/values-zh/strings.xml @@ -11,4 +11,8 @@ 正在处理配置文件 正在回收资源 销毁中 + Clash for Android + 配置文件和外部引用 + 配置文件.yaml + 外部引用文件 \ No newline at end of file diff --git a/service/src/main/res/values/strings.xml b/service/src/main/res/values/strings.xml index 9e1ce4d..505c7d0 100644 --- a/service/src/main/res/values/strings.xml +++ b/service/src/main/res/values/strings.xml @@ -13,4 +13,8 @@ Running Destroying Recycling resources + Clash for Android + Profiles and Providers + Profile.yaml + Provider Files