add documents provider

This commit is contained in:
Kr328 committed 2020-04-09 14:39:57 +08:00
1 parent ef6a37fb7c
commit 606a21dff0
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
}
}
+11
View File
@@ -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)
}
}
@@ -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
}
+11
View File
@@ -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>
+4
View File
@@ -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>