瀏覽代碼

Fix parse https link

世界 4 年之前
父節點
當前提交
e43dcdcc07

+ 1 - 0
.idea/gradle.xml

@@ -14,6 +14,7 @@
           <set>
           <set>
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$/app" />
             <option value="$PROJECT_DIR$/app" />
+            <option value="$PROJECT_DIR$/plugin" />
             <option value="$PROJECT_DIR$/shadowsocks" />
             <option value="$PROJECT_DIR$/shadowsocks" />
             <option value="$PROJECT_DIR$/shadowsocksr" />
             <option value="$PROJECT_DIR$/shadowsocksr" />
           </set>
           </set>

+ 3 - 2
app/build.gradle

@@ -46,7 +46,7 @@ void setupPlay(boolean beta) {
     apply plugin: "com.github.triplet.play"
     apply plugin: "com.github.triplet.play"
     play {
     play {
         track = "beta"
         track = "beta"
-    //    track = beta ? "beta" : "production"
+        //    track = beta ? "beta" : "production"
         defaultToAppBundles = true
         defaultToAppBundles = true
     }
     }
 }
 }
@@ -155,9 +155,10 @@ android {
 dependencies {
 dependencies {
 
 
     implementation fileTree(dir: "libs")
     implementation fileTree(dir: "libs")
+    implementation project(":plugin")
+
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
-    implementation "androidx.core:core-ktx:1.6.0-alpha02"
     implementation "androidx.activity:activity-ktx:1.2.2"
     implementation "androidx.activity:activity-ktx:1.2.2"
     implementation "androidx.fragment:fragment-ktx:1.3.3"
     implementation "androidx.fragment:fragment-ktx:1.3.3"
     implementation "androidx.browser:browser:1.3.0"
     implementation "androidx.browser:browser:1.3.0"

+ 2 - 0
app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt

@@ -125,6 +125,8 @@ class SagerNet : Application() {
         updateNotificationChannels()
         updateNotificationChannels()
 
 
         Seq.setContext(applicationContext)
         Seq.setContext(applicationContext)
+
+        application.filesDir.mkdirs()
         Libv2ray.setAssetsPath(application.filesDir.absolutePath, "v2ray/")
         Libv2ray.setAssetsPath(application.filesDir.absolutePath, "v2ray/")
     }
     }
 
 

+ 2 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpFmt.kt

@@ -29,6 +29,8 @@ fun parseHttp(link: String): HttpBean {
     val httpUrl = link.replace("naive+https://", "https://").toHttpUrlOrNull()
     val httpUrl = link.replace("naive+https://", "https://").toHttpUrlOrNull()
         ?: error("Invalid http(s) link: $link")
         ?: error("Invalid http(s) link: $link")
 
 
+    if (httpUrl.encodedPath != "/") error("Not http proxy")
+
     return HttpBean().apply {
     return HttpBean().apply {
         serverAddress = httpUrl.host
         serverAddress = httpUrl.host
         serverPort = httpUrl.port
         serverPort = httpUrl.port

+ 12 - 12
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt

@@ -774,18 +774,18 @@ fun parseV2RayN(link: String): VMessBean {
     val bean = VMessBean()
     val bean = VMessBean()
     val json = JSONObject(result)
     val json = JSONObject(result)
 
 
-    bean.serverAddress = json.getStr("add")
-    bean.serverPort = json.getInt("port")
-    bean.security = json.getStr("scy")
-    bean.uuid = json.getStr("id")
-    bean.alterId = json.getInt("aid")
-    bean.type = json.getStr("net")
-    bean.headerType = json.getStr("type")
-    bean.host = json.getStr("host")
-    bean.path = json.getStr("path")
-    bean.name = json.getStr("ps")
-    bean.tlsSni = json.getStr("sni")
-    bean.security = json.getStr("tls")
+    bean.serverAddress = json.getStr("add") ?: ""
+    bean.serverPort = json.getInt("port") ?: 1080
+    bean.security = json.getStr("scy") ?: ""
+    bean.uuid = json.getStr("id") ?: ""
+    bean.alterId = json.getInt("aid") ?: 0
+    bean.type = json.getStr("net") ?: ""
+    bean.headerType = json.getStr("type") ?: ""
+    bean.host = json.getStr("host") ?: ""
+    bean.path = json.getStr("path") ?: ""
+    bean.name = json.getStr("ps") ?: ""
+    bean.tlsSni = json.getStr("sni") ?: ""
+    bean.security = json.getStr("tls") ?: ""
 
 
     if (json.getInt("v", 2) < 2) {
     if (json.getInt("v", 2) < 2) {
         when (bean.type) {
         when (bean.type) {

+ 32 - 0
app/src/main/java/io/nekohasekai/sagernet/plugin/NativePlugin.kt

@@ -0,0 +1,32 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.content.pm.ResolveInfo
+
+class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
+    init {
+        check(resolveInfo.providerInfo != null)
+    }
+
+    override val componentInfo get() = resolveInfo.providerInfo!!
+}

+ 41 - 0
app/src/main/java/io/nekohasekai/sagernet/plugin/Plugin.kt

@@ -0,0 +1,41 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.graphics.drawable.Drawable
+
+abstract class Plugin {
+    abstract val id: String
+    abstract val label: CharSequence
+    open val icon: Drawable? get() = null
+    open val defaultConfig: String? get() = null
+    open val packageName: String get() = ""
+    open val directBootAware: Boolean get() = true
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        return id == (other as Plugin).id
+    }
+
+    override fun hashCode() = id.hashCode()
+}

+ 50 - 0
app/src/main/java/io/nekohasekai/sagernet/plugin/PluginList.kt

@@ -0,0 +1,50 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.widget.Toast
+import io.nekohasekai.sagernet.SagerNet
+
+class PluginList : ArrayList<Plugin>() {
+    init {
+        addAll(SagerNet.application.packageManager.queryIntentContentProviders(
+            Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
+            .filter { it.providerInfo.exported }.map { NativePlugin(it) })
+    }
+
+    val lookup = mutableMapOf<String, Plugin>().apply {
+        for (plugin in this@PluginList) {
+            fun check(old: Plugin?) {
+                if (old != null && old !== plugin) {
+                    val packages = [email protected] { it.id == plugin.id }
+                        .joinToString { it.packageName }
+                    val message = "Conflicting plugins found from: $packages"
+                    Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
+                    throw IllegalStateException(message)
+                }
+            }
+            check(put(plugin.id, plugin))
+        }
+    }
+}

+ 198 - 0
app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt

@@ -0,0 +1,198 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.pm.ComponentInfo
+import android.content.pm.PackageManager
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.net.Uri
+import android.os.Build
+import android.system.Os
+import android.widget.Toast
+import androidx.core.os.bundleOf
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.bg.BaseService
+import io.nekohasekai.sagernet.ktx.Logs
+import io.nekohasekai.sagernet.ktx.listenForPackageChanges
+import java.io.File
+import java.io.FileNotFoundException
+
+object PluginManager {
+    class PluginNotFoundException(private val plugin: String) : FileNotFoundException(plugin),
+        BaseService.ExpectedException {
+        override fun getLocalizedMessage() =
+            SagerNet.application.getString(R.string.plugin_unknown, plugin)
+    }
+
+    private var receiver: BroadcastReceiver? = null
+    private var cachedPlugins: PluginList? = null
+    fun fetchPlugins() = synchronized(this) {
+        if (receiver == null) receiver = SagerNet.application.listenForPackageChanges {
+            synchronized(this) {
+                receiver = null
+                cachedPlugins = null
+            }
+        }
+        if (cachedPlugins == null) cachedPlugins = PluginList()
+        cachedPlugins!!
+    }
+
+    private fun buildUri(id: String) = Uri.Builder()
+        .scheme(PluginContract.SCHEME)
+        .authority(PluginContract.AUTHORITY)
+        .path("/$id")
+        .build()
+
+    data class InitResult(
+        val path: String,
+    )
+
+    @Throws(Throwable::class)
+    fun init(pluginId: String): InitResult? {
+        if (pluginId.isEmpty()) return null
+        var throwable: Throwable? = null
+
+        try {
+            val result = initNative(pluginId)
+            if (result != null) return result
+        } catch (t: Throwable) {
+            if (throwable == null) throwable = t else Logs.w(t)
+        }
+
+        throw throwable ?: PluginNotFoundException(pluginId)
+    }
+
+    private fun initNative(pluginId: String): InitResult? {
+        var flags = PackageManager.GET_META_DATA
+        if (Build.VERSION.SDK_INT >= 24) {
+            flags =
+                flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
+        }
+        val providers = SagerNet.application.packageManager.queryIntentContentProviders(
+            Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId)), flags)
+            .filter { it.providerInfo.exported }
+        if (providers.isEmpty()) return null
+        if (providers.size > 1) {
+            val message =
+                "Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
+            Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
+            throw IllegalStateException(message)
+        }
+        val provider = providers.single().providerInfo
+        var failure: Throwable? = null
+        try {
+            initNativeFaster(provider)?.also { return InitResult(it) }
+        } catch (t: Throwable) {
+            Logs.w("Initializing native plugin faster mode failed")
+            failure = t
+        }
+
+        val uri = Uri.Builder().apply {
+            scheme(ContentResolver.SCHEME_CONTENT)
+            authority(provider.authority)
+        }.build()
+        try {
+            return initNativeFast(SagerNet.application.contentResolver,
+                pluginId,
+                uri)?.let { InitResult(it) }
+        } catch (t: Throwable) {
+            Logs.w("Initializing native plugin fast mode failed")
+            failure?.also { t.addSuppressed(it) }
+            failure = t
+        }
+
+        try {
+            return initNativeSlow(SagerNet.application.contentResolver,
+                pluginId,
+                uri)?.let { InitResult(it) }
+        } catch (t: Throwable) {
+            failure?.also { t.addSuppressed(it) }
+            throw t
+        }
+    }
+
+    private fun initNativeFaster(provider: ProviderInfo): String? {
+        return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)
+            ?.let { relativePath ->
+                File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
+                    check(canExecute())
+                }.absolutePath
+            }
+    }
+
+    private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? {
+        return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf())
+            ?.getString(PluginContract.EXTRA_ENTRY)?.also {
+                check(File(it).canExecute())
+            }
+    }
+
+    @SuppressLint("Recycle")
+    private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? {
+        var initialized = false
+        fun entryNotFound(): Nothing =
+            throw IndexOutOfBoundsException("Plugin entry binary not found")
+
+        val pluginDir = File(SagerNet.deviceStorage.noBackupFilesDir, "plugin")
+        (cr.query(uri,
+            arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
+            null,
+            null,
+            null)
+            ?: return null).use { cursor ->
+            if (!cursor.moveToFirst()) entryNotFound()
+            pluginDir.deleteRecursively()
+            if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
+            val pluginDirPath = pluginDir.absolutePath + '/'
+            do {
+                val path = cursor.getString(0)
+                val file = File(pluginDir, path)
+                check(file.absolutePath.startsWith(pluginDirPath))
+                cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
+                    file.outputStream().use { outStream -> inStream.copyTo(outStream) }
+                }
+                Os.chmod(file.absolutePath, when (cursor.getType(1)) {
+                    Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
+                    Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
+                    else -> throw IllegalArgumentException("File mode should be of type int")
+                })
+                if (path == pluginId) initialized = true
+            } while (cursor.moveToNext())
+        }
+        if (!initialized) entryNotFound()
+        return File(pluginDir, pluginId).absolutePath
+    }
+
+    fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {
+        is String -> value
+        is Int -> SagerNet.application.packageManager.getResourcesForApplication(applicationInfo)
+            .getString(value)
+        null -> null
+        else -> error("meta-data $key has invalid type ${value.javaClass}")
+    }
+}

+ 40 - 0
app/src/main/java/io/nekohasekai/sagernet/plugin/ResolvedPlugin.kt

@@ -0,0 +1,40 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.content.pm.ComponentInfo
+import android.content.pm.ResolveInfo
+import android.graphics.drawable.Drawable
+import android.os.Build
+import com.github.shadowsocks.plugin.PluginManager.loadString
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.ktx.signaturesCompat
+
+abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
+    protected abstract val componentInfo: ComponentInfo
+
+    override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
+    override val label: CharSequence get() = resolveInfo.loadLabel(SagerNet.application.packageManager)
+    override val icon: Drawable get() = resolveInfo.loadIcon(SagerNet.application.packageManager)
+    override val packageName: String get() = componentInfo.packageName
+    override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
+}

+ 1 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt

@@ -66,6 +66,7 @@ import java.util.*
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicBoolean
 import kotlin.collections.ArrayList
 import kotlin.collections.ArrayList
 import kotlin.collections.HashMap
 import kotlin.collections.HashMap
+import kotlin.collections.HashSet
 import kotlin.collections.LinkedHashSet
 import kotlin.collections.LinkedHashSet
 
 
 class GroupFragment : ToolbarFragment(R.layout.layout_group), Toolbar.OnMenuItemClickListener {
 class GroupFragment : ToolbarFragment(R.layout.layout_group), Toolbar.OnMenuItemClickListener {

+ 20 - 0
plugin/build.gradle

@@ -0,0 +1,20 @@
+plugins {
+    id "com.android.library"
+    id "kotlin-android"
+    id "kotlin-parcelize"
+}
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "30.0.3"
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 30
+    }
+
+}
+
+dependencies {
+    api "androidx.core:core-ktx:1.6.0-alpha02"
+}

+ 13 - 0
plugin/src/main/AndroidManifest.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.nekohasekai.sagernet.plugin"
+    android:installLocation="internalOnly"
+    android:sharedUserId="io.nekohasekai.sagernet">
+
+    <application>
+        <meta-data
+            android:name="io.nekohasekai.sagernet.plugin.version"
+            android:value="1.0" />
+    </application>
+
+</manifest>

+ 97 - 0
plugin/src/main/java/io/nekohasekai/sagernet/plugin/NativePluginProvider.kt

@@ -0,0 +1,97 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  This program is free software: you can redistribute it and/or modify       *
+ *  it under the terms of the GNU General Public License as published by       *
+ *  the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                        *
+ *                                                                             *
+ *  This program is distributed in the hope that it will be useful,            *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ *  GNU General Public License for more details.                               *
+ *                                                                             *
+ *  You should have received a copy of the GNU General Public License          *
+ *  along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                             *
+ *******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.os.Bundle
+import android.os.ParcelFileDescriptor
+import androidx.core.os.bundleOf
+
+abstract class NativePluginProvider : ContentProvider() {
+    override fun getType(uri: Uri): String? = "application/x-elf"
+
+    override fun onCreate(): Boolean = true
+
+    /**
+     * Provide all files needed for native plugin.
+     *
+     * @param provider A helper object to use to add files.
+     */
+    protected abstract fun populateFiles(provider: PathProvider)
+
+    override fun query(
+        uri: Uri,
+        projection: Array<out String>?,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+        sortOrder: String?,
+    ): Cursor? {
+        check(selection == null && selectionArgs == null && sortOrder == null)
+        val result = MatrixCursor(projection)
+        populateFiles(PathProvider(uri, result))
+        return result
+    }
+
+    /**
+     * Returns executable entry absolute path.
+     * This is used for fast mode initialization where ss-local launches your native binary at the path given directly.
+     * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:
+     *  - android:installLocation="internalOnly" for <manifest>
+     *  - android:extractNativeLibs="true" for <application>
+     *
+     * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
+     * default behavior.
+     *
+     * @return Absolute path for executable entry.
+     */
+    open fun getExecutable(): String = throw UnsupportedOperationException()
+
+    abstract fun openFile(uri: Uri): ParcelFileDescriptor
+    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
+        check(mode == "r")
+        return openFile(uri)
+    }
+
+    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {
+        PluginContract.METHOD_GET_EXECUTABLE -> bundleOf(Pair(PluginContract.EXTRA_ENTRY,
+            getExecutable()))
+        else -> super.call(method, arg, extras)
+    }
+
+    // Methods that should not be used
+    override fun insert(uri: Uri, values: ContentValues?): Uri? =
+        throw UnsupportedOperationException()
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+    ): Int =
+        throw UnsupportedOperationException()
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
+        throw UnsupportedOperationException()
+}

