*
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import android.app.Activity
import android.content.Intent
/**
* Base class for configuration activity. A configuration activity is started when user wishes to configure the
* selected plugin. To create a configuration activity, extend this class, implement abstract methods, invoke
* `saveChanges(options)` and `discardChanges()` when appropriate, and add it to your manifest like this:
*
* <manifest>
* ...
* <application>
* ...
* <activity android:name=".ConfigureActivity">
* <intent-filter>
* <action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/>
* <category android:name="android.intent.category.DEFAULT"/>
* <data android:scheme="plugin"
* android:host="com.github.shadowsocks"
* android:path="/$PLUGIN_ID"/>
* </intent-filter>
* </activity>
* ...
* </application>
*</manifest>
*/
abstract class ConfigurationActivity : OptionsCapableActivity() {
/**
* Equivalent to setResult(RESULT_CANCELED).
*/
fun discardChanges() = setResult(Activity.RESULT_CANCELED)
/**
* Equivalent to setResult(RESULT_OK, args_with_correct_format).
*
* @param options PluginOptions to save.
*/
fun saveChanges(options: PluginOptions) =
setResult(Activity.RESULT_OK, Intent().putExtra(PluginContract.EXTRA_OPTIONS, options.toString()))
/**
* Finish this activity and request manual editor to pop up instead.
*/
fun fallbackToManualEditor() {
setResult(PluginContract.RESULT_FALLBACK)
finish()
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/HelpActivity.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
/**
* Base class for a help activity. A help activity is started when user taps help when configuring options for your
* plugin. To create a help activity, just extend this class, and add it to your manifest like this:
*
* <manifest>
* ...
* <application>
* ...
* <activity android:name=".HelpActivity">
* <intent-filter>
* <action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/>
* <category android:name="android.intent.category.DEFAULT"/>
* <data android:scheme="plugin"
* android:host="com.github.shadowsocks"
* android:path="/$PLUGIN_ID"/>
* </intent-filter>
* </activity>
* ...
* </application>
*</manifest>
*/
abstract class HelpActivity : OptionsCapableActivity() {
override fun onInitializePluginOptions(options: PluginOptions) { }
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/HelpCallback.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.Intent
/**
* HelpCallback is an HelpActivity but you just need to produce a CharSequence help message instead of having to
* provide UI. To create a help callback, just extend this class, implement abstract methods, and add it to your
* manifest following the same procedure as adding a HelpActivity.
*/
abstract class HelpCallback : HelpActivity() {
abstract fun produceHelpMessage(options: PluginOptions): CharSequence
override fun onInitializePluginOptions(options: PluginOptions) {
setResult(RESULT_OK, Intent().putExtra(PluginContract.EXTRA_HELP_MESSAGE, produceHelpMessage(options)))
finish()
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/InternalPlugin.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
class InternalPlugin(override val id: String, override val label: CharSequence) : Plugin() {
companion object {
val SIMPLE_OBFS = InternalPlugin("obfs-local", "Simple Obfs (Internal)")
val V2RAY_PLUGIN = InternalPlugin("v2ray-plugin", "V2Ray Plugin (Internal)")
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/NativePlugin.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.pm.ResolveInfo
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init {
check(resolveInfo.providerInfo != null)
}
override val componentInfo get() = resolveInfo.providerInfo!!
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.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
/**
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
* to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
* class, implement the abstract methods, and add it to your manifest like this:
*
* <manifest>
* ...
* <application>
* ...
* <provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
* android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
* <intent-filter>
* <category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
* </intent-filter>
* </provider>
* ...
* </application>
*</manifest>
*/
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?, selection: String?, selectionArgs: Array?,
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
* - android:extractNativeLibs="true" for
*
* 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?): Int =
throw UnsupportedOperationException()
override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int =
throw UnsupportedOperationException()
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/NoPlugin.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
object NoPlugin : Plugin() {
override val id: String get() = ""
override val label: CharSequence get() = SagerNet.application.getText(R.string.plugin_disabled)
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/OptionsCapableActivity.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import io.nekohasekai.sagernet.ui.ThemedActivity
/**
* Activity that's capable of getting EXTRA_OPTIONS input.
*/
abstract class OptionsCapableActivity : ThemedActivity() {
protected fun pluginOptions(intent: Intent = this.intent) = try {
PluginOptions("", intent.getStringExtra(PluginContract.EXTRA_OPTIONS))
} catch (exc: IllegalArgumentException) {
Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show()
PluginOptions()
}
/**
* Populate args to your user interface.
*
* @param options PluginOptions parsed.
*/
protected abstract fun onInitializePluginOptions(options: PluginOptions = pluginOptions())
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
if (savedInstanceState == null) onInitializePluginOptions()
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/PathProvider.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.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
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/Plugin.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import android.graphics.drawable.Drawable
abstract class Plugin {
abstract val id: String
open val idAliases get() = emptyArray()
abstract val label: CharSequence
open val icon: Drawable? get() = null
open val defaultConfig: String? get() = null
open val packageName: String get() = ""
open val trusted: Boolean get() = true
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()
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/PluginConfiguration.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.utils.Commandline
import java.util.*
class PluginConfiguration(val pluginsOptions: MutableMap, var selected: String) {
private constructor(plugins: List) : this(
plugins.filter { it.id.isNotEmpty() }.associateBy { it.id }.toMutableMap(),
if (plugins.isEmpty()) "" else plugins[0].id)
constructor(): this(listOf())
constructor(plugin: String) : this(plugin.split('\n').map { line ->
if (line.startsWith("kcptun ")) {
val opt = PluginOptions()
opt.id = "kcptun"
try {
val iterator = Commandline.translateCommandline(line).drop(1).iterator()
while (iterator.hasNext()) {
val option = iterator.next()
when {
option == "--nocomp" -> opt["nocomp"] = null
option.startsWith("--") -> opt[option.substring(2)] = iterator.next()
else -> throw IllegalArgumentException("Unknown kcptun parameter: $option")
}
}
} catch (exc: Exception) {
Logs.w(exc)
}
opt
} else PluginOptions(line)
})
fun getOptions(
id: String = selected,
defaultConfig: () -> String? = { PluginManager.fetchPlugins(true).lookup[id]?.defaultConfig }
) = if (id.isEmpty()) PluginOptions() else pluginsOptions[id] ?: PluginOptions(id, defaultConfig())
override fun toString(): String {
val result = LinkedList()
for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt)
if (!pluginsOptions.contains(selected)) result.addFirst(getOptions())
return result.joinToString("\n") { it.toString(false) }
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/PluginContract.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
/**
* The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
*
* This class is written in Java to keep Java interoperability.
*/
object PluginContract {
/**
* ContentProvider Action: Used for NativePluginProvider.
*
* Constant Value: "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
*/
const val ACTION_NATIVE_PLUGIN = "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
/**
* Activity Action: Used for ConfigurationActivity.
*
* Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
*/
const val ACTION_CONFIGURE = "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
/**
* Activity Action: Used for HelpActivity or HelpCallback.
*
* Constant Value: "com.github.shadowsocks.plugin.ACTION_HELP"
*/
const val ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP"
/**
* The lookup key for a string that provides the plugin entry binary.
*
* Example: "/data/data/com.github.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_ENTRY"
*/
const val EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY"
/**
* The lookup key for a string that provides the options as a string.
*
* Example: "obfs=http;obfs-host=www.baidu.com"
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_OPTIONS"
*/
const val EXTRA_OPTIONS = "com.github.shadowsocks.plugin.EXTRA_OPTIONS"
/**
* The lookup key for a CharSequence that provides user relevant help message.
*
* Example: "obfs=|tls> Enable obfuscating: HTTP or TLS (Experimental).
* obfs-host= Hostname for obfuscating (Experimental)."
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
*/
const val EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
/**
* The metadata key to retrieve plugin version. Required for plugin applications.
*
* Constant Value: "com.github.shadowsocks.plugin.version"
*/
const val METADATA_KEY_VERSION = "com.github.shadowsocks.plugin.version"
/**
* The metadata key to retrieve plugin id. Required for plugins.
*
* Constant Value: "com.github.shadowsocks.plugin.id"
*/
const val METADATA_KEY_ID = "com.github.shadowsocks.plugin.id"
/**
* The metadata key to retrieve plugin id aliases.
* Can be a string (representing one alias) or a resource to a string or string array.
*
* Constant Value: "com.github.shadowsocks.plugin.id.aliases"
*/
const val METADATA_KEY_ID_ALIASES = "com.github.shadowsocks.plugin.id.aliases"
/**
* The metadata key to retrieve default configuration. Default value is empty.
*
* Constant Value: "com.github.shadowsocks.plugin.default_config"
*/
const val METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config"
/**
* The metadata key to retrieve executable path to your native binary.
* This path should be relative to your application's nativeLibraryDir.
*
* If this is set, the host app will prefer this value and (probably) not launch your app at all (aka faster mode).
* In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:
* - android:installLocation="internalOnly" for
* - android:extractNativeLibs="true" for
*
* Do not use this if you plan to do some setup work before giving away your binary path,
* or your native binary is not at a fixed location relative to your application's nativeLibraryDir.
*
* Since plugin lib: 1.3.0
*
* Constant Value: "com.github.shadowsocks.plugin.executable_path"
*/
const val METADATA_KEY_EXECUTABLE_PATH = "com.github.shadowsocks.plugin.executable_path"
const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"
/** ConfigurationActivity result: fallback to manual edit mode. */
const val RESULT_FALLBACK = 1
/**
* Relative to the file to be copied. This column is required.
*
* Example: "kcptun", "doc/help.txt"
*
* Type: String
*/
const val COLUMN_PATH = "path"
/**
* File mode bits. Default value is 644 in octal.
*
* Example: 0b110100100 (for 755 in octal)
*
* Type: Int or String (deprecated)
*/
const val COLUMN_MODE = "mode"
/**
* The scheme for general plugin actions.
*/
const val SCHEME = "plugin"
/**
* The authority for general plugin actions.
*/
const val AUTHORITY = "com.github.shadowsocks"
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/PluginList.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.Intent
import android.content.pm.PackageManager
import android.widget.Toast
import io.nekohasekai.sagernet.SagerNet
class PluginList(skipInternal: Boolean) : ArrayList() {
init {
add(NoPlugin)
if (!skipInternal) {
add(InternalPlugin.SIMPLE_OBFS)
add(InternalPlugin.V2RAY_PLUGIN)
}
addAll(SagerNet.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
.filter { it.providerInfo.exported }.map { NativePlugin(it) })
}
val lookup = mutableMapOf().apply {
for (plugin in this@PluginList.toList()) {
fun check(old: Plugin?) {
if (old != null && old != plugin) {
this@PluginList.remove(old)
}
// skip check
/*if (old != null && old !== plugin) {
val packages = this@PluginList.filter { 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))
for (alias in plugin.idAliases) check(put(alias, plugin))
}
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.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.content.pm.Signature
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.system.Os
import android.util.Base64
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 io.nekohasekai.sagernet.ktx.signaturesCompat
import java.io.File
import java.io.FileNotFoundException
object PluginManager {
class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin),
BaseService.ExpectedException {
override fun getLocalizedMessage() = SagerNet.application.getString(R.string.plugin_unknown, plugin)
}
/**
* Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
* to publish or has published plugins for this app. You can obtain your public key by executing:
*
* $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
*
* If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/
val trustedSignatures by lazy {
SagerNet.packageInfo.signaturesCompat.toSet() +
Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
|TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
|ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
|OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
|b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
|b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
|L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
|Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
|M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
|YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
|agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
|6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
|YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
|zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
|RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
|wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
|1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
""", Base64.DEFAULT)) +
Signature(Base64.decode( // @madeye
"""
|MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
|CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
|BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
|ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
|BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
|9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
|4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
|AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
|Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
|AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
|1iNuLIssdhDz3IL9Yg==
""", Base64.DEFAULT))
}
private var receiver: BroadcastReceiver? = null
private var cachedPlugins: PluginList? = null
private var cachedPluginsSkipInternal: PluginList? = null
fun fetchPlugins(skipInternal: Boolean) = synchronized(this) {
if (receiver == null) receiver = SagerNet.application.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = null
cachedPluginsSkipInternal = null
}
}
if (skipInternal) {
if (cachedPlugins == null) cachedPlugins = PluginList(skipInternal)
cachedPlugins!!
} else {
if (cachedPluginsSkipInternal == null) cachedPluginsSkipInternal = PluginList(skipInternal)
cachedPluginsSkipInternal!!
}
}
private fun buildUri(id: String) = Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(PluginContract.AUTHORITY)
.path("/$id")
.build()
fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id))
data class InitResult(
val path: String,
val options: PluginOptions,
val isV2: Boolean = false,
)
// the following parts are meant to be used by :bg
@Throws(Throwable::class)
fun init(configuration: PluginConfiguration): InitResult? {
if (configuration.selected.isEmpty()) return null
var throwable: Throwable? = null
try {
val result = initNative(configuration)
if (result != null) return result
} catch (t: Throwable) {
if (throwable == null) throwable = t else Logs.w(t)
}
// add other plugin types here
throw throwable ?: PluginNotFoundException(configuration.selected)
}
private fun initNative(configuration: PluginConfiguration): 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(configuration.selected)), 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
val options = configuration.getOptions { provider.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
val isV2 = provider.applicationInfo.metaData?.getString(PluginContract.METADATA_KEY_VERSION)
?.substringBefore('.')?.toIntOrNull() ?: 0 >= 2
var failure: Throwable? = null
try {
initNativeFaster(provider)?.also { return InitResult(it, options, isV2) }
} 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, options, uri)?.let { InitResult(it, options, isV2) }
} catch (t: Throwable) {
Logs.w("Initializing native plugin fast mode failed")
failure?.also { t.addSuppressed(it) }
failure = t
}
try {
return initNativeSlow(SagerNet.application.contentResolver, options, uri)?.let { InitResult(it, options, isV2) }
} 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, options: PluginOptions, uri: Uri): String? {
return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null,
bundleOf(PluginContract.EXTRA_OPTIONS to options.id))?.getString(PluginContract.EXTRA_ENTRY)?.also {
check(File(it).canExecute())
}
}
@SuppressLint("Recycle")
private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, 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 == options.id) initialized = true
} while (cursor.moveToNext())
}
if (!initialized) entryNotFound()
return File(pluginDir, options.id).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}")
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/PluginOptions.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin
import java.util.*
/**
* Helper class for processing plugin options.
*
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
*/
class PluginOptions : HashMap {
var id = ""
constructor() : super()
constructor(initialCapacity: Int) : super(initialCapacity)
constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor)
private constructor(options: String?, parseId: Boolean) : this() {
@Suppress("NAME_SHADOWING")
var parseId = parseId
if (options.isNullOrEmpty()) return
check(options.all { !it.isISOControl() }) { "No control characters allowed." }
val tokenizer = StringTokenizer("$options;", "\\=;", true)
val current = StringBuilder()
var key: String? = null
while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
"\\" -> current.append(tokenizer.nextToken())
"=" -> if (key == null) {
key = current.toString()
current.setLength(0)
} else current.append(nextToken)
";" -> {
if (key != null) {
put(key, current.toString())
key = null
} else if (current.isNotEmpty()) {
if (parseId) id = current.toString() else put(current.toString(), null)
}
current.setLength(0)
parseId = false
}
else -> current.append(nextToken)
}
}
constructor(options: String?) : this(options, true)
constructor(id: String, options: String?) : this(options, false) {
this.id = id
}
/**
* Put but if value is null or default, the entry is deleted.
*
* @return Old value before put.
*/
fun putWithDefault(key: String, value: String?, default: String? = null) =
if (value == null || value == default) remove(key) else put(key, value)
private fun append(result: StringBuilder, str: String) = str.indices.map { str[it] }.forEach {
when (it) {
'\\', '=', ';' -> {
result.append('\\') // intentionally no break
result.append(it)
}
else -> result.append(it)
}
}
fun toString(trimId: Boolean): String {
val result = StringBuilder()
if (!trimId) if (id.isEmpty()) return "" else append(result, id)
for ((key, value) in entries) {
if (result.isNotEmpty()) result.append(';')
append(result, key)
if (value != null) {
result.append('=')
append(result, value)
}
}
return result.toString()
}
override fun toString(): String = toString(true)
override fun equals(other: Any?): Boolean {
if (this === other) return true
return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id
}
override fun hashCode(): Int = Objects.hash(super.hashCode(), id)
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/ResolvedPlugin.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.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 idAliases: Array by lazy {
when (val value = componentInfo.metaData.get(PluginContract.METADATA_KEY_ID_ALIASES)) {
is String -> arrayOf(value)
is Int -> SagerNet.application.packageManager.getResourcesForApplication(componentInfo.applicationInfo)
.run {
when (getResourceTypeName(value)) {
"string" -> arrayOf(getString(value))
else -> getStringArray(value)
}
}
null -> emptyArray()
else -> error("unknown type for plugin meta-data idAliases")
}
}
override val label: CharSequence get() = resolveInfo.loadLabel(SagerNet.application.packageManager)
override val icon: Drawable get() = resolveInfo.loadIcon(SagerNet.application.packageManager)
override val defaultConfig by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
override val packageName: String get() = componentInfo.packageName
override val trusted by lazy {
SagerNet.application.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
}
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/Utils.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
@file:JvmName("Utils")
package com.github.shadowsocks.plugin
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class Empty : Parcelable
================================================
FILE: app/src/main/java/com/github/shadowsocks/plugin/fragment/AlertDialogFragment.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.plugin.fragment
import android.app.Activity
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* Based on: https://android.googlesource.com/platform/
packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
*/
abstract class AlertDialogFragment :
AppCompatDialogFragment(), DialogInterface.OnClickListener {
companion object {
private const val KEY_RESULT = "result"
private const val KEY_ARG = "arg"
private const val KEY_RET = "ret"
private const val KEY_WHICH = "which"
fun setResultListener(fragment: Fragment, requestKey: String,
listener: (Int, Ret?) -> Unit) {
fragment.setFragmentResultListener(requestKey) { _, bundle ->
listener(bundle.getInt(KEY_WHICH, Activity.RESULT_CANCELED), bundle.getParcelable(KEY_RET))
}
}
inline fun , Ret : Parcelable?> setResultListener(
fragment: Fragment, noinline listener: (Int, Ret?) -> Unit) =
setResultListener(fragment, T::class.java.name, listener)
}
protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
private val resultKey get() = requireArguments().getString(KEY_RESULT)
protected val arg by lazy { requireArguments().getParcelable(KEY_ARG)!! }
protected open fun ret(which: Int): Ret? = null
private fun args() = arguments ?: Bundle().also { arguments = it }
fun arg(arg: Arg) = args().putParcelable(KEY_ARG, arg)
fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create()
override fun onClick(dialog: DialogInterface?, which: Int) {
setFragmentResult(resultKey ?: return, Bundle().apply {
putInt(KEY_WHICH, which)
putParcelable(KEY_RET, ret(which) ?: return@apply)
})
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onClick(null, Activity.RESULT_CANCELED)
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.preference
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity
import io.nekohasekai.sagernet.ui.profile.TrojanGoSettingsActivity
class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() {
companion object {
private const val PLUGIN_ID_FRAGMENT_TAG =
"com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID"
}
fun setArg(key: String, plugin: String) {
arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key,
PLUGIN_ID_FRAGMENT_TAG to plugin)
}
private lateinit var editText: EditText
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
val intent = PluginManager.buildIntent(arguments?.getString(PLUGIN_ID_FRAGMENT_TAG)!!,
PluginContract.ACTION_HELP)
val activity = activity
if (activity is ShadowsocksSettingsActivity) {
if (intent.resolveActivity(app.packageManager) != null) builder.setNeutralButton("?") { _, _ ->
activity.pluginHelp.launch(intent.putExtra(PluginContract.EXTRA_OPTIONS,
editText.text.toString()))
}
} else {
activity as TrojanGoSettingsActivity
if (intent.resolveActivity(app.packageManager) != null) builder.setNeutralButton(
"?") { _, _ ->
activity.pluginHelp.launch(intent.putExtra(PluginContract.EXTRA_OPTIONS,
editText.text.toString()))
}
}
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
editText = view.findViewById(android.R.id.edit)
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/preference/PluginPreference.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.preference
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.preference.ListPreference
import com.github.shadowsocks.plugin.PluginList
import com.github.shadowsocks.plugin.PluginManager
import io.nekohasekai.sagernet.R
class PluginPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(
context, attrs
) {
companion object FallbackProvider : SummaryProvider {
override fun provideSummary(preference: PluginPreference) = preference.selectedEntry?.label
?: preference.unknownValueSummary.format(preference.value)
}
lateinit var plugins: PluginList
val selectedEntry get() = plugins.lookup[value]
private val entryIcon: Drawable? get() = selectedEntry?.icon
private val unknownValueSummary = context.getString(R.string.plugin_unknown)
private var listener: OnPreferenceChangeListener? = null
override fun getOnPreferenceChangeListener(): OnPreferenceChangeListener? = listener
override fun setOnPreferenceChangeListener(listener: OnPreferenceChangeListener?) {
this.listener = listener
}
init {
super.setOnPreferenceChangeListener { preference, newValue ->
val listener = listener
if (listener == null || listener.onPreferenceChange(preference, newValue)) {
value = newValue.toString()
icon = entryIcon
true
} else false
}
}
fun init(skipInternal: Boolean = false) {
plugins = PluginManager.fetchPlugins(skipInternal)
entryValues = plugins.lookup.map { it.key }.toTypedArray()
icon = entryIcon
summaryProvider = FallbackProvider
}
override fun onSetInitialValue(defaultValue: Any?) {
super.onSetInitialValue(defaultValue)
init()
}
}
================================================
FILE: app/src/main/java/com/github/shadowsocks/preference/PluginPreferenceDialogFragment.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package com.github.shadowsocks.preference
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResult
import androidx.preference.PreferenceDialogFragmentCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.plugin.Plugin
import com.google.android.material.bottomsheet.BottomSheetDialog
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.databinding.LayoutIconListItem2Binding
class PluginPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
companion object {
const val KEY_SELECTED_ID = "id"
}
private inner class IconListViewHolder(val dialog: BottomSheetDialog, binding: LayoutIconListItem2Binding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener, View.OnLongClickListener {
private lateinit var plugin: Plugin
private val text1 = binding.text1
private val text2 = binding.text2
private val icon = binding.icon
private val unlock = binding.unlock.apply {
TooltipCompat.setTooltipText(this, getText(R.string.plugin_auto_connect_unlock_only))
}
init {
binding.root.setOnClickListener(this)
binding.root.setOnLongClickListener(this)
}
fun bind(plugin: Plugin, selected: Boolean = false) {
this.plugin = plugin
val label = plugin.label
text1.text = label
text2.text = plugin.id
val typeface = if (selected) Typeface.BOLD else Typeface.NORMAL
text1.setTypeface(null, typeface)
text2.setTypeface(null, typeface)
text2.isVisible = plugin.id.isNotEmpty() && label != plugin.id
icon.setImageDrawable(plugin.icon)
unlock.isGone = plugin.directBootAware || !DataStore.persistAcrossReboot
}
override fun onClick(v: View?) {
clicked = plugin
dialog.dismiss()
}
override fun onLongClick(v: View?) = try {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.Builder()
.scheme("package").opaquePart(plugin.packageName).build()))
true
} catch (_: ActivityNotFoundException) {
false
}
}
private inner class IconListAdapter(private val dialog: BottomSheetDialog) : RecyclerView.Adapter() {
override fun getItemCount(): Int = preference.plugins.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
IconListViewHolder(dialog, LayoutIconListItem2Binding.inflate(layoutInflater, parent, false))
override fun onBindViewHolder(holder: IconListViewHolder, position: Int) {
if (selected < 0) holder.bind(preference.plugins[position]) else when (position) {
0 -> holder.bind(preference.selectedEntry!!, true)
in selected + 1..Int.MAX_VALUE -> holder.bind(preference.plugins[position])
else -> holder.bind(preference.plugins[position - 1])
}
}
}
fun setArg(key: String) {
arguments = bundleOf(ARG_KEY to key)
}
private val preference by lazy { getPreference() as PluginPreference }
private val selected by lazy { preference.plugins.indexOf(preference.selectedEntry) }
private var clicked: Plugin? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity()
val dialog = BottomSheetDialog(activity, theme)
val recycler = RecyclerView(activity)
val padding = resources.getDimensionPixelOffset(R.dimen.bottom_sheet_padding)
recycler.setPadding(0, padding, 0, padding)
recycler.setHasFixedSize(true)
recycler.layoutManager = LinearLayoutManager(activity)
recycler.itemAnimator = DefaultItemAnimator()
recycler.adapter = IconListAdapter(dialog)
recycler.layoutParams =
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
dialog.setContentView(recycler)
return dialog
}
override fun onDialogClosed(positiveResult: Boolean) {
val clicked = clicked
if (clicked != null && clicked != preference.selectedEntry) {
setFragmentResult(javaClass.name, bundleOf(KEY_SELECTED_ID to clicked.id))
}
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/BootReceiver.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import io.nekohasekai.sagernet.bg.SubscriptionUpdater
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
class BootReceiver : BroadcastReceiver() {
companion object {
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
var enabled: Boolean
get() = app.packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
set(value) = app.packageManager.setComponentEnabledSetting(
componentName, if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
}
override fun onReceive(context: Context, intent: Intent) {
runOnDefaultDispatcher {
SubscriptionUpdater.reconfigureUpdater()
}
if (!DataStore.persistAcrossReboot) { // sanity check
enabled = false
return
}
val doStart = when (intent.action) {
Intent.ACTION_LOCKED_BOOT_COMPLETED -> DataStore.directBootAware
else -> Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked
} && DataStore.currentProfile > 0
if (doStart) SagerNet.startService()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/Constants.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet
const val CONNECTION_TEST_URL = "https://api.v2fly.org/checkConnection.svgz"
object Key {
const val DB_PUBLIC = "configuration.db"
const val DB_PROFILE = "sager_net.db"
const val DISABLE_AEAD = "V2RAY_VMESS_AEAD_DISABLED"
const val PERSIST_ACROSS_REBOOT = "isAutoConnect"
const val DIRECT_BOOT_AWARE = "directBootAware"
const val APP_THEME = "appTheme"
const val NIGHT_THEME = "nightTheme"
const val SERVICE_MODE = "serviceMode"
const val MODE_VPN = "vpn"
const val MODE_PROXY = "proxy"
const val REMOTE_DNS = "remoteDns"
const val DIRECT_DNS = "directDns"
const val ENABLE_DNS_ROUTING = "enableDnsRouting"
const val ENABLE_FAKEDNS = "enableFakeDns"
const val DNS_HOSTS = "dnsHosts"
const val IPV6_MODE = "ipv6Mode"
const val PROXY_APPS = "proxyApps"
const val BYPASS_MODE = "bypassMode"
const val INDIVIDUAL = "individual"
const val METERED_NETWORK = "meteredNetwork"
const val DOMAIN_STRATEGY = "domainStrategy"
const val TRAFFIC_SNIFFING = "trafficSniffing"
const val DESTINATION_OVERRIDE = "destinationOverride"
const val RESOLVE_DESTINATION = "resolveDestination"
const val BYPASS_LAN = "bypassLan"
const val BYPASS_LAN_IN_CORE_ONLY = "bypassLanInCoreOnly"
const val SOCKS_PORT = "socksPort"
const val ALLOW_ACCESS = "allowAccess"
const val SPEED_INTERVAL = "speedInterval"
const val SHOW_DIRECT_SPEED = "showDirectSpeed"
const val LOCAL_DNS_PORT = "portLocalDns"
const val REQUIRE_HTTP = "requireHttp"
const val APPEND_HTTP_PROXY = "appendHttpProxy"
const val HTTP_PORT = "httpPort"
const val REQUIRE_TRANSPROXY = "requireTransproxy"
const val TRANSPROXY_MODE = "transproxyMode"
const val TRANSPROXY_PORT = "transproxyPort"
const val CONNECTION_TEST_URL = "connectionTestURL"
const val ENABLE_MUX = "enableMux"
const val ENABLE_MUX_FOR_ALL = "enableMuxForAll"
const val MUX_CONCURRENCY = "muxConcurrency"
const val SHOW_STOP_BUTTON = "showStopButton"
const val SECURITY_ADVISORY = "securityAdvisory"
const val TCP_KEEP_ALIVE_INTERVAL = "tcpKeepAliveInterval"
const val RULES_PROVIDER = "rulesProvider"
const val ENABLE_LOG = "enableLog"
const val ALWAYS_SHOW_ADDRESS = "alwaysShowAddress"
const val PROVIDER_TROJAN = "providerTrojan"
const val PROVIDER_SS_AEAD = "providerShadowsocksAEAD"
const val PROVIDER_SS_STREAM = "providerShadowsocksStream"
const val UTLS_FINGERPRINT = "utlsFingerprint"
const val TUN_IMPLEMENTATION = "tunImplementation"
const val ENABLE_PCAP = "enablePcap"
const val APP_TRAFFIC_STATISTICS = "appTrafficStatistics"
const val PROFILE_TRAFFIC_STATISTICS = "profileTrafficStatistics"
const val PROFILE_DIRTY = "profileDirty"
const val PROFILE_ID = "profileId"
const val PROFILE_NAME = "profileName"
const val PROFILE_GROUP = "profileGroup"
const val PROFILE_STARTED = "profileStarted"
const val PROFILE_CURRENT = "profileCurrent"
const val SERVER_ADDRESS = "serverAddress"
const val SERVER_PORT = "serverPort"
const val SERVER_USERNAME = "serverUsername"
const val SERVER_PASSWORD = "serverPassword"
const val SERVER_METHOD = "serverMethod"
const val SERVER_PLUGIN = "serverPlugin"
const val SERVER_PLUGIN_CONFIGURE = "serverPluginConfigure"
const val SERVER_PASSWORD1 = "serverPassword1"
const val SERVER_PROTOCOL = "serverProtocol"
const val SERVER_PROTOCOL_PARAM = "serverProtocolParam"
const val SERVER_OBFS = "serverObfs"
const val SERVER_OBFS_PARAM = "serverObfsParam"
const val SERVER_USER_ID = "serverUserId"
const val SERVER_ALTER_ID = "serverAlterId"
const val SERVER_SECURITY = "serverSecurity"
const val SERVER_NETWORK = "serverNetwork"
const val SERVER_HEADER = "serverHeader"
const val SERVER_HOST = "serverHost"
const val SERVER_PATH = "serverPath"
const val SERVER_SNI = "serverSNI"
const val SERVER_TLS = "serverTLS"
const val SERVER_ENCRYPTION = "serverEncryption"
const val SERVER_ALPN = "serverALPN"
const val SERVER_CERTIFICATES = "serverCertificates"
const val SERVER_FLOW = "serverFlow"
const val SERVER_QUIC_SECURITY = "serverQuicSecurity"
const val SERVER_WS_BROWSER_FORWARDING = "serverWsBrowserForwarding"
const val SERVER_CONFIG = "serverConfig"
const val SERVER_SECURITY_CATEGORY = "serverSecurityCategory"
const val SERVER_WS_CATEGORY = "serverWsCategory"
const val SERVER_SS_CATEGORY = "serverSsCategory"
const val SERVER_HEADERS = "serverHeaders"
const val SERVER_MULTI_MODE = "serverMultiMode"
const val SERVER_ALLOW_INSECURE = "serverAllowInsecure"
const val SERVER_AUTH_TYPE = "serverAuthType"
const val SERVER_UPLOAD_SPEED = "serverUploadSpeed"
const val SERVER_DOWNLOAD_SPEED = "serverDownloadSpeed"
const val SERVER_STREAM_RECEIVE_WINDOW = "serverStreamReceiveWindow"
const val SERVER_CONNECTION_RECEIVE_WINDOW = "serverConnectionReceiveWindow"
const val SERVER_DISABLE_MTU_DISCOVERY = "serverDisableMtuDiscovery"
const val SERVER_VMESS_EXPERIMENTS_CATEGORY = "serverVMessExperimentsCategory"
const val SERVER_VMESS_EXPERIMENTAL_AUTHENTICATED_LENGTH = "serverVMessExperimentalAuthenticatedLength"
const val SERVER_VMESS_EXPERIMENTAL_NO_TERMINATION_SIGNAL = "serverVMessExperimentalNoTerminationSignal"
const val SERVER_PRIVATE_KEY = "serverPrivateKey"
const val SERVER_LOCAL_ADDRESS = "serverLocalAddress"
const val BALANCER_TYPE = "balancerType"
const val BALANCER_GROUP = "balancerGroup"
const val BALANCER_STRATEGY = "balancerStrategy"
const val ROUTE_NAME = "routeName"
const val ROUTE_DOMAIN = "routeDomain"
const val ROUTE_IP = "routeIP"
const val ROUTE_PORT = "routePort"
const val ROUTE_SOURCE_PORT = "routeSourcePort"
const val ROUTE_NETWORK = "routeNetwork"
const val ROUTE_SOURCE = "routeSource"
const val ROUTE_PROTOCOL = "routeProtocol"
const val ROUTE_ATTRS = "routeAttrs"
const val ROUTE_OUTBOUND = "routeOutbound"
const val ROUTE_OUTBOUND_RULE = "routeOutboundRule"
const val ROUTE_REVERSE = "routeReverse"
const val ROUTE_REDIRECT = "routeRedirect"
const val ROUTE_PACKAGES = "routePackages"
const val ROUTE_FOREGROUND_STATUS = "routeForegroundStatus"
const val GROUP_NAME = "groupName"
const val GROUP_TYPE = "groupType"
const val GROUP_ORDER = "groupOrder"
const val GROUP_SUBSCRIPTION = "groupSubscription"
const val SUBSCRIPTION_TYPE = "subscriptionType"
const val SUBSCRIPTION_LINK = "subscriptionLink"
const val SUBSCRIPTION_TOKEN = "subscriptionToken"
const val SUBSCRIPTION_FORCE_RESOLVE = "subscriptionForceResolve"
const val SUBSCRIPTION_DEDUPLICATION = "subscriptionDeduplication"
const val SUBSCRIPTION_FORCE_VMESS_AEAD = "subscriptionForceVMessAEAD"
const val SUBSCRIPTION_UPDATE = "subscriptionUpdate"
const val SUBSCRIPTION_UPDATE_WHEN_CONNECTED_ONLY = "subscriptionUpdateWhenConnectedOnly"
const val SUBSCRIPTION_USER_AGENT = "subscriptionUserAgent"
const val SUBSCRIPTION_AUTO_UPDATE = "subscriptionAutoUpdate"
const val SUBSCRIPTION_AUTO_UPDATE_DELAY = "subscriptionAutoUpdateDelay"
}
object TunImplementation {
const val GVISOR = 0
const val LWIP = 1
}
object TrojanProvider {
const val V2RAY = 0
const val TROJAN = 1
const val TROJAN_GO = 2
}
object ShadowsocksProvider {
const val V2RAY = 0
const val SHADOWSOCKS_RUST = 1
const val CLASH = 2
const val SHADOWSOCKS_LIBEV = 3
}
object ShadowsocksStreamProvider {
const val SHADOWSOCKS_RUST = 0
const val CLASH = 1
const val SHADOWSOCKS_LIBEV = 2
}
object IPv6Mode {
const val DISABLE = 0
const val ENABLE = 1
const val PREFER = 2
const val ONLY = 3
}
object PacketStrategy {
const val DIRECT = 0
const val DROP = 1
const val REPLY = 2
}
object GroupType {
const val BASIC = 0
const val SUBSCRIPTION = 1
}
object SubscriptionType {
const val RAW = 0
const val OOCv1 = 1
const val SIP008 = 2
}
object ExtraType {
const val NONE = 0
const val OOCv1 = 1
const val SIP008 = 2
}
object GroupOrder {
const val ORIGIN = 0
const val BY_NAME = 1
const val BY_DELAY = 2
}
object AppStatus {
const val FOREGROUND = "foreground"
const val BACKGROUND = "background"
}
object Action {
const val SERVICE = "io.nekohasekai.sagernet.SERVICE"
const val CLOSE = "io.nekohasekai.sagernet.CLOSE"
const val RELOAD = "io.nekohasekai.sagernet.RELOAD"
const val ABORT = "io.nekohasekai.sagernet.ABORT"
const val EXTRA_PROFILE_ID = "io.nekohasekai.sagernet.EXTRA_PROFILE_ID"
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/QuickToggleShortcut.kt
================================================
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv *
* Copyright (C) 2017 by Mygod Studio *
* *
* 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 . *
* *
*******************************************************************************/
package io.nekohasekai.sagernet
import android.app.Activity
import android.content.Intent
import android.content.pm.ShortcutManager
import android.os.Build
import android.os.Bundle
import androidx.core.content.getSystemService
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import io.nekohasekai.sagernet.aidl.ISagerNetService
import io.nekohasekai.sagernet.bg.BaseService
import io.nekohasekai.sagernet.bg.SagerConnection
@Suppress("DEPRECATION")
class QuickToggleShortcut : Activity(), SagerConnection.Callback {
private val connection = SagerConnection()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
ShortcutInfoCompat.Builder(this, "toggle")
.setIntent(Intent(this,
QuickToggleShortcut::class.java).setAction(Intent.ACTION_MAIN))
.setIcon(IconCompat.createWithResource(this,
R.drawable.ic_qu_shadowsocks_launcher))
.setShortLabel(getString(R.string.quick_toggle))
.build()))
finish()
} else {
connection.connect(this, this)
if (Build.VERSION.SDK_INT >= 25) getSystemService()!!.reportShortcutUsed(
"toggle")
}
}
override fun onServiceDisconnected() {
super.onServiceDisconnected()
}
override fun onServiceConnected(service: ISagerNetService) {
val state = BaseService.State.values()[service.state]
when {
state.canStop -> SagerNet.stopService()
state == BaseService.State.Stopped -> SagerNet.startService()
}
finish()
}
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {}
override fun onDestroy() {
connection.disconnect(this)
super.onDestroy()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet
import android.annotation.SuppressLint
import android.app.*
import android.app.admin.DevicePolicyManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.ConnectivityManager
import android.os.Build
import android.os.StrictMode
import android.os.UserManager
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import go.Seq
import io.nekohasekai.sagernet.bg.SagerConnection
import io.nekohasekai.sagernet.bg.proto.UidDumper
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.SagerDatabase
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.checkMT
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
import io.nekohasekai.sagernet.ui.MainActivity
import io.nekohasekai.sagernet.utils.CrashHandler
import io.nekohasekai.sagernet.utils.DeviceStorageApp
import io.nekohasekai.sagernet.utils.PackageCache
import io.nekohasekai.sagernet.utils.Theme
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
import libcore.Libcore
import org.conscrypt.Conscrypt
import java.security.Security
import androidx.work.Configuration as WorkConfiguration
class SagerNet : Application(),
WorkConfiguration.Provider {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
application = this
}
val externalAssets by lazy { getExternalFilesDir(null) ?: filesDir }
override fun onCreate() {
super.onCreate()
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
Thread.setDefaultUncaughtExceptionHandler(CrashHandler)
DataStore.init()
updateNotificationChannels()
Seq.setContext(this)
externalAssets.mkdirs()
Libcore.initializeV2Ray(
filesDir.absolutePath + "/", externalAssets.absolutePath + "/", "v2ray/"
) {
DataStore.rulesProvider == 0
}
//Libcore.setenv("v2ray.conf.geoloader", "memconservative")
Libcore.setUidDumper(UidDumper)
runOnDefaultDispatcher {
PackageCache.register()
checkMT()
}
Theme.apply(this)
Theme.applyNightTheme()
Security.insertProviderAt(Conscrypt.newProvider(), 1)
if (BuildConfig.DEBUG) StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.penaltyLog()
.build()
)
}
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
)!!
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateNotificationChannels()
}
override fun getWorkManagerConfiguration(): WorkConfiguration {
return WorkConfiguration.Builder()
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
.build()
}
@SuppressLint("InlinedApi")
companion object {
@Volatile
var started = false
lateinit var application: SagerNet
val isTv by lazy {
uiMode.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
}
val deviceStorage by lazy {
if (Build.VERSION.SDK_INT < 24) application else DeviceStorageApp(application)
}
val configureIntent: (Context) -> PendingIntent by lazy {
{
PendingIntent.getActivity(
it,
0,
Intent(
application, MainActivity::class.java
).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
)
}
}
val activity by lazy { application.getSystemService()!! }
val clipboard by lazy { application.getSystemService()!! }
val connectivity by lazy { application.getSystemService()!! }
val notification by lazy { application.getSystemService()!! }
val user by lazy { application.getSystemService()!! }
val uiMode by lazy { application.getSystemService()!! }
val packageInfo: PackageInfo by lazy { application.getPackageInfo(application.packageName) }
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && try {
app.getSystemService()?.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
} catch (_: RuntimeException) {
false
}
}
val currentProfile get() = SagerDatabase.proxyDao.getById(DataStore.selectedProxy)
fun getClipboardText(): String {
return clipboard.primaryClip?.takeIf { it.itemCount > 0 }
?.getItemAt(0)?.text?.toString() ?: ""
}
fun trySetPrimaryClip(clip: String) = try {
clipboard.setPrimaryClip(ClipData.newPlainText(null, clip))
true
} catch (e: RuntimeException) {
Logs.w(e)
false
}
fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
notification.createNotificationChannels(
listOf(
NotificationChannel(
"service-vpn",
application.getText(R.string.service_vpn),
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN
else NotificationManager.IMPORTANCE_LOW
), // #1355
NotificationChannel(
"service-proxy",
application.getText(R.string.service_proxy),
NotificationManager.IMPORTANCE_LOW
), NotificationChannel(
"service-subscription",
application.getText(R.string.service_subscription),
NotificationManager.IMPORTANCE_DEFAULT
)
)
)
}
}
fun startService() = ContextCompat.startForegroundService(
application, Intent(application, SagerConnection.serviceClass)
)
fun reloadService() =
application.sendBroadcast(Intent(Action.RELOAD).setPackage(application.packageName))
fun stopService() =
application.sendBroadcast(Intent(Action.CLOSE).setPackage(application.packageName))
}
override fun onLowMemory() {
super.onLowMemory()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/aidl/AppStats.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.aidl
import android.os.Parcelable
import io.nekohasekai.sagernet.database.StatsEntity
import kotlinx.parcelize.Parcelize
@Parcelize
data class AppStats(
var packageName: String,
var uid: Int,
var tcpConnections: Int,
var udpConnections: Int,
var tcpConnectionsTotal: Int,
var udpConnectionsTotal: Int,
var uplink: Long,
var downlink: Long,
var uplinkTotal: Long,
var downlinkTotal: Long,
var deactivateAt: Int
) : Parcelable {
operator fun plusAssign(stats: StatsEntity) {
tcpConnectionsTotal += stats.tcpConnections
udpConnectionsTotal += stats.udpConnections
uplinkTotal += stats.uplink
downlinkTotal += stats.downlink
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/aidl/AppStatsList.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.aidl
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class AppStatsList(
var data: List
) : Parcelable
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/aidl/TrafficStats.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.aidl
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class TrafficStats(
// Bytes per second
var txRateProxy: Long = 0L,
var rxRateProxy: Long = 0L,
var txRateDirect: Long = 0L,
var rxRateDirect: Long = 0L,
// Bytes for the current session
// Outbound "bypass" usage is not counted
var txTotal: Long = 0L,
var rxTotal: Long = 0L,
) : Parcelable {
operator fun plus(other: TrafficStats) = TrafficStats(
txRateProxy + other.txRateProxy, rxRateProxy + other.rxRateProxy,
txRateDirect + other.txRateDirect, rxRateDirect + other.rxRateDirect,
txTotal + other.txTotal, rxTotal + other.rxTotal)
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/AbstractInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import java.io.Closeable
interface AbstractInstance : Closeable {
fun launch()
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import android.os.RemoteCallbackList
import android.os.RemoteException
import cn.hutool.json.JSONException
import io.nekohasekai.sagernet.Action
import io.nekohasekai.sagernet.BootReceiver
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.aidl.AppStatsList
import io.nekohasekai.sagernet.aidl.ISagerNetService
import io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback
import io.nekohasekai.sagernet.aidl.TrafficStats
import io.nekohasekai.sagernet.bg.proto.ProxyInstance
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.SagerDatabase
import io.nekohasekai.sagernet.fmt.TAG_SOCKS
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.plugin.PluginManager
import io.nekohasekai.sagernet.utils.PackageCache
import kotlinx.coroutines.*
import libcore.AppStats
import libcore.Libcore
import libcore.TrafficListener
import java.net.UnknownHostException
import com.github.shadowsocks.plugin.PluginManager as ShadowsocksPluginPluginManager
import io.nekohasekai.sagernet.aidl.AppStats as AidlAppStats
class BaseService {
enum class State(val canStop: Boolean = false) {
/**
* Idle state is only used by UI and will never be returned by BaseService.
*/
Idle,
Connecting(true),
Connected(true),
Stopping,
Stopped,
}
interface ExpectedException
class ExpectedExceptionWrapper(e: Exception) : Exception(e.localizedMessage, e),
ExpectedException
class Data internal constructor(private val service: Interface) {
var state = State.Stopped
var proxy: ProxyInstance? = null
var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) {
Intent.ACTION_SHUTDOWN -> service.persistStats()
Action.RELOAD -> service.forceLoad()
else -> service.stopRunner(keepState = false)
}
}
var closeReceiverRegistered = false
val binder = Binder(this)
var connectingJob: Job? = null
fun changeState(s: State, msg: String? = null) {
if (state == s && msg == null) return
binder.stateChanged(s, msg)
state = s
}
}
class Binder(private var data: Data? = null) : ISagerNetService.Stub(),
CoroutineScope,
AutoCloseable,
TrafficListener {
private val callbacks = object : RemoteCallbackList() {
override fun onCallbackDied(callback: ISagerNetServiceCallback?, cookie: Any?) {
super.onCallbackDied(callback, cookie)
stopListeningForBandwidth(callback ?: return)
stopListeningForStats(callback)
}
}
private val bandwidthListeners = mutableMapOf() // the binder is the real identifier
private val statsListeners = mutableMapOf() // the binder is the real identifier
override val coroutineContext = Dispatchers.Main.immediate + Job()
private var looper: Job? = null
private var statsLooper: Job? = null
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
override fun getProfileName(): String = data?.proxy?.profile?.displayName() ?: "Idle"
override fun registerCallback(cb: ISagerNetServiceCallback) {
callbacks.register(cb)
}
fun broadcast(work: (ISagerNetServiceCallback) -> Unit) {
val count = callbacks.beginBroadcast()
try {
repeat(count) {
try {
work(callbacks.getBroadcastItem(it))
} catch (_: RemoteException) {
} catch (e: Exception) {
}
}
} finally {
callbacks.finishBroadcast()
}
}
private suspend fun loop() {
var lastQueryTime = 0L
val showDirectSpeed = DataStore.showDirectSpeed
while (true) {
val delayMs = bandwidthListeners.values.minOrNull()
delay(delayMs ?: return)
if (delayMs == 0L) return
val queryTime = System.currentTimeMillis()
val sinceLastQueryInSeconds = (queryTime - lastQueryTime).toDouble() / 1000L
val proxy = data?.proxy ?: continue
lastQueryTime = queryTime
val (statsOut, outs) = proxy.outboundStats()
val stats = TrafficStats(
(proxy.uplinkProxy / sinceLastQueryInSeconds).toLong(),
(proxy.downlinkProxy / sinceLastQueryInSeconds).toLong(),
if (showDirectSpeed) (proxy.uplinkDirect() / sinceLastQueryInSeconds).toLong() else 0L,
if (showDirectSpeed) (proxy.downlinkDirect() / sinceLastQueryInSeconds).toLong() else 0L,
statsOut.uplinkTotal,
statsOut.downlinkTotal
)
if (data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
broadcast { item ->
if (bandwidthListeners.contains(item.asBinder())) {
item.trafficUpdated(proxy.profile.id, stats, true)
outs.forEach { (profileId, stats) ->
item.trafficUpdated(
profileId, TrafficStats(
txRateDirect = stats.uplinkTotal,
rxTotal = stats.downlinkTotal
), false
)
}
}
}
}
}
}
val appStats = ArrayList()
override fun updateStats(t: AppStats) {
appStats.add(t)
}
private suspend fun loopStats() {
var lastQueryTime = 0L
val tun = (data?.proxy?.service as? VpnService)?.getTun() ?: return
if (!tun.trafficStatsEnabled) return
while (true) {
val delayMs = statsListeners.values.minOrNull()
if (delayMs == 0L) return
val queryTime = System.currentTimeMillis()
val sinceLastQueryInSeconds = ((queryTime - lastQueryTime).toDouble() / 1000).toLong()
lastQueryTime = queryTime
appStats.clear()
tun.readAppTraffics(this)
val statsList = AppStatsList(appStats.map {
val uid = if (it.uid >= 10000) it.uid else 1000
val packageName = if (uid != 1000) {
PackageCache.uidMap[it.uid]?.iterator()?.next() ?: "android"
} else {
"android"
}
AidlAppStats(
packageName,
uid, it.tcpConn, it.udpConn, it.tcpConnTotal, it.udpConnTotal,
it.uplink / sinceLastQueryInSeconds,
it.downlink / sinceLastQueryInSeconds,
it.uplinkTotal,
it.downlinkTotal,
it.deactivateAt
)
})
if (data?.state == State.Connected && statsListeners.isNotEmpty()) {
broadcast { item ->
if (statsListeners.contains(item.asBinder())) {
item.statsUpdated(statsList)
}
}
}
delay(delayMs ?: return)
}
}
override fun startListeningForBandwidth(
cb: ISagerNetServiceCallback,
timeout: Long,
) {
launch {
if (bandwidthListeners.isEmpty() and (bandwidthListeners.put(
cb.asBinder(), timeout
) == null)
) {
check(looper == null)
looper = launch { loop() }
}
if (data?.state != State.Connected) return@launch
val data = data
data?.proxy ?: return@launch
val sum = TrafficStats()
cb.trafficUpdated(0, sum, true)
}
}
override fun stopListeningForBandwidth(cb: ISagerNetServiceCallback) {
launch {
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
looper!!.cancel()
looper = null
}
}
}
override fun unregisterCallback(cb: ISagerNetServiceCallback) {
stopListeningForBandwidth(cb) // saves an RPC, and safer
stopListeningForStats(cb)
callbacks.unregister(cb)
}
override fun protect(fd: Int) {
(data?.proxy?.service as VpnService?)?.protect(fd)
}
override fun urlTest(): Int {
if (data?.proxy?.v2rayPoint == null) {
error("core not started")
}
try {
return Libcore.urlTestV2ray(
data!!.proxy!!.v2rayPoint, TAG_SOCKS, DataStore.connectionTestURL, 5000
)
} catch (e: Exception) {
var msg = e.readableMessage
if (msg.lowercase().contains("timeout")) {
msg = app.getString(R.string.connection_test_timeout)
} else if (msg.lowercase().contains("refused")) {
msg = app.getString(R.string.connection_test_refused)
}
error(msg)
}
}
override fun startListeningForStats(cb: ISagerNetServiceCallback, timeout: Long) {
launch {
if (statsListeners.isEmpty() and (statsListeners.put(
cb.asBinder(), timeout
) == null)
) {
check(statsLooper == null)
statsLooper = launch { loopStats() }
}
}
}
override fun stopListeningForStats(cb: ISagerNetServiceCallback) {
launch {
if (statsListeners.remove(cb.asBinder()) != null && statsListeners.isEmpty()) {
statsLooper!!.cancel()
statsLooper = null
}
}
}
override fun resetTrafficStats() {
runOnDefaultDispatcher {
SagerDatabase.statsDao.deleteAll()
(data?.proxy?.service as? VpnService)?.getTun()?.resetAppTraffics()
val empty = AppStatsList(emptyList())
broadcast { item ->
if (statsListeners.contains(item.asBinder())) {
item.statsUpdated(empty)
}
}
}
}
fun stateChanged(s: State, msg: String?) = launch {
val profileName = profileName
broadcast { it.stateChanged(s.ordinal, profileName, msg) }
}
fun profilePersisted(ids: List) = launch {
if (bandwidthListeners.isNotEmpty() && ids.isNotEmpty()) broadcast { item ->
if (bandwidthListeners.contains(item.asBinder())) ids.forEach(item::profilePersisted)
}
}
fun missingPlugin(pluginName: String) = launch {
val profileName = profileName
broadcast { it.missingPlugin(profileName, pluginName) }
}
override fun getTrafficStatsEnabled(): Boolean {
return (data?.proxy?.service as? VpnService)?.getTun()?.trafficStatsEnabled ?: false
}
override fun close() {
callbacks.kill()
cancel()
data = null
}
}
interface Interface {
val data: Data
val tag: String
fun createNotification(profileName: String): ServiceNotification
fun onBind(intent: Intent): IBinder? =
if (intent.action == Action.SERVICE) data.binder else null
fun forceLoad() {
if (DataStore.selectedProxy == 0L) {
stopRunner(false, (this as Context).getString(R.string.profile_empty))
}
val s = data.state
when {
s == State.Stopped -> startRunner()
s.canStop -> stopRunner(true)
else -> Logs.w("Illegal state $s when invoking use")
}
}
val isVpnService get() = false
suspend fun startProcesses() {
data.proxy!!.launch()
}
fun startRunner() {
this as Context
if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))
else startService(Intent(this, javaClass))
}
fun killProcesses() {
data.proxy?.close()
}
fun stopRunner(restart: Boolean = false, msg: String? = null, keepState: Boolean = true) {
if (data.state == State.Stopping) return
data.notification?.destroy()
data.notification = null
this as Service
data.changeState(State.Stopping)
runOnMainDispatcher {
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
// we use a coroutineScope here to allow clean-up in parallel
coroutineScope {
killProcesses()
val data = data
if (data.closeReceiverRegistered) {
unregisterReceiver(data.closeReceiver)
data.closeReceiverRegistered = false
}
data.binder.profilePersisted(listOfNotNull(data.proxy).map { it.profile.id })
data.proxy = null
}
// change the state
data.changeState(State.Stopped, msg)
// stop the service if nothing has bound to it
if (restart) startRunner() else { // BootReceiver.enabled = false
stopSelf()
}
}
}
fun persistStats() {
Logs.w(Exception())
data.proxy?.persistStats()
(this as? VpnService)?.persistAppStats()
}
suspend fun preInit() {}
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data
if (data.state != State.Stopped) return Service.START_NOT_STICKY
val profile = SagerDatabase.proxyDao.getById(DataStore.selectedProxy)
this as Context
if (profile == null) { // gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
data.notification = createNotification("")
stopRunner(false, getString(R.string.profile_empty))
return Service.START_NOT_STICKY
}
val proxy = ProxyInstance(profile, this)
data.proxy = proxy
BootReceiver.enabled = DataStore.persistAcrossReboot
if (!data.closeReceiverRegistered) {
registerReceiver(data.closeReceiver, IntentFilter().apply {
addAction(Action.RELOAD)
addAction(Intent.ACTION_SHUTDOWN)
addAction(Action.CLOSE)
}, "$packageName.SERVICE", null)
data.closeReceiverRegistered = true
}
data.notification = createNotification(profile.displayName())
data.changeState(State.Connecting)
runOnMainDispatcher {
try {
Executable.killAll() // clean up old processes
preInit()
try {
proxy.init()
} catch (jsonEx: JSONException) {
error(jsonEx.readableMessage.replace("cn.hutool.json.", ""))
}
proxy.processes = GuardedProcessPool {
Logs.w(it)
stopRunner(false, it.readableMessage)
}
DataStore.currentProfile = profile.id
DataStore.startedProfile = profile.id
startProcesses()
data.changeState(State.Connected)
for ((type, routeName) in proxy.config.alerts) {
data.binder.broadcast {
it.routeAlert(type, routeName)
}
}
} catch (_: CancellationException) { // if the job was cancelled, it is canceller's responsibility to call stopRunner
} catch (_: UnknownHostException) {
stopRunner(false, getString(R.string.invalid_server))
} catch (e: PluginManager.PluginNotFoundException) {
Logs.d(e.readableMessage)
data.binder.missingPlugin(e.plugin)
stopRunner(false, null)
} catch (e: ShadowsocksPluginPluginManager.PluginNotFoundException) {
Logs.d(e.readableMessage)
data.binder.missingPlugin("shadowsocks-" + e.plugin)
stopRunner(false, null)
} catch (exc: Throwable) {
if (exc is ExpectedException) Logs.d(exc.readableMessage) else Logs.w(exc)
stopRunner(
false, "${getString(R.string.service_failed)}: ${exc.readableMessage}"
)
} finally {
data.connectingJob = null
}
}
return Service.START_NOT_STICKY
}
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/ClashBasedInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import libcore.ClashBasedInstance
abstract class ClashBasedInstance : AbstractInstance {
lateinit var instance: ClashBasedInstance
abstract fun createInstance()
override fun launch() {
createInstance()
instance.start()
}
override fun close() {
if (::instance.isInitialized) instance.close()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.text.TextUtils
import io.nekohasekai.sagernet.ktx.Logs
import java.io.File
import java.io.IOException
object Executable {
const val SS_LOCAL = "libsslocal.so"
const val SS_LIBEV_LOCAL = "libss-local.so"
private val EXECUTABLES = setOf(
SS_LOCAL,
SS_LIBEV_LOCAL,
"libtrojan.so",
"libtrojan-go.so",
"libnaive.so",
"libbrook.so",
"libhysteria.so",
"libpingtunnel.so",
"librelaybaton.so",
"libwg.so"
)
fun killAll() {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }
?: return) {
val exe = File(try {
File(process, "cmdline").inputStream().bufferedReader().use {
it.readText()
}
} catch (_: IOException) {
continue
}.split(Character.MIN_VALUE, limit = 2).first())
if (EXECUTABLES.contains(exe.name)) try {
Os.kill(process.name.toInt(), OsConstants.SIGKILL)
Logs.w("SIGKILL ${exe.nameWithoutExtension} (${process.name}) succeed")
} catch (e: ErrnoException) {
if (e.errno != OsConstants.ESRCH) {
Logs.w("SIGKILL ${exe.absolutePath} (${process.name}) failed")
Logs.w(e)
}
}
}
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/ExternalInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import io.nekohasekai.sagernet.bg.proto.V2RayInstance
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.fmt.buildCustomConfig
import io.nekohasekai.sagernet.ktx.Logs
class ExternalInstance(
profile: ProxyEntity, val port: Int
) : V2RayInstance(profile) {
override fun init() {
super.init()
Logs.d(config.config)
pluginConfigs.forEach { (_, plugin) ->
val (_, content) = plugin
Logs.d(content)
}
}
override fun buildConfig() {
config = buildCustomConfig(profile, port)
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/ForegroundDetectorService.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent
import android.view.inputmethod.InputMethodManager
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.utils.PackageCache
import libcore.Libcore
class ForegroundDetectorService : AccessibilityService() {
class NotStartedException(val routeName: String) : IllegalStateException()
val imeApps by lazy {
(applicationContext.getSystemService(
INPUT_METHOD_SERVICE
) as InputMethodManager).inputMethodList.map { it.packageName }
}
var fromIme = false
override fun onCreate() {
super.onCreate()
Logs.i("Started")
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
val packageName = event.packageName?.takeIf { it.isNotBlank() }?.toString() ?: return
if (packageName == "com.android.systemui") return
if (packageName in imeApps) {
val uid = PackageCache[packageName] ?: return
PackageCache.awaitLoadSync()
Libcore.setForegroundImeUid(uid)
fromIme = true
Logs.d("Foreground IME changed to ${event.packageName}/${event.className}: uid $uid")
return
}
PackageCache.awaitLoadSync()
var uid = PackageCache[packageName] ?: -1
if (uid < 10000) {
uid = 1000
}
Libcore.setForegroundUid(uid)
if (fromIme) {
Libcore.setForegroundImeUid(0)
fromIme = false
Logs.d("Foreground IME changed to none")
}
Logs.d("Foreground changed to ${event.packageName}/${event.className}: uid $uid")
}
override fun onInterrupt() {
Logs.i("Interrupted")
Libcore.setForegroundUid(0)
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/GuardedProcessPool.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.os.Build
import android.os.SystemClock
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.util.Log
import androidx.annotation.MainThread
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.utils.Commandline
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.io.File
import java.io.IOException
import java.io.InputStream
import kotlin.concurrent.thread
class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope {
companion object {
private val pid by lazy {
Class.forName("java.lang.ProcessManager\$ProcessImpl").getDeclaredField("pid")
.apply { isAccessible = true }
}
}
private inner class Guard(private val cmd: List, private val env: Map = mapOf()) {
private lateinit var process: Process
private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
input.bufferedReader().forEachLine(logger)
} catch (_: IOException) {
} // ignore
fun start() {
process = ProcessBuilder(cmd).directory(SagerNet.deviceStorage.noBackupFilesDir).apply {
environment().putAll(env)
}.start()
}
@DelicateCoroutinesApi
suspend fun looper(onRestartCallback: (suspend () -> Unit)?) {
var running = false
val cmdName = File(cmd.first()).nameWithoutExtension
val exitChannel = Channel()
try {
while (true) {
thread(name = "stderr-$cmdName") {
streamLogger(process.errorStream) { Log.e(cmdName, it) }
}
thread(name = "stdout-$cmdName") {
streamLogger(process.inputStream) { Log.i(cmdName, it) }
// this thread also acts as a daemon thread for waitFor
runBlocking { exitChannel.send(process.waitFor()) }
}
val startTime = SystemClock.elapsedRealtime()
val exitCode = exitChannel.receive()
running = false
when {
SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException(
"$cmdName exits too fast (exit code: $exitCode)")
exitCode == 128 + OsConstants.SIGKILL -> Logs.w("$cmdName was killed")
else -> Logs.w(IOException("$cmdName unexpectedly exits with code $exitCode"))
}
Logs.i("restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)")
start()
running = true
onRestartCallback?.invoke()
}
} catch (e: IOException) {
Logs.w("error occurred. stop guard: ${Commandline.toString(cmd)}")
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
} finally {
if (running) withContext(NonCancellable) { // clean-up cannot be cancelled
if (Build.VERSION.SDK_INT < 24) {
try {
Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)
} catch (e: ErrnoException) {
if (e.errno != OsConstants.ESRCH) Logs.w(e)
} catch (e: ReflectiveOperationException) {
Logs.w(e)
}
if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return@withContext
}
process.destroy() // kill the process
if (Build.VERSION.SDK_INT >= 26) {
if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return@withContext
process.destroyForcibly() // Force to kill the process if it's still alive
}
exitChannel.receive()
} // otherwise process already exited, nothing to be done
}
}
}
override val coroutineContext = Dispatchers.Main.immediate + Job()
@MainThread
fun start(cmd: List,env: Map = mapOf(), onRestartCallback: (suspend () -> Unit)? = null) {
Logs.i("start process: ${Commandline.toString(cmd)}")
Guard(cmd, env).apply {
start() // if start fails, IOException will be thrown directly
launch { looper(onRestartCallback) }
}
}
@MainThread
fun close(scope: CoroutineScope) {
cancel()
coroutineContext[Job]!!.also { job -> scope.launch { job.cancelAndJoin() } }
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/ProxyService.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.app.Service
import android.content.Intent
class ProxyService : Service(), BaseService.Interface {
override val data = BaseService.Data(this)
override val tag: String get() = "SagerNetProxyService"
override fun createNotification(profileName: String): ServiceNotification =
ServiceNotification(this, profileName, "service-proxy", true)
override fun onBind(intent: Intent) = super.onBind(intent)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super.onStartCommand(intent, flags, startId)
override fun onDestroy() {
super.onDestroy()
data.binder.close()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.RemoteException
import io.nekohasekai.sagernet.Action
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.aidl.*
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.runOnMainDispatcher
class SagerConnection(private var listenForDeath: Boolean = false) : ServiceConnection,
IBinder.DeathRecipient {
companion object {
val serviceClass
get() = when (DataStore.serviceMode) {
Key.MODE_PROXY -> ProxyService::class
Key.MODE_VPN -> VpnService::class // Key.MODE_TRANS -> TransproxyService::class
else -> throw UnknownError()
}.java
}
interface Callback {
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
fun trafficUpdated(profileId: Long, stats: TrafficStats, isCurrent: Boolean) {}
fun statsUpdated(stats: List) {}
fun observatoryResultsUpdated(groupId: Long) {}
fun profilePersisted(profileId: Long) {}
fun missingPlugin(profileName: String, pluginName: String) {}
fun routeAlert(type: Int, routeName: String) {}
fun onServiceConnected(service: ISagerNetService)
/**
* Different from Android framework, this method will be called even when you call `detachService`.
*/
fun onServiceDisconnected() {}
fun onBinderDied() {}
}
private var connectionActive = false
private var callbackRegistered = false
private var callback: Callback? = null
private val serviceCallback = object : ISagerNetServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
val s = BaseService.State.values()[state]
SagerNet.started = s.canStop
val callback = callback ?: return
runOnMainDispatcher {
callback.stateChanged(s, profileName, msg)
}
}
override fun trafficUpdated(profileId: Long, stats: TrafficStats, isCurrent: Boolean) {
val callback = callback ?: return
runOnMainDispatcher {
callback.trafficUpdated(profileId, stats, isCurrent)
}
}
override fun profilePersisted(profileId: Long) {
val callback = callback ?: return
runOnMainDispatcher { callback.profilePersisted(profileId) }
}
override fun missingPlugin(profileName: String, pluginName: String) {
val callback = callback ?: return
runOnMainDispatcher {
callback.missingPlugin(profileName, pluginName)
}
}
override fun statsUpdated(statsList: AppStatsList) {
val callback = callback ?: return
callback.statsUpdated(statsList.data)
}
override fun routeAlert(type: Int, routeName: String) {
val callback = callback ?: return
runOnMainDispatcher {
callback.routeAlert(type, routeName)
}
}
override fun observatoryResultsUpdated(groupId: Long) {
val callback = callback ?: return
runOnMainDispatcher {
callback.observatoryResultsUpdated(groupId)
}
}
}
private var binder: IBinder? = null
var bandwidthTimeout = 0L
set(value) {
try {
if (value > 0) service?.startListeningForBandwidth(serviceCallback, value)
else service?.stopListeningForBandwidth(serviceCallback)
} catch (_: RemoteException) {
}
field = value
}
var trafficTimeout = 0L
set(value) {
try {
if (value > 0) service?.startListeningForStats(serviceCallback, value)
else service?.stopListeningForStats(serviceCallback)
} catch (_: RemoteException) {
}
field = value
}
var service: ISagerNetService? = null
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
this.binder = binder
val service = ISagerNetService.Stub.asInterface(binder)!!
this.service = service
try {
if (listenForDeath) binder.linkToDeath(this, 0)
check(!callbackRegistered)
service.registerCallback(serviceCallback)
callbackRegistered = true
if (bandwidthTimeout > 0) service.startListeningForBandwidth(
serviceCallback, bandwidthTimeout
)
if (trafficTimeout > 0) service.startListeningForStats(
serviceCallback, trafficTimeout
)
} catch (e: RemoteException) {
e.printStackTrace()
}
callback!!.onServiceConnected(service)
}
override fun onServiceDisconnected(name: ComponentName?) {
unregisterCallback()
callback?.onServiceDisconnected()
service = null
binder = null
}
override fun binderDied() {
service = null
callbackRegistered = false
callback?.also { runOnMainDispatcher { it.onBinderDied() } }
}
private fun unregisterCallback() {
val service = service
if (service != null && callbackRegistered) try {
service.unregisterCallback(serviceCallback)
} catch (_: RemoteException) {
}
callbackRegistered = false
}
fun connect(context: Context, callback: Callback) {
if (connectionActive) return
connectionActive = true
check(this.callback == null)
this.callback = callback
val intent = Intent(context, serviceClass).setAction(Action.SERVICE)
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
}
fun disconnect(context: Context) {
unregisterCallback()
if (connectionActive) try {
context.unbindService(this)
} catch (_: IllegalArgumentException) {
} // ignore
connectionActive = false
if (listenForDeath) try {
binder?.unlinkToDeath(this, 0)
} catch (_: NoSuchElementException) {
}
binder = null
try {
service?.stopListeningForBandwidth(serviceCallback)
service?.stopListeningForStats(serviceCallback)
} catch (_: RemoteException) {
}
service = null
callback = null
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/ServiceNotification.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.PowerManager
import android.text.format.Formatter
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import io.nekohasekai.sagernet.Action
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.aidl.AppStatsList
import io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback
import io.nekohasekai.sagernet.aidl.TrafficStats
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.getColorAttr
import io.nekohasekai.sagernet.utils.Theme
/**
* User can customize visibility of notification since Android 8.
* The default visibility:
*
* Android 8.x: always visible due to system limitations
* VPN: always invisible because of VPN notification/icon
* Other: always visible
*
* See also: https://github.com/aosp-mirror/platform_frameworks_base/commit/070d142993403cc2c42eca808ff3fafcee220ac4
*/
class ServiceNotification(
private val service: BaseService.Interface, profileName: String,
channel: String, visible: Boolean = false,
) : BroadcastReceiver() {
val trafficStatistics = DataStore.profileTrafficStatistics
val showDirectSpeed = DataStore.showDirectSpeed
private val callback: ISagerNetServiceCallback by lazy {
object : ISagerNetServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) {} // ignore
override fun trafficUpdated(profileId: Long, stats: TrafficStats, isCurrent: Boolean) {
if (!trafficStatistics || profileId == 0L || !isCurrent) return
builder.apply {
if (showDirectSpeed) {
val speedDetail = (service as Context).getString(
R.string.speed_detail, service.getString(
R.string.speed, Formatter.formatFileSize(service, stats.txRateProxy)
), service.getString(
R.string.speed, Formatter.formatFileSize(service, stats.rxRateProxy)
), service.getString(
R.string.speed,
Formatter.formatFileSize(service, stats.txRateDirect)
), service.getString(
R.string.speed,
Formatter.formatFileSize(service, stats.rxRateDirect)
)
)
setStyle(NotificationCompat.BigTextStyle().bigText(speedDetail))
setContentText(speedDetail)
} else {
val speedSimple = (service as Context).getString(
R.string.traffic, service.getString(
R.string.speed, Formatter.formatFileSize(service, stats.txRateProxy)
), service.getString(
R.string.speed, Formatter.formatFileSize(service, stats.rxRateProxy)
)
)
setContentText(speedSimple)
}
setSubText(
service.getString(
R.string.traffic,
Formatter.formatFileSize(service, stats.txTotal),
Formatter.formatFileSize(service, stats.rxTotal)
)
)
}
show()
}
override fun statsUpdated(statsList: AppStatsList?) {
}
override fun observatoryResultsUpdated(groupId: Long) {
}
override fun profilePersisted(profileId: Long) {
}
override fun missingPlugin(profileName: String?, pluginName: String?) {
}
override fun routeAlert(type: Int, routeName: String?) {
}
}
}
private var callbackRegistered = false
private val builder = NotificationCompat.Builder(service as Context, channel).setWhen(0)
.setTicker(service.getString(R.string.forward_success)).setContentTitle(profileName)
.setContentIntent(SagerNet.configureIntent(service))
.setSmallIcon(R.drawable.ic_service_ax).setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN)
init {
service as Context
val closeAction = NotificationCompat.Action.Builder(
R.drawable.ic_navigation_close,
service.getText(R.string.stop),
PendingIntent.getBroadcast(
service,
0,
Intent(Action.CLOSE).setPackage(service.packageName),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
)
).apply {
setShowsUserInterface(false)
}.build()
if (Build.VERSION.SDK_INT < 24 || DataStore.showStopButton) builder.addAction(closeAction) else builder.addInvisibleAction(
closeAction
)
Theme.apply(app)
Theme.apply(service)
builder.color = service.getColorAttr(R.attr.colorPrimary)
updateCallback(service.getSystemService()?.isInteractive != false)
service.registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
})
show()
}
override fun onReceive(context: Context, intent: Intent) {
if (service.data.state == BaseService.State.Connected) updateCallback(intent.action == Intent.ACTION_SCREEN_ON)
}
private fun updateCallback(screenOn: Boolean) {
if (!trafficStatistics) return
if (screenOn) {
service.data.binder.registerCallback(callback)
service.data.binder.startListeningForBandwidth(
callback, DataStore.speedInterval.toLong()
)
callbackRegistered = true
} else if (callbackRegistered) { // unregister callback to save battery
service.data.binder.unregisterCallback(callback)
callbackRegistered = false
}
}
private fun show() = (service as Service).startForeground(1, builder.build())
fun destroy() {
(service as Service).stopForeground(true)
service.unregisterReceiver(this)
updateCallback(false)
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkerParameters
import androidx.work.multiprocess.RemoteWorkManager
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.SagerDatabase
import io.nekohasekai.sagernet.group.GroupUpdater
import io.nekohasekai.sagernet.ktx.app
import java.util.concurrent.TimeUnit
object SubscriptionUpdater {
private const val WORK_NAME = "SubscriptionUpdater"
suspend fun reconfigureUpdater() {
RemoteWorkManager.getInstance(app).cancelUniqueWork(WORK_NAME)
val subscriptions = SagerDatabase.groupDao.subscriptions()
.filter { it.subscription!!.autoUpdate }
if (subscriptions.isEmpty()) return
// PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS
var minDelay = subscriptions.minByOrNull { it.subscription!!.autoUpdateDelay }!!.subscription!!.autoUpdateDelay.toLong()
val now = System.currentTimeMillis() / 1000L
val minInitDelay = subscriptions.minOf { now - it.subscription!!.lastUpdated - (minDelay * 60) }
if (minDelay < 15) minDelay = 15
RemoteWorkManager.getInstance(app).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
PeriodicWorkRequest.Builder(UpdateTask::class.java, minDelay, TimeUnit.MINUTES)
.apply {
if (minInitDelay > 0) setInitialDelay(minInitDelay, TimeUnit.SECONDS)
}
.build()
)
}
class UpdateTask(
appContext: Context, params: WorkerParameters
) : CoroutineWorker(appContext, params) {
val nm = NotificationManagerCompat.from(applicationContext)
val notification = NotificationCompat.Builder(applicationContext, "service-subscription")
.setWhen(0)
.setTicker(applicationContext.getString(R.string.forward_success))
.setContentTitle(applicationContext.getString(R.string.subscription_update))
.setSmallIcon(R.drawable.ic_service_active)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
override suspend fun doWork(): Result {
var subscriptions = SagerDatabase.groupDao.subscriptions()
.filter { it.subscription!!.autoUpdate }
if (DataStore.startedProfile == 0L) {
subscriptions = subscriptions.filter { !it.subscription!!.updateWhenConnectedOnly }
}
if (subscriptions.isNotEmpty()) for (profile in subscriptions) {
val subscription = profile.subscription!!
if (((System.currentTimeMillis() / 1000).toInt() - subscription.lastUpdated) < subscription.autoUpdateDelay * 60) {
continue
}
notification.setContentText(
applicationContext.getString(
R.string.subscription_update_message, profile.displayName()
)
)
nm.notify(2, notification.build())
GroupUpdater.executeUpdate(profile, false)
}
nm.cancel(2)
return Result.success()
}
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt
================================================
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv *
* Copyright (C) 2017 by Mygod Studio *
* *
* 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 . *
* *
*******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.app.KeyguardManager
import android.graphics.drawable.Icon
import android.service.quicksettings.Tile
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.aidl.ISagerNetService
import io.nekohasekai.sagernet.database.DataStore
import android.service.quicksettings.TileService as BaseTileService
@RequiresApi(24)
class TileService : BaseTileService(), SagerConnection.Callback {
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_ax) }
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_service_busy) }
private val iconConnected by lazy {
Icon.createWithResource(this,
R.drawable.ic_service_active)
}
private val keyguard by lazy { getSystemService()!! }
private var tapPending = false
private val connection = SagerConnection()
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) =
updateTile(state) { profileName }
override fun onServiceConnected(service: ISagerNetService) {
updateTile(BaseService.State.values()[service.state]) { service.profileName }
if (tapPending) {
tapPending = false
onClick()
}
}
override fun onStartListening() {
super.onStartListening()
connection.connect(this, this)
}
override fun onStopListening() {
connection.disconnect(this)
super.onStopListening()
}
override fun onClick() {
if (isLocked && !DataStore.canToggleLocked) unlockAndRun(this::toggle) else toggle()
}
private fun updateTile(serviceState: BaseService.State, profileName: () -> String?) {
qsTile?.apply {
label = null
when (serviceState) {
BaseService.State.Idle -> error("serviceState")
BaseService.State.Connecting -> {
icon = iconIdle
state = Tile.STATE_ACTIVE
}
BaseService.State.Connected -> {
icon = iconIdle
if (!keyguard.isDeviceLocked) label = profileName()
state = Tile.STATE_ACTIVE
}
BaseService.State.Stopping -> {
icon = iconIdle
state = Tile.STATE_UNAVAILABLE
}
BaseService.State.Stopped -> {
icon = iconIdle
state = Tile.STATE_INACTIVE
}
}
label = label ?: getString(R.string.app_name)
updateTile()
}
}
private fun toggle() {
val service = connection.service
if (service == null) tapPending =
true else BaseService.State.values()[service.state].let { state ->
when {
state.canStop -> SagerNet.stopService()
state == BaseService.State.Stopped -> SagerNet.startService()
}
}
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg
import android.Manifest
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Network
import android.net.ProxyInfo
import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.ErrnoException
import android.system.Os
import androidx.annotation.RequiresApi
import io.nekohasekai.sagernet.*
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.SagerDatabase
import io.nekohasekai.sagernet.database.StatsEntity
import io.nekohasekai.sagernet.fmt.LOCALHOST
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ui.VpnRequestActivity
import io.nekohasekai.sagernet.utils.DefaultNetworkListener
import io.nekohasekai.sagernet.utils.PackageCache
import io.nekohasekai.sagernet.utils.Subnet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import libcore.AppStats
import libcore.Libcore
import libcore.TrafficListener
import libcore.Tun2ray
import java.io.FileDescriptor
import android.net.VpnService as BaseVpnService
class VpnService : BaseVpnService(),
BaseService.Interface,
TrafficListener {
companion object {
var instance: VpnService? = null
const val VPN_MTU = 1500
const val PRIVATE_VLAN4_CLIENT = "172.19.0.1"
const val PRIVATE_VLAN4_ROUTER = "172.19.0.2"
const val FAKEDNS_VLAN4_CLIENT = "198.18.0.0"
const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
const val FAKEDNS_VLAN6_CLIENT = "fc00::"
private fun FileDescriptor.use(block: (FileDescriptor) -> T) = try {
block(this)
} finally {
try {
Os.close(this)
} catch (_: ErrnoException) {
}
}
}
lateinit var conn: ParcelFileDescriptor
private lateinit var tun: Tun2ray
fun getTun(): Tun2ray? {
if (!::tun.isInitialized) return null
return tun
}
private var active = false
private var metered = false
@Volatile
private var underlyingNetwork: Network? = null
@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) set(value) {
field = value
if (active && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
setUnderlyingNetworks(underlyingNetworks)
}
}
private val underlyingNetworks
get() = // clearing underlyingNetworks makes Android 9 consider the network to be metered
if (Build.VERSION.SDK_INT == 28 && metered) null else underlyingNetwork?.let {
arrayOf(it)
}
override suspend fun startProcesses() {
super.startProcesses()
startVpn()
}
@Suppress("EXPERIMENTAL_API_USAGE")
override fun killProcesses() {
getTun()?.close()
if (::conn.isInitialized) conn.close()
super.killProcesses()
persistAppStats()
active = false
GlobalScope.launch(Dispatchers.Default) { DefaultNetworkListener.stop(this) }
}
override fun onBind(intent: Intent) = when (intent.action) {
SERVICE_INTERFACE -> super.onBind(intent)
else -> super.onBind(intent)
}
override val data = BaseService.Data(this)
override val tag = "SagerNetVpnService"
override fun createNotification(profileName: String) =
ServiceNotification(this, profileName, "service-vpn")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (DataStore.serviceMode == Key.MODE_VPN) {
if (prepare(this) != null) {
startActivity(
Intent(
this, VpnRequestActivity::class.java
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} else return super.onStartCommand(intent, flags, startId)
}
stopRunner()
return Service.START_NOT_STICKY
}
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
inner class NullConnectionException : NullPointerException(),
BaseService.ExpectedException {
override fun getLocalizedMessage() = getString(R.string.reboot_required)
}
private fun startVpn() {
instance = this
val profile = data.proxy!!.profile
val builder = Builder().setConfigureIntent(SagerNet.configureIntent(this))
.setSession(profile.displayName())
.setMtu(VPN_MTU)
val useFakeDns = DataStore.enableFakeDns
val ipv6Mode = DataStore.ipv6Mode
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
if (ipv6Mode != IPv6Mode.DISABLE) {
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
}
if (useFakeDns) {
if (ipv6Mode != IPv6Mode.ONLY) {
builder.addAddress(FAKEDNS_VLAN4_CLIENT, 15)
} else {
builder.addAddress(FAKEDNS_VLAN6_CLIENT, 18)
}
}
if (DataStore.bypassLan && !DataStore.bypassLanInCoreOnly) {
resources.getStringArray(R.array.bypass_private_route).forEach {
val subnet = Subnet.fromString(it)!!
builder.addRoute(subnet.address.hostAddress!!, subnet.prefixSize)
}
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
// https://issuetracker.google.com/issues/149636790
if (ipv6Mode != IPv6Mode.DISABLE) {
builder.addRoute("2000::", 3)
}
} else {
builder.addRoute("0.0.0.0", 0)
if (ipv6Mode != IPv6Mode.DISABLE) {
builder.addRoute("::", 0)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
builder.setUnderlyingNetworks(underlyingNetworks)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(metered)
val packageName = packageName
val proxyApps = DataStore.proxyApps
val needBypassRootUid = data.proxy!!.config.outboundTagsAll.values.any { it.ptBean != null }
val needIncludeSelf = data.proxy!!.config.index.any { !it.isBalancer && it.chain.size > 1 }
if (proxyApps || needBypassRootUid) {
var bypass = DataStore.bypass
val individual = mutableSetOf()
val allApps by lazy {
packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS).filter {
when (it.packageName) {
packageName -> false
"android" -> true
else -> it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
}
}.map {
it.packageName
}
}
if (proxyApps) {
individual.addAll(DataStore.individual.split('\n').filter { it.isNotBlank() })
if (bypass && needBypassRootUid) {
val individualNew = allApps.toMutableList()
individualNew.removeAll(individual)
individual.clear()
individual.addAll(individualNew)
bypass = false
}
} else {
individual.addAll(allApps)
bypass = false
}
individual.apply {
if (bypass xor needIncludeSelf) add(packageName) else remove(packageName)
}.forEach {
try {
if (bypass) {
builder.addDisallowedApplication(it)
Logs.d("Add bypass: $it")
} else {
builder.addAllowedApplication(it)
Logs.d("Add allow: $it")
}
} catch (ex: PackageManager.NameNotFoundException) {
Logs.w(ex)
}
}
} else {
builder.addDisallowedApplication(packageName)
}
builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && DataStore.appendHttpProxy && DataStore.requireHttp) {
builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOCALHOST, DataStore.httpPort))
}
metered = DataStore.meteredNetwork
active = true // possible race condition here?
if (Build.VERSION.SDK_INT >= 29) builder.setMetered(metered)
conn = builder.establish() ?: throw NullConnectionException()
tun = Libcore.newTun2ray(
conn.fd,
VPN_MTU,
data.proxy!!.v2rayPoint,
PRIVATE_VLAN4_ROUTER,
DataStore.tunImplementation == TunImplementation.GVISOR,
true,
DataStore.trafficSniffing,
DataStore.destinationOverride,
DataStore.enableFakeDns,
DataStore.enableLog,
data.proxy!!.config.dumpUid,
DataStore.appTrafficStatistics,
DataStore.enablePcap
)
}
val appStats = mutableListOf()
override fun updateStats(stats: AppStats) {
appStats.add(stats)
}
fun persistAppStats() {
if (!DataStore.appTrafficStatistics) return
val tun = getTun() ?: return
appStats.clear()
tun.readAppTraffics(this)
val toUpdate = mutableListOf()
val all = SagerDatabase.statsDao.all().associateBy { it.packageName }
for (stats in appStats) {
val packageName = if (stats.uid >= 10000) {
PackageCache.uidMap[stats.uid]?.iterator()?.next() ?: "android"
} else {
"android"
}
if (!all.containsKey(packageName)) {
SagerDatabase.statsDao.create(
StatsEntity(
packageName = packageName,
tcpConnections = stats.tcpConnTotal,
udpConnections = stats.udpConnTotal,
uplink = stats.uplinkTotal,
downlink = stats.downlinkTotal
)
)
} else {
val entity = all[packageName]!!
entity.tcpConnections += stats.tcpConnTotal
entity.udpConnections += stats.udpConnTotal
entity.uplink += stats.uplinkTotal
entity.downlink += stats.downlinkTotal
toUpdate.add(entity)
}
if (toUpdate.isNotEmpty()) {
SagerDatabase.statsDao.update(toUpdate)
}
}
}
override fun onRevoke() = stopRunner()
override fun onDestroy() {
super.onDestroy()
data.binder.close()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/proto/ApiInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.proto
import android.os.Build
import android.provider.Settings
import io.nekohasekai.sagernet.bg.AbstractInstance
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.app
import libcore.ApiInstance
class ApiInstance : AbstractInstance {
lateinit var point: ApiInstance
override fun launch() {
var deviceName = Settings.Secure.getString(app.contentResolver, "bluetooth_name")
if (deviceName.isNullOrBlank()) {
deviceName = Build.DEVICE
if (!deviceName.startsWith(Build.MANUFACTURER)) {
deviceName = Build.MANUFACTURER + " " + deviceName
}
}
point = ApiInstance(
deviceName,
DataStore.socksPort,
DataStore.localDNSPort,
DataStore.enableLog,
DataStore.bypassLan
)
point.start()
}
override fun close() {
if (::point.isInitialized) point.close()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.proto
//import io.nekohasekai.sagernet.BuildConfig
//import io.nekohasekai.sagernet.bg.test.DebugInstance
//import com.xray.app.observatory.ObservationResult
//import com.xray.app.observatory.OutboundStatus
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.bg.BaseService
import io.nekohasekai.sagernet.bg.VpnService
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.database.SagerDatabase
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.utils.DirectBoot
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import libcore.Libcore
import java.io.IOException
class ProxyInstance(profile: ProxyEntity, val service: BaseService.Interface) : V2RayInstance(
profile
) {
lateinit var observatoryJob: Job
override fun init() {
if (service is VpnService) {
Libcore.setProtector { service.protect(it) }
} else {
Libcore.setProtector { true }
}
super.init()
Logs.d(config.config)
pluginConfigs.forEach { (_, plugin) ->
val (_, content) = plugin
Logs.d(content)
}
}
override fun launch() {
super.launch()
/* if (config.observatoryTags.isNotEmpty()) {
observatoryJob = runOnDefaultDispatcher {
sendInitStatuses()
val interval = 10000L
while (isActive) {
try {
loopObservatoryResults()
} catch (e: Exception) {
if (e.message?.contains("unavailable") == false) {
Logs.w(e)
}
break
}
delay(interval)
}
}
}*/
if (DataStore.allowAccess) {
val api = ApiInstance()
try {
api.launch()
externalInstances[11451] = api
} catch (e: Exception) {
Logs.w("Failed to start api server", e)
}
}
/* if (BuildConfig.DEBUG && DataStore.enableLog) {
externalInstances[9999] = DebugInstance().apply {
launch()
}
}*/
SagerNet.started = true
}
fun sendInitStatuses() {
/*val time = (System.currentTimeMillis() / 1000) - 300
for (observatoryTag in config.observatoryTags) {
val profileId = observatoryTag.substringAfter("global-")
if (NumberUtil.isLong(profileId)) {
val id = profileId.toLong()
val profile = when {
id == profile.id -> profile
statsOutbounds.containsKey(id) -> statsOutbounds[id]!!.proxyEntity
else -> SagerDatabase.proxyDao.getById(id)
} ?: continue
if (profile.status > 0) v2rayPoint.updateStatus(
observatoryTag,
OutboundStatus.newBuilder()
.setOutboundTag(observatoryTag)
.setAlive(profile.status == 1)
.setDelay(profile.ping.toLong())
.setLastErrorReason(profile.error ?: "")
.setLastTryTime(time)
.setLastSeenTime(time)
.build()
.toByteArray()
)
}
}*/
}
/* suspend fun loopObservatoryResults() {
val statusPb = v2rayPoint.observatoryStatus
if (statusPb == null || statusPb.isEmpty()) {
return
}
val statusList = ObservationResult.parseFrom(statusPb)
val notify = mutableSetOf()
for (status in statusList.statusList) {
val profileId = status.outboundTag.substringAfter("global-")
if (NumberUtil.isLong(profileId)) {
val id = profileId.toLong()
var flush = false
val profile = when {
id == profile.id -> profile
statsOutbounds.containsKey(id) -> statsOutbounds[id]!!.proxyEntity
else -> {
flush = true
SagerDatabase.proxyDao.getById(id)
}
}
if (profile != null) {
val newStatus = if (status.alive) 1 else 3
val newDelay = status.delay.toInt()
val newErrorReason = status.lastErrorReason
if (profile.status != newStatus || profile.ping != newDelay || profile.error != newErrorReason) {
profile.status = newStatus
profile.ping = newDelay
profile.error = newErrorReason
notify.add(profile.groupId)
if (flush) SagerDatabase.proxyDao.updateProxy(profile)
Logs.d("Send result for #$profileId ${profile.displayName()}")
}
} else {
Logs.d("Profile with id #$profileId not found")
}
} else {
Logs.d("Persist skipped on outbound ${status.outboundTag}")
}
}
if (notify.isNotEmpty()) {
onMainDispatcher {
service.data.binder.broadcast {
for (groupId in notify) it.observatoryResultsUpdated(groupId)
}
}
}
}*/
override fun close() {
SagerNet.started = false
persistStats()
super.close()
if (::observatoryJob.isInitialized) observatoryJob.cancel()
}
// ------------- stats -------------
private suspend fun queryStats(tag: String, direct: String): Long {
return v2rayPoint.queryStats(tag, direct)
}
private val currentTags by lazy {
mapOf(* config.outboundTagsCurrent.map {
it to config.outboundTagsAll[it]
}.toTypedArray())
}
private val statsTags by lazy {
mapOf(* config.outboundTags.toMutableList().apply {
removeAll(config.outboundTagsCurrent)
}.map {
it to config.outboundTagsAll[it]
}.toTypedArray())
}
private val interTags by lazy {
config.outboundTagsAll.filterKeys { !config.outboundTags.contains(it) }
}
class OutboundStats(
val proxyEntity: ProxyEntity, var uplinkTotal: Long = 0L, var downlinkTotal: Long = 0L
)
private val statsOutbounds = hashMapOf()
private fun registerStats(
proxyEntity: ProxyEntity, uplink: Long? = null, downlink: Long? = null
) {
if (proxyEntity.id == outboundStats.proxyEntity.id) return
val stats = statsOutbounds.getOrPut(proxyEntity.id) {
OutboundStats(proxyEntity)
}
if (uplink != null) {
stats.uplinkTotal += uplink
}
if (downlink != null) {
stats.downlinkTotal += downlink
}
}
var uplinkProxy = 0L
var downlinkProxy = 0L
var uplinkTotalDirect = 0L
var downlinkTotalDirect = 0L
private val outboundStats = OutboundStats(profile)
suspend fun outboundStats(): Pair> {
if (!isInitialized()) return outboundStats to statsOutbounds
uplinkProxy = 0L
downlinkProxy = 0L
val currentUpLink = currentTags.map { (tag, profile) ->
queryStats(tag, "uplink").apply { profile?.also { registerStats(it, uplink = this) } }
}
val currentDownLink = currentTags.map { (tag, profile) ->
queryStats(tag, "downlink").apply {
profile?.also {
registerStats(it, downlink = this)
}
}
}
uplinkProxy += currentUpLink.fold(0L) { acc, l -> acc + l }
downlinkProxy += currentDownLink.fold(0L) { acc, l -> acc + l }
outboundStats.uplinkTotal += uplinkProxy
outboundStats.downlinkTotal += downlinkProxy
if (statsTags.isNotEmpty()) {
uplinkProxy += statsTags.map { (tag, profile) ->
queryStats(tag, "uplink").apply {
profile?.also {
registerStats(it, uplink = this)
}
}
}.fold(0L) { acc, l -> acc + l }
downlinkProxy += statsTags.map { (tag, profile) ->
queryStats(tag, "downlink").apply {
profile?.also {
registerStats(it, downlink = this)
}
}
}.fold(0L) { acc, l -> acc + l }
}
if (interTags.isNotEmpty()) {
interTags.map { (tag, profile) ->
queryStats(tag, "uplink").also { registerStats(profile, uplink = it) }
}
interTags.map { (tag, profile) ->
queryStats(tag, "downlink").also {
registerStats(profile, downlink = it)
}
}
}
return outboundStats to statsOutbounds
}
suspend fun bypassStats(direct: String): Long {
if (!isInitialized()) return 0L
return queryStats(config.bypassTag, direct)
}
suspend fun uplinkDirect() = bypassStats("uplink").also {
uplinkTotalDirect += it
}
suspend fun downlinkDirect() = bypassStats("downlink").also {
downlinkTotalDirect += it
}
fun persistStats() {
runBlocking {
try {
outboundStats()
val toUpdate = mutableListOf()
if (outboundStats.uplinkTotal + outboundStats.downlinkTotal != 0L) {
profile.tx += outboundStats.uplinkTotal
profile.rx += outboundStats.downlinkTotal
toUpdate.add(profile)
}
statsOutbounds.values.forEach {
if (it.uplinkTotal + it.downlinkTotal != 0L) {
it.proxyEntity.tx += it.uplinkTotal
it.proxyEntity.rx += it.downlinkTotal
toUpdate.add(it.proxyEntity)
}
}
if (toUpdate.isNotEmpty()) {
SagerDatabase.proxyDao.updateProxy(toUpdate)
}
} catch (e: IOException) {
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
val profile = DirectBoot.getDeviceProfile()!!
profile.tx += outboundStats.uplinkTotal
profile.rx += outboundStats.downlinkTotal
profile.dirty = true
DirectBoot.update(profile)
DirectBoot.listenForUnlock()
}
}
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/proto/SSHInstance.kt
================================================
/******************************************************************************
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.proto
import io.nekohasekai.sagernet.bg.ClashBasedInstance
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
import libcore.Libcore
class SSHInstance(val server: SSHBean, val socksPort: Int) : ClashBasedInstance() {
override fun createInstance() {
instance = Libcore.newSSHInstance(
socksPort,
server.finalAddress,
server.finalPort,
server.username,
server.authType,
server.password,
server.privateKey,
server.privateKeyPassphrase,
server.publicKey
)
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/proto/ShadowsocksInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.proto
import cn.hutool.json.JSONObject
import com.github.shadowsocks.plugin.PluginConfiguration
import io.nekohasekai.sagernet.bg.ClashBasedInstance
import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
import libcore.Libcore
class ShadowsocksInstance(val server: ShadowsocksBean, val port: Int) : ClashBasedInstance() {
override fun createInstance() {
var pluginName = ""
val pluginOpts = JSONObject()
if (server.plugin.isNotBlank()) {
val plugin = PluginConfiguration(server.plugin)
pluginName = plugin.selected
val options = plugin.getOptions()
when (pluginName) {
"obfs-local" -> {
pluginOpts["mode"] = options["obfs"]
pluginOpts["host"] = options["obfs-host"]
}
"v2ray-plugin" -> {
pluginOpts["mode"] = options["mode"]
pluginOpts["host"] = options["host"]
pluginOpts["path"] = options["path"]
if (options.containsKey("tls")) {
pluginOpts["tls"] = true
}
if (options.containsKey("mux")) {
pluginOpts["mux"] = true
}
}
}
}
instance = Libcore.newShadowsocksInstance(
port,
server.finalAddress,
server.finalPort,
server.password,
server.method,
pluginName,
pluginOpts.toStringPretty()
)
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/proto/ShadowsocksRInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.proto
import io.nekohasekai.sagernet.bg.ClashBasedInstance
import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean
import libcore.Libcore
class ShadowsocksRInstance(val server: ShadowsocksRBean, val port: Int) : ClashBasedInstance() {
override fun createInstance() {
instance = Libcore.newShadowsocksRInstance(
port,
server.finalAddress,
server.finalPort,
server.password,
server.method,
server.obfs,
server.obfsParam,
server.protocol,
server.protocolParam
)
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/proto/SnellInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.proto
import io.nekohasekai.sagernet.bg.ClashBasedInstance
import io.nekohasekai.sagernet.fmt.snell.SnellBean
import libcore.Libcore
class SnellInstance(val server: SnellBean, val port: Int) : ClashBasedInstance() {
override fun createInstance() {
instance = Libcore.newSnellInstance(
port,
server.finalAddress,
server.finalPort,
server.psk,
server.obfsMode,
server.obfsHost,
server.version
)
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/proto/UidDumper.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.proto
import android.annotation.SuppressLint
import android.os.Build
import android.system.OsConstants
import cn.hutool.cache.impl.LFUCacheCompact
import cn.hutool.core.util.HexUtil
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.utils.PackageCache
import libcore.UidDumper
import libcore.UidInfo
import java.io.File
import java.net.InetAddress
import java.net.InetSocketAddress
object UidDumper : UidDumper {
private val TCP_IPV4_PROC = File("/proc/net/tcp")
private val TCP_IPV6_PROC = File("/proc/net/tcp6")
private val UDP_IPV4_PROC = File("/proc/net/udp")
private val UDP_IPV6_PROC = File("/proc/net/udp6")
private data class ProcStats constructor(val remoteAddress: InetSocketAddress, val uid: Int)
private fun mkMap() = LFUCacheCompact(-1, 5 * 60 * 1000L).build(false)
private val uidCacheMapTcp = mkMap()
private val uidCacheMapTcp6 = mkMap()
private val uidCacheMapUdp = mkMap()
private val uidCacheMapUdp6 = mkMap()
private val canReadProc = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
private val useApi = !canReadProc/* || BuildConfig.DEBUG && tun.enableLog)*/
override fun dumpUid(
ipv6: Boolean, udp: Boolean, srcIp: String, srcPort: Int, destIp: String, destPort: Int
): Int {
return dumpUid(
ipv6, udp, InetSocketAddress(srcIp, srcPort), InetSocketAddress(destIp, destPort)
)
}
override fun getUidInfo(uid: Int): UidInfo {
PackageCache.awaitLoadSync()
if (uid <= 1000L) {
val uidInfo = UidInfo()
uidInfo.label = PackageCache.loadLabel("android")
uidInfo.packageName = "android"
return uidInfo
}
val packageNames = PackageCache.uidMap[uid.toInt()]
if (!packageNames.isNullOrEmpty()) for (packageName in packageNames) {
val uidInfo = UidInfo()
uidInfo.label = PackageCache.loadLabel(packageName)
uidInfo.packageName = packageName
return uidInfo
}
error("unknown uid $uid")
}
@SuppressLint("NewApi")
fun dumpUid(
ipv6: Boolean, udp: Boolean, local: InetSocketAddress, remote: InetSocketAddress
): Int {
if (useApi) return SagerNet.connectivity.getConnectionOwnerUid(
if (!udp) OsConstants.IPPROTO_TCP else OsConstants.IPPROTO_UDP, local, remote
)
val proc = if (!udp) {
if (!ipv6) TCP_IPV4_PROC else TCP_IPV6_PROC
} else {
if (!ipv6) UDP_IPV4_PROC else UDP_IPV6_PROC
}
val cacheMap = if (!udp) {
if (!ipv6) uidCacheMapTcp else uidCacheMapTcp6
} else {
if (!ipv6) uidCacheMapUdp else uidCacheMapUdp6
}
if (cacheMap.containsKey(local.port)) {
val cache = cacheMap[local.port]
if (cache.remoteAddress == remote) return cache.uid
}
var lines = proc.readLines().map { line ->
line.split(" ").filterNot { it.isBlank() }
}
lines = lines.subList(1, lines.size)
for (process in lines) {
val localPort = process[1].substringAfter(":").toInt(16)
val remoteAddress = InetAddress.getByAddress(
HexUtil.decodeHex(
process[2].substringBefore(
":"
)
)
)
val remotePort = process[2].substringAfter(":").toInt(16)
val uid = process[7].toInt()
cacheMap.put(
localPort, ProcStats(InetSocketAddress(remoteAddress, remotePort), uid)
)
}
return cacheMap[local.port]?.uid ?: -1
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/proto/V2RayInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.proto
import android.annotation.SuppressLint
import android.os.Build
import android.os.SystemClock
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.ShadowsocksProvider
import io.nekohasekai.sagernet.TrojanProvider
import io.nekohasekai.sagernet.bg.AbstractInstance
import io.nekohasekai.sagernet.bg.Executable
import io.nekohasekai.sagernet.bg.ExternalInstance
import io.nekohasekai.sagernet.bg.GuardedProcessPool
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.fmt.LOCALHOST
import io.nekohasekai.sagernet.fmt.V2rayBuildResult
import io.nekohasekai.sagernet.fmt.brook.BrookBean
import io.nekohasekai.sagernet.fmt.brook.internalUri
import io.nekohasekai.sagernet.fmt.buildV2RayConfig
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
import io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig
import io.nekohasekai.sagernet.fmt.internal.ConfigBean
import io.nekohasekai.sagernet.fmt.naive.NaiveBean
import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig
import io.nekohasekai.sagernet.fmt.pingtunnel.PingTunnelBean
import io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean
import io.nekohasekai.sagernet.fmt.relaybaton.buildRelayBatonConfig
import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
import io.nekohasekai.sagernet.fmt.shadowsocks.buildShadowsocksConfig
import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean
import io.nekohasekai.sagernet.fmt.snell.SnellBean
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
import io.nekohasekai.sagernet.fmt.trojan.buildTrojanConfig
import io.nekohasekai.sagernet.fmt.trojan.buildTrojanGoConfig
import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
import io.nekohasekai.sagernet.fmt.trojan_go.buildCustomTrojanConfig
import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
import io.nekohasekai.sagernet.fmt.wireguard.buildWireGuardUapiConf
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.plugin.PluginManager
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.plus
import libcore.Libcore
import libcore.V2RayInstance
import okhttp3.internal.closeQuietly
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
abstract class V2RayInstance(
val profile: ProxyEntity
) : AbstractInstance {
lateinit var config: V2rayBuildResult
lateinit var v2rayPoint: V2RayInstance
private lateinit var wsForwarder: WebView
val pluginPath = hashMapOf()
val pluginConfigs = hashMapOf>()
val externalInstances = hashMapOf()
open lateinit var processes: GuardedProcessPool
private var cacheFiles = ArrayList()
var closed by AtomicBoolean()
fun isInitialized(): Boolean {
return ::config.isInitialized
}
protected fun initPlugin(name: String): PluginManager.InitResult {
return pluginPath.getOrPut(name) { PluginManager.init(name)!! }
}
protected open fun buildConfig() {
config = buildV2RayConfig(profile)
}
protected open fun loadConfig() {
v2rayPoint.loadConfig(config.config)
}
open fun init() {
v2rayPoint = V2RayInstance()
buildConfig()
for ((isBalancer, chain) in config.index) {
chain.entries.forEachIndexed { index, (port, profile) ->
val needChain = !isBalancer && index != chain.size - 1
val mux = DataStore.enableMux && (isBalancer || chain.size == 0)
when (val bean = profile.requireBean()) {
is ShadowsocksBean -> when (val provider = profile.pickShadowsocksProvider()) {
ShadowsocksProvider.CLASH -> {
externalInstances[port] = ShadowsocksInstance(bean, port)
}
else -> {
pluginConfigs[port] = provider to bean.buildShadowsocksConfig(
port
)
}
}
is ShadowsocksRBean -> {
externalInstances[port] = ShadowsocksRInstance(bean, port)
}
is TrojanBean -> {
when (DataStore.providerTrojan) {
TrojanProvider.TROJAN -> {
initPlugin("trojan-plugin")
pluginConfigs[port] = profile.type to bean.buildTrojanConfig(
port
)
}
TrojanProvider.TROJAN_GO -> {
initPlugin("trojan-go-plugin")
pluginConfigs[port] = profile.type to bean.buildTrojanGoConfig(
port, mux
)
}
}
}
is TrojanGoBean -> {
initPlugin("trojan-go-plugin")
pluginConfigs[port] = profile.type to bean.buildTrojanGoConfig(
port, mux
)
}
is NaiveBean -> {
initPlugin("naive-plugin")
pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port, mux)
}
is PingTunnelBean -> {
if (needChain) error("PingTunnel is incompatible with chain")
initPlugin("pingtunnel-plugin")
}
is RelayBatonBean -> {
initPlugin("relaybaton-plugin")
pluginConfigs[port] = profile.type to bean.buildRelayBatonConfig(port)
}
is BrookBean -> {
initPlugin("brook-plugin")
}
is HysteriaBean -> {
initPlugin("hysteria-plugin")
pluginConfigs[port] = profile.type to bean.buildHysteriaConfig(port) {
File(
app.noBackupFilesDir,
"hysteria_" + SystemClock.elapsedRealtime() + ".ca"
).apply {
parentFile?.mkdirs()
cacheFiles.add(this)
}
}
}
is WireGuardBean -> {
initPlugin("wireguard-plugin")
pluginConfigs[port] = profile.type to bean.buildWireGuardUapiConf()
}
is ConfigBean -> {
when (bean.type) {
"trojan-go" -> {
initPlugin("trojan-go-plugin")
pluginConfigs[port] = profile.type to buildCustomTrojanConfig(
bean.content, port
)
}
else -> {
externalInstances[port] = ExternalInstance(
profile, port
).apply {
init()
}
}
}
}
is SnellBean -> {
externalInstances[port] = SnellInstance(bean, port)
}
is SSHBean -> {
externalInstances[port] = SSHInstance(bean, port)
}
}
}
}
loadConfig()
}
@SuppressLint("SetJavaScriptEnabled")
override fun launch() {
val context = if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked) SagerNet.application else SagerNet.deviceStorage
for ((isBalancer, chain) in config.index) {
chain.entries.forEachIndexed { index, (port, profile) ->
val bean = profile.requireBean()
val needChain = !isBalancer && index != chain.size - 1
val (profileType, config) = pluginConfigs[port] ?: 0 to ""
when {
externalInstances.containsKey(port) -> {
externalInstances[port]!!.launch()
}
bean is ShadowsocksBean -> {
val configFile = File(
context.noBackupFilesDir,
"shadowsocks_" + SystemClock.elapsedRealtime() + ".json"
)
configFile.parentFile?.mkdirs()
configFile.writeText(config)
cacheFiles.add(configFile)
val commands = mutableListOf(
File(
SagerNet.application.applicationInfo.nativeLibraryDir,
when (profileType) {
ShadowsocksProvider.SHADOWSOCKS_RUST -> Executable.SS_LOCAL
else -> Executable.SS_LIBEV_LOCAL
}
).absolutePath, "-c", configFile.absolutePath
)
if (profileType == ShadowsocksProvider.SHADOWSOCKS_RUST) {
commands.add("--log-without-time")
} else {
commands.addAll(arrayOf("-u", "-t", "600"))
}
if (DataStore.enableLog) commands.add("-v")
processes.start(commands)
}
bean is TrojanBean -> {
val configFile = File(
context.noBackupFilesDir,
"trojan_" + SystemClock.elapsedRealtime() + ".json"
)
configFile.parentFile?.mkdirs()
configFile.writeText(config)
cacheFiles.add(configFile)
val commands = listOf(
when (DataStore.providerTrojan) {
TrojanProvider.TROJAN -> initPlugin("trojan-plugin")
else -> initPlugin("trojan-go-plugin")
}.path, "--config", configFile.absolutePath
)
processes.start(commands)
}
bean is TrojanGoBean || bean is ConfigBean && bean.type == "trojan-go" -> {
val configFile = File(
context.noBackupFilesDir,
"trojan_go_" + SystemClock.elapsedRealtime() + ".json"
)
configFile.parentFile?.mkdirs()
configFile.writeText(config)
cacheFiles.add(configFile)
val commands = mutableListOf(
initPlugin("trojan-go-plugin").path, "-config", configFile.absolutePath
)
processes.start(commands)
}
bean is NaiveBean -> {
val configFile = File(
context.noBackupFilesDir,
"naive_" + SystemClock.elapsedRealtime() + ".json"
)
configFile.parentFile?.mkdirs()
configFile.writeText(config)
cacheFiles.add(configFile)
val commands = mutableListOf(
initPlugin("naive-plugin").path, configFile.absolutePath
)
processes.start(commands)
}
bean is PingTunnelBean -> {
if (needChain) error("PingTunnel is incompatible with chain")
val commands = mutableListOf(
"su",
"-c",
initPlugin("pingtunnel-plugin").path,
"-type",
"client",
"-sock5",
"1",
"-l",
"$LOCALHOST:$port",
"-s",
bean.serverAddress
)
if (bean.key.isNotBlank() && bean.key != "1") {
commands.add("-key")
commands.add(bean.key)
}
processes.start(commands)
}
bean is RelayBatonBean -> {
val configFile = File(
context.noBackupFilesDir,
"rb_" + SystemClock.elapsedRealtime() + ".toml"
)
configFile.parentFile?.mkdirs()
configFile.writeText(config)
cacheFiles.add(configFile)
val commands = mutableListOf(
initPlugin("relaybaton-plugin").path,
"client",
"--config",
configFile.absolutePath
)
processes.start(commands)
}
bean is BrookBean -> {
val commands = mutableListOf(initPlugin("brook-plugin").path)
when (bean.protocol) {
"ws" -> {
commands.add("wsclient")
commands.add("--wsserver")
}
"wss" -> {
commands.add("wssclient")
commands.add("--wssserver")
}
else -> {
commands.add("client")
commands.add("--server")
}
}
commands.add(bean.internalUri())
if (bean.password.isNotBlank()) {
commands.add("--password")
commands.add(bean.password)
}
commands.add("--socks5")
commands.add("$LOCALHOST:$port")
processes.start(commands)
}
bean is HysteriaBean -> {
val configFile = File(
context.noBackupFilesDir,
"hysteria_" + SystemClock.elapsedRealtime() + ".json"
)
configFile.parentFile?.mkdirs()
configFile.writeText(config)
cacheFiles.add(configFile)
val commands = mutableListOf(
initPlugin("hysteria-plugin").path,
"--no-check",
"--config",
configFile.absolutePath,
"--log-level",
if (DataStore.enableLog) "trace" else "warn",
"client"
)
processes.start(commands)
}
bean is WireGuardBean -> {
val configFile = File(
context.noBackupFilesDir,
"wg_" + SystemClock.elapsedRealtime() + ".conf"
)
configFile.parentFile?.mkdirs()
configFile.writeText(config)
cacheFiles.add(configFile)
val commands = mutableListOf(
initPlugin("wireguard-plugin").path,
"-a",
bean.localAddress.split("\n").joinToString(","),
"-b",
"127.0.0.1:$port",
"-c",
configFile.absolutePath,
"-d",
"127.0.0.1:${DataStore.localDNSPort}"
)
processes.start(commands)
}
}
}
}
lateinit var wsUrl: String
if (config.requireWs) {
val wsPort = mkPort()
wsUrl = "http://$LOCALHOST:$wsPort/"
Libcore.setenv("XRAY_BROWSER_DIALER", "$LOCALHOST:$wsPort")
} else {
Libcore.unsetenv("XRAY_BROWSER_DIALER")
}
v2rayPoint.start()
if (config.requireWs) {
runOnMainDispatcher {
wsForwarder = WebView(context)
wsForwarder.settings.javaScriptEnabled = true
wsForwarder.webViewClient = object : WebViewClient() {
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?,
) {
Logs.d("WebView load r: $error")
runOnMainDispatcher {
wsForwarder.loadUrl("about:blank")
delay(1000L)
wsForwarder.loadUrl(wsUrl)
}
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
Logs.d("WebView loaded: ${view.title}")
}
}
wsForwarder.loadUrl(wsUrl)
}
}
}
@Suppress("EXPERIMENTAL_API_USAGE")
override fun close() {
for (instance in externalInstances.values) {
instance.closeQuietly()
}
cacheFiles.removeAll { it.delete(); true }
if (::wsForwarder.isInitialized) {
runBlocking {
onMainDispatcher {
wsForwarder.loadUrl("about:blank")
wsForwarder.destroy()
}
}
}
if (::processes.isInitialized) processes.close(GlobalScope + Dispatchers.IO)
if (::v2rayPoint.isInitialized) {
v2rayPoint.close()
}
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/test/DebugInstance.kt
================================================
/******************************************************************************
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.test
//import libcore.DebugInstance
import io.nekohasekai.sagernet.bg.AbstractInstance
class DebugInstance : AbstractInstance {
// lateinit var instance: DebugInstance
override fun launch() {
// instance = Libcore.newDebugInstance()
}
override fun close() {
// if (::instance.isInitialized) instance.close()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/test/LocalDnsInstance.kt
================================================
/******************************************************************************
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.test
import cn.hutool.core.util.NumberUtil
import io.nekohasekai.sagernet.bg.AbstractInstance
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.fmt.LOCALHOST
import io.nekohasekai.sagernet.fmt.TAG_DNS_IN
import io.nekohasekai.sagernet.fmt.TAG_DNS_OUT
import io.nekohasekai.sagernet.fmt.gson.gson
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.*
import io.nekohasekai.sagernet.ktx.isIpAddress
import libcore.Libcore
import libcore.V2RayInstance
import java.io.Closeable
class LocalDnsInstance : AbstractInstance,
Closeable {
lateinit var instance: V2RayInstance
override fun launch() {
val bind = LOCALHOST
val directDNS = DataStore.directDns.split("\n")
.mapNotNull { dns -> dns.trim().takeIf { it.isNotBlank() && !it.startsWith("#") } }
val config = V2RayConfig().apply {
dns = DnsObject().apply {
servers = directDNS.map {
DnsObject.StringOrServerObject().apply {
valueY = DnsObject.ServerObject().apply {
address = it
}
}
}
}
inbounds = listOf(InboundObject().apply {
tag = TAG_DNS_IN
listen = bind
port = DataStore.localDNSPort
protocol = "dokodemo-door"
settings = LazyInboundConfigurationObject(this,
DokodemoDoorInboundConfigurationObject().apply {
address = "1.0.0.1"
network = "tcp,udp"
port = 53
})
})
outbounds = mutableListOf()
outbounds.add(OutboundObject().apply {
protocol = "freedom"
settings = LazyOutboundConfigurationObject(this,
FreedomOutboundConfigurationObject().apply {
domainStrategy = "UseIP"
})
})
outbounds.add(OutboundObject().apply {
protocol = "dns"
tag = TAG_DNS_OUT
settings = LazyOutboundConfigurationObject(this,
DNSOutboundConfigurationObject().apply {
var dns = directDNS.first()
if (dns.contains(":")) {
val lPort = dns.substringAfterLast(":")
dns = dns.substringBeforeLast(":")
if (NumberUtil.isInteger(lPort)) {
port = lPort.toInt()
}
}
if (dns.isIpAddress()) {
address = dns
} else if (dns.contains("://")) {
network = "tcp"
address = dns.substringAfter("://")
}
})
})
routing = RoutingObject().apply {
domainStrategy = "AsIs"
rules = listOf(RoutingObject.RuleObject().apply {
type = "field"
inboundTag = listOf(TAG_DNS_IN)
outboundTag = TAG_DNS_OUT
})
}
}
val i = Libcore.newV2rayInstance()
i.loadConfig(gson.toJson(config))
i.start()
instance = i
}
override fun close() {
if (::instance.isInitialized) instance.close()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/test/UrlTest.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.test
import io.nekohasekai.sagernet.bg.proto.SSHInstance
import io.nekohasekai.sagernet.bg.proto.ShadowsocksInstance
import io.nekohasekai.sagernet.bg.proto.ShadowsocksRInstance
import io.nekohasekai.sagernet.bg.proto.SnellInstance
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProxyEntity
import libcore.Libcore
class UrlTest {
val link = DataStore.connectionTestURL
val timeout = 5000
suspend fun doTest(profile: ProxyEntity): Int {
if (profile.useClashBased()) {
val instance = when (profile.type) {
ProxyEntity.TYPE_SS -> ShadowsocksInstance(profile.ssBean!!, 0)
ProxyEntity.TYPE_SSR -> ShadowsocksRInstance(profile.ssrBean!!, 0)
ProxyEntity.TYPE_SNELL -> SnellInstance(profile.snellBean!!, 0)
ProxyEntity.TYPE_SSH -> SSHInstance(profile.sshBean!!, 0)
else -> error("unexpected")
}
instance.createInstance()
return Libcore.urlTestClashBased(instance.instance, link, timeout).toInt()
}
return V2RayTestInstance(profile, link, timeout).doTest()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/bg/test/V2RayTestInstance.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.bg.test
import io.nekohasekai.sagernet.bg.GuardedProcessPool
import io.nekohasekai.sagernet.bg.proto.V2RayInstance
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.fmt.buildV2RayConfig
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
import io.nekohasekai.sagernet.ktx.tryResume
import io.nekohasekai.sagernet.ktx.tryResumeWithException
import libcore.Libcore
import kotlin.coroutines.suspendCoroutine
class V2RayTestInstance(profile: ProxyEntity, val link: String, val timeout: Int) : V2RayInstance(
profile
) {
suspend fun doTest(): Int {
return suspendCoroutine { c ->
processes = GuardedProcessPool {
Logs.w(it)
c.tryResumeWithException(it)
}
runOnDefaultDispatcher {
use {
try {
init()
launch()
c.tryResume(Libcore.urlTestV2ray(v2rayPoint, "", link, timeout))
} catch (e: Exception) {
c.tryResumeWithException(e)
}
}
}
}
}
override fun buildConfig() {
config = buildV2RayConfig(profile, true)
}
override fun loadConfig() {
v2rayPoint.loadConfig(config.config)
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.database
import android.os.Binder
import android.os.Build
import androidx.preference.PreferenceDataStore
import io.nekohasekai.sagernet.*
import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
import io.nekohasekai.sagernet.database.preference.PublicDatabase
import io.nekohasekai.sagernet.database.preference.RoomPreferenceDataStore
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.utils.DirectBoot
object DataStore : OnPreferenceDataStoreChangeListener {
val configurationStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
val profileCacheStore = RoomPreferenceDataStore(SagerDatabase.profileCacheDao)
fun init() {
if (Build.VERSION.SDK_INT >= 24) {
SagerNet.deviceStorage.moveDatabaseFrom(SagerNet.application, Key.DB_PUBLIC)
}
if (Build.VERSION.SDK_INT >= 24 && directBootAware && SagerNet.user.isUserUnlocked) {
DirectBoot.flushTrafficStats()
}
}
var selectedProxy by configurationStore.long(Key.PROFILE_ID)
var currentProfile by configurationStore.long(Key.PROFILE_CURRENT)
var startedProfile by configurationStore.long(Key.PROFILE_STARTED)
var selectedGroup by configurationStore.long(Key.PROFILE_GROUP) {
SagerNet.currentProfile?.groupId ?: 0L
}
fun currentGroupId(): Long {
val currentSelected = selectedGroup
if (currentSelected > 0L) return currentSelected
val groups = SagerDatabase.groupDao.allGroups()
if (groups.isNotEmpty()) {
val groupId = groups[0].id
selectedGroup = groupId
return groupId
}
val groupId = SagerDatabase.groupDao.createGroup(ProxyGroup(ungrouped = true))
selectedGroup = groupId
return groupId
}
fun currentGroup(): ProxyGroup {
var group: ProxyGroup? = null
val currentSelected = selectedGroup
if (currentSelected > 0L) {
group = SagerDatabase.groupDao.getById(currentSelected)
}
if (group != null) return group
val groups = SagerDatabase.groupDao.allGroups()
if (groups.isEmpty()) {
group = ProxyGroup(ungrouped = true).apply {
id = SagerDatabase.groupDao.createGroup(this)
}
} else {
group = groups[0]
}
selectedGroup = group.id
return group
}
fun selectedGroupForImport(): Long {
val current = currentGroup()
if (current.type == GroupType.BASIC) return current.id
val groups = SagerDatabase.groupDao.allGroups()
return groups.find { it.type == GroupType.BASIC }!!.id
}
var appTheme by configurationStore.int(Key.APP_THEME)
var nightTheme by configurationStore.stringToInt(Key.NIGHT_THEME)
var serviceMode by configurationStore.string(Key.SERVICE_MODE) { Key.MODE_VPN }
var domainStrategy by configurationStore.string(Key.DOMAIN_STRATEGY) { "AsIs" }
var trafficSniffing by configurationStore.boolean(Key.TRAFFIC_SNIFFING) { true }
var destinationOverride by configurationStore.boolean(Key.DESTINATION_OVERRIDE)
var resolveDestination by configurationStore.boolean(Key.RESOLVE_DESTINATION)
var tcpKeepAliveInterval by configurationStore.stringToInt(Key.TCP_KEEP_ALIVE_INTERVAL) { 15 }
var bypassLan by configurationStore.boolean(Key.BYPASS_LAN)
var bypassLanInCoreOnly by configurationStore.boolean(Key.BYPASS_LAN_IN_CORE_ONLY)
var allowAccess by configurationStore.boolean(Key.ALLOW_ACCESS)
var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL)
// https://github.com/SagerNet/SagerNet/issues/180
var remoteDns by configurationStore.string(Key.REMOTE_DNS) { "https://1.0.0.1/dns-query" }
var directDns by configurationStore.string(Key.DIRECT_DNS) { "https+local://223.5.5.5/dns-query" }
var enableDnsRouting by configurationStore.boolean(Key.ENABLE_DNS_ROUTING)
var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS)
var hosts by configurationStore.string(Key.DNS_HOSTS) { "domain:googleapis.cn googleapis.com" }
var securityAdvisory by configurationStore.boolean(Key.SECURITY_ADVISORY) { true }
var rulesProvider by configurationStore.stringToInt(Key.RULES_PROVIDER)
var enableLog by configurationStore.boolean(Key.ENABLE_LOG) { BuildConfig.DEBUG }
var enablePcap by configurationStore.boolean(Key.ENABLE_PCAP)
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
var socksPort: Int
get() = getLocalPort(Key.SOCKS_PORT, 2081)
set(value) = saveLocalPort(Key.SOCKS_PORT, value)
var localDNSPort: Int
get() = getLocalPort(Key.LOCAL_DNS_PORT, 6451)
set(value) {
saveLocalPort(Key.LOCAL_DNS_PORT, value)
}
var httpPort: Int
get() = getLocalPort(Key.HTTP_PORT, 9081)
set(value) = saveLocalPort(Key.HTTP_PORT, value)
var transproxyPort: Int
get() = getLocalPort(Key.TRANSPROXY_PORT, 9201)
set(value) = saveLocalPort(Key.TRANSPROXY_PORT, value)
fun initGlobal() {
if (configurationStore.getString(Key.SOCKS_PORT) == null) {
socksPort = socksPort
}
if (configurationStore.getString(Key.LOCAL_DNS_PORT) == null) {
localDNSPort = localDNSPort
}
if (configurationStore.getString(Key.HTTP_PORT) == null) {
httpPort = httpPort
}
if (configurationStore.getString(Key.TRANSPROXY_PORT) == null) {
transproxyPort = transproxyPort
}
}
private fun getLocalPort(key: String, default: Int): Int {
return parsePort(configurationStore.getString(key), default + userIndex)
}
private fun saveLocalPort(key: String, value: Int) {
configurationStore.putString(key, "$value")
}
var ipv6Mode by configurationStore.stringToInt(Key.IPV6_MODE) { IPv6Mode.ENABLE }
var meteredNetwork by configurationStore.boolean(Key.METERED_NETWORK)
var proxyApps by configurationStore.boolean(Key.PROXY_APPS)
var bypass by configurationStore.boolean(Key.BYPASS_MODE) { true }
var individual by configurationStore.string(Key.INDIVIDUAL)
var enableMux by configurationStore.boolean(Key.ENABLE_MUX)
var enableMuxForAll by configurationStore.boolean(Key.ENABLE_MUX_FOR_ALL)
var muxConcurrency by configurationStore.stringToInt(Key.MUX_CONCURRENCY) { 8 }
var showStopButton by configurationStore.boolean(Key.SHOW_STOP_BUTTON)
var showDirectSpeed by configurationStore.boolean(Key.SHOW_DIRECT_SPEED)
val persistAcrossReboot by configurationStore.boolean(Key.PERSIST_ACROSS_REBOOT) { true }
val canToggleLocked: Boolean get() = configurationStore.getBoolean(Key.DIRECT_BOOT_AWARE) == true
val directBootAware: Boolean get() = SagerNet.directBootSupported && canToggleLocked
var requireHttp by configurationStore.boolean(Key.REQUIRE_HTTP) { true }
var appendHttpProxy by configurationStore.boolean(Key.APPEND_HTTP_PROXY) { true }
var requireTransproxy by configurationStore.boolean(Key.REQUIRE_TRANSPROXY)
var transproxyMode by configurationStore.stringToInt(Key.TRANSPROXY_MODE)
var connectionTestURL by configurationStore.string(Key.CONNECTION_TEST_URL) { CONNECTION_TEST_URL }
var alwaysShowAddress by configurationStore.boolean(Key.ALWAYS_SHOW_ADDRESS)
var utlsFingerprint by configurationStore.string(Key.UTLS_FINGERPRINT)
var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.GVISOR }
var appTrafficStatistics by configurationStore.boolean(Key.APP_TRAFFIC_STATISTICS)
var profileTrafficStatistics by configurationStore.boolean(Key.PROFILE_TRAFFIC_STATISTICS) { true }
// protocol
var providerTrojan by configurationStore.stringToInt(Key.PROVIDER_TROJAN)
var providerShadowsocksAEAD by configurationStore.stringToInt(Key.PROVIDER_SS_AEAD)
var providerShadowsocksStream by configurationStore.stringToInt(Key.PROVIDER_SS_STREAM)
// cache
var dirty by profileCacheStore.boolean(Key.PROFILE_DIRTY)
var editingId by profileCacheStore.long(Key.PROFILE_ID)
var editingGroup by profileCacheStore.long(Key.PROFILE_GROUP)
var profileName by profileCacheStore.string(Key.PROFILE_NAME)
var serverAddress by profileCacheStore.string(Key.SERVER_ADDRESS)
var serverPort by profileCacheStore.stringToInt(Key.SERVER_PORT)
var serverUsername by profileCacheStore.string(Key.SERVER_USERNAME)
var serverPassword by profileCacheStore.string(Key.SERVER_PASSWORD)
var serverPassword1 by profileCacheStore.string(Key.SERVER_PASSWORD1)
var serverMethod by profileCacheStore.string(Key.SERVER_METHOD)
var serverPlugin by profileCacheStore.string(Key.SERVER_PLUGIN)
var serverProtocol by profileCacheStore.string(Key.SERVER_PROTOCOL)
var serverProtocolParam by profileCacheStore.string(Key.SERVER_PROTOCOL_PARAM)
var serverObfs by profileCacheStore.string(Key.SERVER_OBFS)
var serverObfsParam by profileCacheStore.string(Key.SERVER_OBFS_PARAM)
var serverUserId by profileCacheStore.string(Key.SERVER_USER_ID)
var serverAlterId by profileCacheStore.stringToInt(Key.SERVER_ALTER_ID)
var serverSecurity by profileCacheStore.string(Key.SERVER_SECURITY)
var serverNetwork by profileCacheStore.string(Key.SERVER_NETWORK)
var serverHeader by profileCacheStore.string(Key.SERVER_HEADER)
var serverHost by profileCacheStore.string(Key.SERVER_HOST)
var serverPath by profileCacheStore.string(Key.SERVER_PATH)
var serverSNI by profileCacheStore.string(Key.SERVER_SNI)
var serverTLS by profileCacheStore.boolean(Key.SERVER_TLS)
var serverEncryption by profileCacheStore.string(Key.SERVER_ENCRYPTION)
var serverALPN by profileCacheStore.string(Key.SERVER_ALPN)
var serverCertificates by profileCacheStore.string(Key.SERVER_CERTIFICATES)
var serverFlow by profileCacheStore.string(Key.SERVER_FLOW)
var serverQuicSecurity by profileCacheStore.string(Key.SERVER_QUIC_SECURITY)
var serverWsBrowserForwarding by profileCacheStore.boolean(Key.SERVER_WS_BROWSER_FORWARDING)
var serverHeaders by profileCacheStore.string(Key.SERVER_HEADERS)
var serverAllowInsecure by profileCacheStore.boolean(Key.SERVER_ALLOW_INSECURE)
var serverMultiMode by profileCacheStore.boolean(Key.SERVER_MULTI_MODE)
var serverVMessExperimentalAuthenticatedLength by profileCacheStore.boolean(Key.SERVER_VMESS_EXPERIMENTAL_AUTHENTICATED_LENGTH)
var serverVMessExperimentalNoTerminationSignal by profileCacheStore.boolean(Key.SERVER_VMESS_EXPERIMENTAL_NO_TERMINATION_SIGNAL)
var serverAuthType by profileCacheStore.stringToInt(Key.SERVER_AUTH_TYPE)
var serverUploadSpeed by profileCacheStore.stringToInt(Key.SERVER_UPLOAD_SPEED)
var serverDownloadSpeed by profileCacheStore.stringToInt(Key.SERVER_DOWNLOAD_SPEED)
var serverStreamReceiveWindow by profileCacheStore.stringToIntIfExists(Key.SERVER_STREAM_RECEIVE_WINDOW)
var serverConnectionReceiveWindow by profileCacheStore.stringToIntIfExists(Key.SERVER_CONNECTION_RECEIVE_WINDOW)
var serverDisableMtuDiscovery by profileCacheStore.boolean(Key.SERVER_DISABLE_MTU_DISCOVERY)
var serverProtocolVersion by profileCacheStore.stringToInt(Key.SERVER_PROTOCOL)
var serverPrivateKey by profileCacheStore.string(Key.SERVER_PRIVATE_KEY)
var serverLocalAddress by profileCacheStore.string(Key.SERVER_LOCAL_ADDRESS)
var balancerType by profileCacheStore.stringToInt(Key.BALANCER_TYPE)
var balancerGroup by profileCacheStore.stringToLong(Key.BALANCER_GROUP)
var balancerStrategy by profileCacheStore.string(Key.BALANCER_STRATEGY)
var routeName by profileCacheStore.string(Key.ROUTE_NAME)
var routeDomain by profileCacheStore.string(Key.ROUTE_DOMAIN)
var routeIP by profileCacheStore.string(Key.ROUTE_IP)
var routePort by profileCacheStore.string(Key.ROUTE_PORT)
var routeSourcePort by profileCacheStore.string(Key.ROUTE_SOURCE_PORT)
var routeNetwork by profileCacheStore.string(Key.ROUTE_NETWORK)
var routeSource by profileCacheStore.string(Key.ROUTE_SOURCE)
var routeProtocol by profileCacheStore.string(Key.ROUTE_PROTOCOL)
var routeAttrs by profileCacheStore.string(Key.ROUTE_ATTRS)
var routeOutbound by profileCacheStore.stringToInt(Key.ROUTE_OUTBOUND)
var routeOutboundRule by profileCacheStore.long(Key.ROUTE_OUTBOUND_RULE)
var routeReverse by profileCacheStore.boolean(Key.ROUTE_REVERSE)
var routeRedirect by profileCacheStore.string(Key.ROUTE_REDIRECT)
var routePackages by profileCacheStore.string(Key.ROUTE_PACKAGES)
var routeForegroundStatus by profileCacheStore.string(Key.ROUTE_FOREGROUND_STATUS)
var serverConfig by profileCacheStore.string(Key.SERVER_CONFIG)
var groupName by profileCacheStore.string(Key.GROUP_NAME)
var groupType by profileCacheStore.stringToInt(Key.GROUP_TYPE)
var groupOrder by profileCacheStore.stringToInt(Key.GROUP_ORDER)
var subscriptionType by profileCacheStore.stringToInt(Key.SUBSCRIPTION_TYPE)
var subscriptionLink by profileCacheStore.string(Key.SUBSCRIPTION_LINK)
var subscriptionToken by profileCacheStore.string(Key.SUBSCRIPTION_TOKEN)
var subscriptionForceResolve by profileCacheStore.boolean(Key.SUBSCRIPTION_FORCE_RESOLVE)
var subscriptionDeduplication by profileCacheStore.boolean(Key.SUBSCRIPTION_DEDUPLICATION)
var subscriptionForceVMessAEAD by profileCacheStore.boolean(Key.SUBSCRIPTION_FORCE_VMESS_AEAD) { true }
var subscriptionUpdateWhenConnectedOnly by profileCacheStore.boolean(Key.SUBSCRIPTION_UPDATE_WHEN_CONNECTED_ONLY)
var subscriptionUserAgent by profileCacheStore.string(Key.SUBSCRIPTION_USER_AGENT)
var subscriptionAutoUpdate by profileCacheStore.boolean(Key.SUBSCRIPTION_AUTO_UPDATE)
var subscriptionAutoUpdateDelay by profileCacheStore.stringToInt(Key.SUBSCRIPTION_AUTO_UPDATE_DELAY) { 360 }
var rulesFirstCreate by profileCacheStore.boolean("rulesFirstCreate")
var systemDnsFinal by profileCacheStore.string("systemDnsFinal")
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) {
Key.PROFILE_ID -> if (directBootAware) DirectBoot.update()
}
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.database
import io.nekohasekai.sagernet.GroupType
import io.nekohasekai.sagernet.bg.SubscriptionUpdater
import io.nekohasekai.sagernet.ktx.applyDefaultValues
import io.nekohasekai.sagernet.utils.DirectBoot
object GroupManager {
interface Listener {
suspend fun groupAdd(group: ProxyGroup)
suspend fun groupUpdated(group: ProxyGroup)
suspend fun groupRemoved(groupId: Long)
suspend fun groupUpdated(groupId: Long)
}
interface Interface {
suspend fun confirm(message: String): Boolean
suspend fun alert(message: String)
suspend fun onUpdateSuccess(
group: ProxyGroup,
changed: Int,
added: List,
updated: Map,
deleted: List,
duplicate: List,
byUser: Boolean
)
suspend fun onUpdateFailure(group: ProxyGroup, message: String)
}
private val listeners = ArrayList()
var userInterface: Interface? = null
suspend fun iterator(what: suspend Listener.() -> Unit) {
synchronized(listeners) {
listeners.toList()
}.forEach { listener ->
what(listener)
}
}
fun addListener(listener: Listener) {
synchronized(listeners) {
listeners.add(listener)
}
}
fun removeListener(listener: Listener) {
synchronized(listeners) {
listeners.remove(listener)
}
}
suspend fun clearGroup(groupId: Long) {
DataStore.selectedProxy = 0L
SagerDatabase.proxyDao.deleteAll(groupId)
if (DataStore.directBootAware) DirectBoot.clean()
iterator { groupUpdated(groupId) }
}
fun rearrange(groupId: Long) {
val entities = SagerDatabase.proxyDao.getByGroup(groupId)
for (index in entities.indices) {
entities[index].userOrder = (index + 1).toLong()
}
SagerDatabase.proxyDao.updateProxy(entities)
}
suspend fun postUpdate(group: ProxyGroup) {
iterator { groupUpdated(group) }
}
suspend fun postUpdate(groupId: Long) {
postUpdate(SagerDatabase.groupDao.getById(groupId) ?: return)
}
suspend fun postReload(groupId: Long) {
iterator { groupUpdated(groupId) }
}
suspend fun createGroup(group: ProxyGroup): ProxyGroup {
group.userOrder = SagerDatabase.groupDao.nextOrder() ?: 1
group.id = SagerDatabase.groupDao.createGroup(group.applyDefaultValues())
iterator { groupAdd(group) }
if (group.type == GroupType.SUBSCRIPTION) {
SubscriptionUpdater.reconfigureUpdater()
}
return group
}
suspend fun updateGroup(group: ProxyGroup) {
SagerDatabase.groupDao.updateGroup(group)
iterator { groupUpdated(group) }
if (group.type == GroupType.SUBSCRIPTION) {
SubscriptionUpdater.reconfigureUpdater()
}
}
suspend fun deleteGroup(groupId: Long) {
SagerDatabase.groupDao.deleteById(groupId)
SagerDatabase.proxyDao.deleteByGroup(groupId)
iterator { groupRemoved(groupId) }
SubscriptionUpdater.reconfigureUpdater()
}
suspend fun deleteGroup(group: List) {
SagerDatabase.groupDao.deleteGroup(group)
SagerDatabase.proxyDao.deleteByGroup(group.map { it.id }.toLongArray())
for (proxyGroup in group) iterator { groupRemoved(proxyGroup.id) }
SubscriptionUpdater.reconfigureUpdater()
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.database
import android.database.sqlite.SQLiteCantOpenDatabaseException
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.aidl.TrafficStats
import io.nekohasekai.sagernet.fmt.AbstractBean
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.applyDefaultValues
import io.nekohasekai.sagernet.utils.DirectBoot
import java.io.IOException
import java.sql.SQLException
import java.util.*
object ProfileManager {
interface Listener {
suspend fun onAdd(profile: ProxyEntity)
suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats)
suspend fun onUpdated(profile: ProxyEntity)
suspend fun onRemoved(groupId: Long, profileId: Long)
}
interface RuleListener {
suspend fun onAdd(rule: RuleEntity)
suspend fun onUpdated(rule: RuleEntity)
suspend fun onRemoved(ruleId: Long)
suspend fun onCleared()
}
private val listeners = ArrayList()
private val ruleListeners = ArrayList()
suspend fun iterator(what: suspend Listener.() -> Unit) {
synchronized(listeners) {
listeners.toList()
}.forEach { listener ->
what(listener)
}
}
suspend fun ruleIterator(what: suspend RuleListener.() -> Unit) {
val ruleListeners = synchronized(ruleListeners) {
ruleListeners.toList()
}
for (listener in ruleListeners) {
what(listener)
}
}
fun addListener(listener: Listener) {
synchronized(listeners) {
listeners.add(listener)
}
}
fun removeListener(listener: Listener) {
synchronized(listeners) {
listeners.remove(listener)
}
}
fun addListener(listener: RuleListener) {
synchronized(ruleListeners) {
ruleListeners.add(listener)
}
}
fun removeListener(listener: RuleListener) {
synchronized(ruleListeners) {
ruleListeners.remove(listener)
}
}
suspend fun createProfile(groupId: Long, bean: AbstractBean): ProxyEntity {
bean.applyDefaultValues()
val profile = ProxyEntity(groupId = groupId).apply {
id = 0
putBean(bean)
userOrder = SagerDatabase.proxyDao.nextOrder(groupId) ?: 1
}
profile.id = SagerDatabase.proxyDao.addProxy(profile)
iterator { onAdd(profile) }
return profile
}
suspend fun updateProfile(profile: ProxyEntity) {
SagerDatabase.proxyDao.updateProxy(profile)
iterator { onUpdated(profile) }
}
suspend fun updateProfile(profiles: List) {
SagerDatabase.proxyDao.updateProxy(profiles)
profiles.forEach {
iterator { onUpdated(it) }
}
}
suspend fun deleteProfile(groupId: Long, profileId: Long) {
if (SagerDatabase.proxyDao.deleteById(profileId) == 0) return
if (DataStore.selectedProxy == profileId) {
if (DataStore.directBootAware) DirectBoot.clean()
DataStore.selectedProxy = 0L
}
iterator { onRemoved(groupId, profileId) }
if (SagerDatabase.proxyDao.countByGroup(groupId) > 1) {
GroupManager.rearrange(groupId)
}
}
fun getProfile(profileId: Long): ProxyEntity? {
if (profileId == 0L) return null
return try {
SagerDatabase.proxyDao.getById(profileId)
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
} catch (ex: SQLException) {
Logs.w(ex)
null
}
}
fun getProfiles(profileIds: List): List {
if (profileIds.isEmpty()) return listOf()
return try {
SagerDatabase.proxyDao.getEntities(profileIds)
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
} catch (ex: SQLException) {
Logs.w(ex)
listOf()
}
}
suspend fun postUpdate(profileId: Long) {
postUpdate(getProfile(profileId) ?: return)
}
suspend fun postUpdate(profile: ProxyEntity) {
iterator { onUpdated(profile) }
}
suspend fun postTrafficUpdated(profileId: Long, stats: TrafficStats) {
iterator { onUpdated(profileId, stats) }
}
suspend fun createRule(rule: RuleEntity, post: Boolean = true): RuleEntity {
rule.userOrder = SagerDatabase.rulesDao.nextOrder() ?: 1
rule.id = SagerDatabase.rulesDao.createRule(rule)
if (post) {
ruleIterator { onAdd(rule) }
}
return rule
}
suspend fun updateRule(rule: RuleEntity) {
SagerDatabase.rulesDao.updateRule(rule)
ruleIterator { onUpdated(rule) }
}
suspend fun deleteRule(ruleId: Long) {
SagerDatabase.rulesDao.deleteById(ruleId)
ruleIterator { onRemoved(ruleId) }
}
suspend fun deleteRules(rules: List) {
SagerDatabase.rulesDao.deleteRules(rules)
ruleIterator {
rules.forEach {
onRemoved(it.id)
}
}
}
suspend fun getRules(): List {
var rules = SagerDatabase.rulesDao.allRules()
if (rules.isEmpty() && !DataStore.rulesFirstCreate) {
DataStore.rulesFirstCreate = true
createRule(
RuleEntity(
name = app.getString(R.string.route_opt_block_ads),
domains = "geosite:category-ads-all",
outbound = -2
)
)
var country = Locale.getDefault().country.lowercase()
var displayCountry = Locale.getDefault().displayCountry
if (country in arrayOf(
"ir"
)
) {
createRule(
RuleEntity(
name = app.getString(R.string.route_bypass_domain, displayCountry),
domains = "domain:$country",
outbound = -1
), false
)
} else {
country = Locale.CHINA.country.lowercase()
displayCountry = Locale.CHINA.displayCountry
createRule(
RuleEntity(
name = app.getString(R.string.route_bypass_domain, displayCountry),
domains = "geosite:$country",
outbound = -1
), false
)
}
createRule(
RuleEntity(
name = app.getString(R.string.route_bypass_ip, displayCountry),
ip = "geoip:$country",
outbound = -1
), false
)
rules = SagerDatabase.rulesDao.allRules()
}
return rules
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.database
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import androidx.room.*
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.ShadowsocksProvider
import io.nekohasekai.sagernet.ShadowsocksStreamProvider
import io.nekohasekai.sagernet.TrojanProvider
import io.nekohasekai.sagernet.aidl.TrafficStats
import io.nekohasekai.sagernet.fmt.AbstractBean
import io.nekohasekai.sagernet.fmt.KryoConverters
import io.nekohasekai.sagernet.fmt.brook.BrookBean
import io.nekohasekai.sagernet.fmt.buildV2RayConfig
import io.nekohasekai.sagernet.fmt.http.HttpBean
import io.nekohasekai.sagernet.fmt.http.toUri
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
import io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig
import io.nekohasekai.sagernet.fmt.internal.BalancerBean
import io.nekohasekai.sagernet.fmt.internal.ChainBean
import io.nekohasekai.sagernet.fmt.internal.ConfigBean
import io.nekohasekai.sagernet.fmt.naive.NaiveBean
import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig
import io.nekohasekai.sagernet.fmt.naive.toUri
import io.nekohasekai.sagernet.fmt.pingtunnel.PingTunnelBean
import io.nekohasekai.sagernet.fmt.pingtunnel.toUri
import io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean
import io.nekohasekai.sagernet.fmt.relaybaton.buildRelayBatonConfig
import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
import io.nekohasekai.sagernet.fmt.shadowsocks.buildShadowsocksConfig
import io.nekohasekai.sagernet.fmt.shadowsocks.methodsXray
import io.nekohasekai.sagernet.fmt.shadowsocks.toUri
import io.nekohasekai.sagernet.fmt.shadowsocks.*
import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean
import io.nekohasekai.sagernet.fmt.shadowsocksr.buildShadowsocksRConfig
import io.nekohasekai.sagernet.fmt.shadowsocksr.toUri
import io.nekohasekai.sagernet.fmt.snell.SnellBean
import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
import io.nekohasekai.sagernet.fmt.socks.toUri
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
import io.nekohasekai.sagernet.fmt.toUniversalLink
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
import io.nekohasekai.sagernet.fmt.trojan.toUri
import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
import io.nekohasekai.sagernet.fmt.trojan_go.toUri
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean
import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
import io.nekohasekai.sagernet.fmt.v2ray.toUri
import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.applyDefaultValues
import io.nekohasekai.sagernet.ktx.ssSecureList
import io.nekohasekai.sagernet.ui.profile.*
@Entity(
tableName = "proxy_entities", indices = [Index("groupId", name = "groupId")]
)
data class ProxyEntity(
@PrimaryKey(autoGenerate = true) var id: Long = 0L,
var groupId: Long = 0L,
var type: Int = 0,
var userOrder: Long = 0L,
var tx: Long = 0L,
var rx: Long = 0L,
var status: Int = 0,
var ping: Int = 0,
var uuid: String = "",
var error: String? = null,
var socksBean: SOCKSBean? = null,
var httpBean: HttpBean? = null,
var ssBean: ShadowsocksBean? = null,
var ssrBean: ShadowsocksRBean? = null,
var vmessBean: VMessBean? = null,
var vlessBean: VLESSBean? = null,
var trojanBean: TrojanBean? = null,
var trojanGoBean: TrojanGoBean? = null,
var naiveBean: NaiveBean? = null,
var ptBean: PingTunnelBean? = null,
var rbBean: RelayBatonBean? = null,
var brookBean: BrookBean? = null,
var hysteriaBean: HysteriaBean? = null,
var snellBean: SnellBean? = null,
var sshBean: SSHBean? = null,
var wgBean: WireGuardBean? = null,
var configBean: ConfigBean? = null,
var chainBean: ChainBean? = null,
var balancerBean: BalancerBean? = null
) : Parcelable {
companion object {
const val TYPE_SOCKS = 0
const val TYPE_HTTP = 1
const val TYPE_SS = 2
const val TYPE_SSR = 3
const val TYPE_VMESS = 4
const val TYPE_VLESS = 5
const val TYPE_TROJAN = 6
const val TYPE_TROJAN_GO = 7
const val TYPE_NAIVE = 9
const val TYPE_PING_TUNNEL = 10
const val TYPE_RELAY_BATON = 11
const val TYPE_BROOK = 12
const val TYPE_HYSTERIA = 15
const val TYPE_SNELL = 16
const val TYPE_SSH = 17
const val TYPE_WG = 18
const val TYPE_CHAIN = 8
const val TYPE_BALANCER = 14
const val TYPE_CONFIG = 13
val chainName by lazy { app.getString(R.string.proxy_chain) }
val configName by lazy { app.getString(R.string.custom_config) }
val balancerName by lazy { app.getString(R.string.balancer) }
private val placeHolderBean = SOCKSBean().applyDefaultValues()
@JvmField
val CREATOR = object : Parcelable.Creator {
override fun createFromParcel(parcel: Parcel): ProxyEntity {
return ProxyEntity(parcel)
}
override fun newArray(size: Int): Array {
return arrayOfNulls(size)
}
}
}
@Ignore
@Transient
var dirty: Boolean = false
@Ignore
@Transient
var stats: TrafficStats? = null
constructor(parcel: Parcel) : this(
parcel.readLong(),
parcel.readLong(),
parcel.readInt(),
parcel.readLong(),
parcel.readLong(),
parcel.readLong()
) {
dirty = parcel.readByte() > 0
val byteArray = ByteArray(parcel.readInt())
parcel.readByteArray(byteArray)
putByteArray(byteArray)
}
fun putByteArray(byteArray: ByteArray) {
when (type) {
TYPE_SOCKS -> socksBean = KryoConverters.socksDeserialize(byteArray)
TYPE_HTTP -> httpBean = KryoConverters.httpDeserialize(byteArray)
TYPE_SS -> ssBean = KryoConverters.shadowsocksDeserialize(byteArray)
TYPE_SSR -> ssrBean = KryoConverters.shadowsocksRDeserialize(byteArray)
TYPE_VMESS -> vmessBean = KryoConverters.vmessDeserialize(byteArray)
TYPE_VLESS -> vlessBean = KryoConverters.vlessDeserialize(byteArray)
TYPE_TROJAN -> trojanBean = KryoConverters.trojanDeserialize(byteArray)
TYPE_TROJAN_GO -> trojanGoBean = KryoConverters.trojanGoDeserialize(byteArray)
TYPE_NAIVE -> naiveBean = KryoConverters.naiveDeserialize(byteArray)
TYPE_PING_TUNNEL -> ptBean = KryoConverters.pingTunnelDeserialize(byteArray)
TYPE_RELAY_BATON -> rbBean = KryoConverters.relayBatonDeserialize(byteArray)
TYPE_BROOK -> brookBean = KryoConverters.brookDeserialize(byteArray)
TYPE_HYSTERIA -> hysteriaBean = KryoConverters.hysteriaDeserialize(byteArray)
TYPE_SNELL -> snellBean = KryoConverters.snellDeserialize(byteArray)
TYPE_SSH -> sshBean = KryoConverters.sshDeserialize(byteArray)
TYPE_WG -> wgBean = KryoConverters.wireguardDeserialize(byteArray)
TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray)
TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray)
TYPE_BALANCER -> balancerBean = KryoConverters.balancerBeanDeserialize(byteArray)
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(id)
parcel.writeLong(groupId)
parcel.writeInt(type)
parcel.writeLong(userOrder)
parcel.writeLong(tx)
parcel.writeLong(rx)
parcel.writeByte(if (dirty) 1 else 0)
val byteArray = KryoConverters.serialize(requireBean())
parcel.writeInt(byteArray.size)
parcel.writeByteArray(byteArray)
}
fun displayType() = when (type) {
TYPE_SOCKS -> socksBean!!.protocolName()
TYPE_HTTP -> if (httpBean!!.tls) "HTTPS" else "HTTP"
TYPE_SS -> "Shadowsocks"
TYPE_SSR -> "ShadowsocksR"
TYPE_VMESS -> "VMess"
TYPE_VLESS -> "VLESS"
TYPE_TROJAN -> "Trojan"
TYPE_TROJAN_GO -> "Trojan-Go"
TYPE_NAIVE -> "Naïve"
TYPE_PING_TUNNEL -> "PingTunnel"
TYPE_RELAY_BATON -> "relaybaton"
TYPE_BROOK -> "Brook"
TYPE_HYSTERIA -> "Hysteria"
TYPE_SNELL -> "Snell"
TYPE_SSH -> "SSH"
TYPE_WG -> "WireGuard"
TYPE_CHAIN -> chainName
TYPE_CONFIG -> configName
TYPE_BALANCER -> balancerName
else -> "Undefined type $type"
}
fun displayName() = requireBean().displayName()
fun displayAddress() = requireBean().displayAddress()
fun requireBean(): AbstractBean {
return when (type) {
TYPE_SOCKS -> socksBean
TYPE_HTTP -> httpBean
TYPE_SS -> ssBean
TYPE_SSR -> ssrBean
TYPE_VMESS -> vmessBean
TYPE_VLESS -> vlessBean
TYPE_TROJAN -> trojanBean
TYPE_TROJAN_GO -> trojanGoBean
TYPE_NAIVE -> naiveBean
TYPE_PING_TUNNEL -> ptBean
TYPE_RELAY_BATON -> rbBean
TYPE_BROOK -> brookBean
TYPE_HYSTERIA -> hysteriaBean
TYPE_SNELL -> snellBean
TYPE_SSH -> sshBean
TYPE_WG -> wgBean
TYPE_CONFIG -> configBean
TYPE_CHAIN -> chainBean
TYPE_BALANCER -> balancerBean
else -> error("Undefined type $type")
} ?: error("Null ${displayType()} profile")
}
fun haveLink(): Boolean {
return when (type) {
TYPE_CHAIN -> false
TYPE_CONFIG -> false
TYPE_BALANCER -> false
else -> true
}
}
fun haveStandardLink(): Boolean {
return when (requireBean()) {
is RelayBatonBean -> false
is BrookBean -> false
is ConfigBean -> false
is HysteriaBean -> false
is SnellBean -> false
is SSHBean -> false
is WireGuardBean -> false
else -> true
}
}
fun toLink(): String? = with(requireBean()) {
when (this) {
is SOCKSBean -> toUri()
is HttpBean -> toUri()
is ShadowsocksBean -> toUri()
is ShadowsocksRBean -> toUri()
is VMessBean -> toUri()
is VLESSBean -> toUri()
is TrojanBean -> toUri()
is TrojanGoBean -> toUri()
is NaiveBean -> toUri()
is PingTunnelBean -> toUri()
is RelayBatonBean -> toUniversalLink()
is BrookBean -> toUniversalLink()
is ConfigBean -> toUniversalLink()
is HysteriaBean -> toUniversalLink()
is SnellBean -> toUniversalLink()
is SSHBean -> toUniversalLink()
is WireGuardBean -> toUniversalLink()
else -> null
}
}
fun exportConfig(): Pair {
var name = "profile.json"
return with(requireBean()) {
StringBuilder().apply {
val config = buildV2RayConfig(this@ProxyEntity)
append(config.config)
if (!config.index.all { it.chain.isEmpty() }) {
name = "profiles.txt"
}
for ((isBalancer, chain) in config.index) {
chain.entries.forEachIndexed { index, (port, profile) ->
val needChain = !isBalancer && index != chain.size - 1
val needMux = index == 0 && DataStore.enableMux
when (val bean = profile.requireBean()) {
is ShadowsocksBean -> {
append("\n\n")
append(bean.buildShadowsocksConfig(port))
}
is ShadowsocksRBean -> {
append("\n\n")
append(bean.buildShadowsocksRConfig())
}
is TrojanGoBean -> {
append("\n\n")
append(bean.buildTrojanGoConfig(port, needMux))
}
is NaiveBean -> {
append("\n\n")
append(bean.buildNaiveConfig(port, needMux))
}
is RelayBatonBean -> {
append("\n\n")
append(bean.buildRelayBatonConfig(port))
}
is HysteriaBean -> {
append("\n\n")
append(bean.buildHysteriaConfig(port, null))
}
}
}
}
}.toString()
} to name
}
fun needExternal(): Boolean {
return when (type) {
TYPE_SOCKS -> false
TYPE_HTTP -> false
TYPE_SS -> pickShadowsocksProvider() != ShadowsocksProvider.V2RAY
TYPE_VMESS -> false
TYPE_VLESS -> false
TYPE_TROJAN -> DataStore.providerTrojan != TrojanProvider.V2RAY
TYPE_CHAIN -> false
TYPE_BALANCER -> false
else -> true
}
}
fun useClashBased(): Boolean {
if (!needExternal()) return false
return when (type) {
TYPE_SS -> pickShadowsocksProvider() == ShadowsocksProvider.CLASH
TYPE_SSR -> true
TYPE_SNELL -> true
TYPE_SSH -> true
else -> false
}
}
fun isV2RayNetworkTcp(): Boolean {
val bean = requireBean() as StandardV2RayBean
return when (bean.type) {
"tcp", "ws", "http" -> true
else -> false
}
}
fun needCoreMux(): Boolean {
val enableMuxForAll by lazy { DataStore.enableMuxForAll }
return when (type) {
TYPE_VMESS, TYPE_VLESS -> isV2RayNetworkTcp()
TYPE_TROJAN_GO -> false
else -> enableMuxForAll
}
}
fun pickShadowsocksProvider(): Int {
val bean = ssBean ?: return -1
if (bean.method.contains(ssSecureList)) {
val prefer = DataStore.providerShadowsocksAEAD
when {
prefer == ShadowsocksProvider.V2RAY && bean.method in methodsXray && bean.plugin.isBlank() -> {
return ShadowsocksProvider.V2RAY
}
prefer == ShadowsocksProvider.CLASH && bean.method in methodsClash && ssPluginSupportedByClash(
true
) -> {
return ShadowsocksProvider.CLASH
}
prefer == ShadowsocksProvider.SHADOWSOCKS_RUST && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && bean.method in methodsSsRust && !ssPluginSupportedByClash(
false
) -> {
return ShadowsocksProvider.SHADOWSOCKS_RUST
}
prefer == ShadowsocksProvider.SHADOWSOCKS_LIBEV && bean.method in methodsSsLibev && !ssPluginSupportedByClash(
false
) -> {
return ShadowsocksProvider.SHADOWSOCKS_LIBEV
}
}
return if (ssPreferClash()) {
ShadowsocksProvider.CLASH
} else if (bean.method in methodsXray && bean.plugin.isBlank()) {
ShadowsocksProvider.V2RAY
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ShadowsocksProvider.SHADOWSOCKS_RUST
} else {
ShadowsocksProvider.SHADOWSOCKS_LIBEV
}
} else {
val prefer = DataStore.providerShadowsocksStream
when {
prefer == ShadowsocksStreamProvider.CLASH && bean.method in methodsClash && ssPluginSupportedByClash(
true
) -> {
return ShadowsocksProvider.CLASH
}
prefer == ShadowsocksStreamProvider.SHADOWSOCKS_RUST && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && bean.method in methodsSsRust && !ssPluginSupportedByClash(
false
) -> {
return ShadowsocksProvider.SHADOWSOCKS_RUST
}
prefer == ShadowsocksStreamProvider.SHADOWSOCKS_LIBEV && bean.method in methodsSsLibev && !ssPluginSupportedByClash(
false
) -> {
return ShadowsocksProvider.SHADOWSOCKS_LIBEV
}
}
return if (ssPreferClash()) {
ShadowsocksProvider.CLASH
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ShadowsocksProvider.SHADOWSOCKS_RUST
} else {
ShadowsocksProvider.SHADOWSOCKS_LIBEV
}
}
}
fun ssPluginSupportedByClash(prefer: Boolean): Boolean {
val bean = ssBean ?: return false
if (bean.plugin.isNotBlank()) {
val plugin = PluginConfiguration(bean.plugin)
if (plugin.selected !in arrayOf("obfs-local", "v2ray-plugin")) return false
if (plugin.selected == "v2ray-plugin") {
if (plugin.getOptions()["mode"] != "websocket") return false
}
try {
PluginManager.init(plugin)
return prefer
} catch (e: Exception) {
}
return true
}
return prefer
}
fun ssPreferClash(): Boolean {
val bean = ssBean ?: return false
val onlyClash = bean.method !in methodsXray && bean.method !in methodsSsRust && bean.method !in methodsSsLibev
return onlyClash || ssPluginSupportedByClash(false)
}
fun putBean(bean: AbstractBean): ProxyEntity {
socksBean = null
httpBean = null
ssBean = null
ssrBean = null
vmessBean = null
vlessBean = null
trojanBean = null
trojanGoBean = null
naiveBean = null
ptBean = null
rbBean = null
brookBean = null
hysteriaBean = null
snellBean = null
sshBean = null
wgBean = null
configBean = null
chainBean = null
balancerBean = null
when (bean) {
is SOCKSBean -> {
type = TYPE_SOCKS
socksBean = bean
}
is HttpBean -> {
type = TYPE_HTTP
httpBean = bean
}
is ShadowsocksBean -> {
type = TYPE_SS
ssBean = bean
}
is ShadowsocksRBean -> {
type = TYPE_SSR
ssrBean = bean
}
is VMessBean -> {
type = TYPE_VMESS
vmessBean = bean
}
is VLESSBean -> {
type = TYPE_VLESS
vlessBean = bean
}
is TrojanBean -> {
type = TYPE_TROJAN
trojanBean = bean
}
is TrojanGoBean -> {
type = TYPE_TROJAN_GO
trojanGoBean = bean
}
is NaiveBean -> {
type = TYPE_NAIVE
naiveBean = bean
}
is PingTunnelBean -> {
type = TYPE_PING_TUNNEL
ptBean = bean
}
is RelayBatonBean -> {
type = TYPE_RELAY_BATON
rbBean = bean
}
is BrookBean -> {
type = TYPE_BROOK
brookBean = bean
}
is HysteriaBean -> {
type = TYPE_HYSTERIA
hysteriaBean = bean
}
is SnellBean -> {
type = TYPE_SNELL
snellBean = bean
}
is SSHBean -> {
type = TYPE_SSH
sshBean = bean
}
is WireGuardBean -> {
type = TYPE_WG
wgBean = bean
}
is ConfigBean -> {
type = TYPE_CONFIG
configBean = bean
}
is ChainBean -> {
type = TYPE_CHAIN
chainBean = bean
}
is BalancerBean -> {
type = TYPE_BALANCER
balancerBean = bean
}
else -> error("Undefined type $type")
}
return this
}
fun settingIntent(ctx: Context, isSubscription: Boolean): Intent {
return Intent(
ctx, when (type) {
TYPE_SOCKS -> SocksSettingsActivity::class.java
TYPE_HTTP -> HttpSettingsActivity::class.java
TYPE_SS -> ShadowsocksSettingsActivity::class.java
TYPE_SSR -> ShadowsocksRSettingsActivity::class.java
TYPE_VMESS -> VMessSettingsActivity::class.java
TYPE_VLESS -> VLESSSettingsActivity::class.java
TYPE_TROJAN -> TrojanSettingsActivity::class.java
TYPE_TROJAN_GO -> TrojanGoSettingsActivity::class.java
TYPE_NAIVE -> NaiveSettingsActivity::class.java
TYPE_PING_TUNNEL -> PingTunnelSettingsActivity::class.java
TYPE_RELAY_BATON -> RelayBatonSettingsActivity::class.java
TYPE_BROOK -> BrookSettingsActivity::class.java
TYPE_HYSTERIA -> HysteriaSettingsActivity::class.java
TYPE_SNELL -> SnellSettingsActivity::class.java
TYPE_SSH -> SSHSettingsActivity::class.java
TYPE_WG -> WireGuardSettingsActivity::class.java
TYPE_CONFIG -> ConfigSettingsActivity::class.java
TYPE_CHAIN -> ChainSettingsActivity::class.java
TYPE_BALANCER -> BalancerSettingsActivity::class.java
else -> throw IllegalArgumentException()
}
).apply {
putExtra(ProfileSettingsActivity.EXTRA_PROFILE_ID, id)
putExtra(ProfileSettingsActivity.EXTRA_IS_SUBSCRIPTION, isSubscription)
}
}
@androidx.room.Dao
interface Dao {
@Query("SELECT id FROM proxy_entities WHERE groupId = :groupId ORDER BY userOrder")
fun getIdsByGroup(groupId: Long): List
@Query("SELECT * FROM proxy_entities WHERE groupId = :groupId ORDER BY userOrder")
fun getByGroup(groupId: Long): List
@Query("SELECT * FROM proxy_entities WHERE id in (:proxyIds)")
fun getEntities(proxyIds: List): List
@Query("SELECT COUNT(*) FROM proxy_entities WHERE groupId = :groupId")
fun countByGroup(groupId: Long): Long
@Query("SELECT MAX(userOrder) + 1 FROM proxy_entities WHERE groupId = :groupId")
fun nextOrder(groupId: Long): Long?
@Query("SELECT * FROM proxy_entities WHERE id = :proxyId")
fun getById(proxyId: Long): ProxyEntity?
@Query("DELETE FROM proxy_entities WHERE id IN (:proxyId)")
fun deleteById(proxyId: Long): Int
@Query("DELETE FROM proxy_entities WHERE groupId = :groupId")
fun deleteByGroup(groupId: Long)
@Query("DELETE FROM proxy_entities WHERE groupId in (:groupId)")
fun deleteByGroup(groupId: LongArray)
@Delete
fun deleteProxy(proxy: ProxyEntity): Int
@Delete
fun deleteProxy(proxies: List): Int
@Update
fun updateProxy(proxy: ProxyEntity): Int
@Update
fun updateProxy(proxies: List): Int
@Insert
fun addProxy(proxy: ProxyEntity): Long
@Query("DELETE FROM proxy_entities WHERE groupId = :groupId")
fun deleteAll(groupId: Long): Int
}
override fun describeContents(): Int {
return 0
}
}
================================================
FILE: app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt
================================================
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.database
import androidx.room.*
import com.esotericsoftware.kryo.io.ByteBufferInput
import com.esotericsoftware.kryo.io.ByteBufferOutput
import io.nekohasekai.sagernet.GroupOrder
import io.nekohasekai.sagernet.GroupType
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.fmt.Serializable
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.applyDefaultValues
@Entity(tableName = "proxy_groups")
data class ProxyGroup(
@PrimaryKey(autoGenerate = true) var id: Long = 0L,
var userOrder: Long = 0L,
var ungrouped: Boolean = false,
var name: String? = null,
var type: Int = GroupType.BASIC,
var subscription: SubscriptionBean? = null,
var order: Int = GroupOrder.ORIGIN,
) : Serializable() {
@Transient
var export = false
override fun initializeDefaultValues() {
subscription?.applyDefaultValues()
}
override fun serializeToBuffer(output: ByteBufferOutput) {
if (export) {
output.writeInt(0)
output.writeString(name)
output.writeInt(type)
val subscription = subscription!!
subscription.serializeForShare(output)
} else {
output.writeInt(0)
output.writeLong(id)
output.writeLong(userOrder)
output.writeBoolean(ungrouped)
output.writeString(name)
output.writeInt(type)
if (type == GroupType.SUBSCRIPTION) {
subscription?.serializeToBuffer(output)
}
}
}
override fun deserializeFromBuffer(input: ByteBufferInput) {
if (export) {
val version = input.readInt()
name = input.readString()
type = input.readInt()
val subscription = SubscriptionBean()
this.subscription = subscription
subscription.deserializeFromShare(input)
} else {
val version = input.readInt()
id = input.readLong()
userOrder = input.readLong()
ungrouped = input.readBoolean()
name = input.readString()
type = input.readInt()
if (type == GroupType.SUBSCRIPTION) {
val subscription = SubscriptionBean()
this.subscription = subscription
subscription.deserializeFromBuffer(input)
}
}
}
fun displayName(): String {
return name.takeIf { !it.isNullOrBlank() } ?: app.getString(R.string.group_default)
}
@androidx.room.Dao
interface Dao {
@Query("SELECT * FROM proxy_groups ORDER BY userOrder")
fun allGroups(): List
@Query("SELECT * FROM proxy_groups WHERE type = ${GroupType.SUBSCRIPTION}")
suspend fun subscriptions(): List
@Query("SELECT MAX(userOrder) + 1 FROM proxy_groups")
fun nextOrder(): Long?
@Query("SELECT * FROM proxy_groups WHERE id = :groupId")
fun getById(groupId: Long): ProxyGroup?
@Query("DELETE FROM proxy_groups WHERE id = :groupId")
fun deleteById(groupId: Long): Int
@Delete
fun deleteGroup(group: ProxyGroup)
@Delete
fun deleteGroup(groupList: List)
@Insert
fun createGroup(group: ProxyGroup): Long
@Update
fun updateGroup(group: ProxyGroup)
}
companion object CREATOR : Serializable.CREATOR