+ 54 - 0
plugin/src/main/java/io/nekohasekai/sagernet/plugin/PathProvider.kt

@@ -0,0 +1,54 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  This program is free software: you can redistribute it and/or modify       *
+ *  it under the terms of the GNU General Public License as published by       *
+ *  the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                        *
+ *                                                                             *
+ *  This program is distributed in the hope that it will be useful,            *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ *  GNU General Public License for more details.                               *
+ *                                                                             *
+ *  You should have received a copy of the GNU General Public License          *
+ *  along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                             *
+ *******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.database.MatrixCursor
+import android.net.Uri
+import java.io.File
+
+/**
+ * Helper class to provide relative paths of files to copy.
+ */
+class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
+    private val basePath = baseUri.path?.trim('/') ?: ""
+
+    fun addPath(path: String, mode: Int = 0b110100100): PathProvider {
+        val trimmed = path.trim('/')
+        if (trimmed.startsWith(basePath)) cursor.newRow()
+                .add(PluginContract.COLUMN_PATH, trimmed)
+                .add(PluginContract.COLUMN_MODE, mode)
+        return this
+    }
+    fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider {
+        var sub = to + file.name
+        if (basePath.startsWith(sub)) if (file.isDirectory) {
+            sub += '/'
+            file.listFiles()!!.forEach { addTo(it, sub, mode) }
+        } else addPath(sub, mode)
+        return this
+    }
+    fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider {
+        if (basePath.startsWith(at)) {
+            if (file.isDirectory) file.listFiles()!!.forEach { addTo(it, at, mode) } else addPath(at, mode)
+        }
+        return this
+    }
+}

+ 39 - 0
plugin/src/main/java/io/nekohasekai/sagernet/plugin/PluginContract.kt

@@ -0,0 +1,39 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+object PluginContract {
+
+    const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.ACTION_NATIVE_PLUGIN"
+    const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY"
+    const val METADATA_KEY_VERSION = "io.nekohasekai.sagernet.version"
+    const val METADATA_KEY_ID = "io.nekohasekai.sagernet.id"
+    const val METADATA_KEY_ID_ALIASES = "io.nekohasekai.sagernet.id.aliases"
+    const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.executable_path"
+    const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable"
+
+    const val RESULT_FALLBACK = 1
+    const val COLUMN_PATH = "path"
+    const val COLUMN_MODE = "mode"
+    const val SCHEME = "plugin"
+    const val AUTHORITY = "io.nekohasekai.sagernet"
+}

+ 3 - 1
settings.gradle

@@ -2,4 +2,6 @@ include ':app'
 rootProject.name = "SagerNet"
 rootProject.name = "SagerNet"
 
 
 include ':shadowsocks'
 include ':shadowsocks'
-include ':shadowsocksr'
+include ':shadowsocksr'
+
+include ':plugin'