mirror of
https://github.com/MatsuriDayo/NekoBoxForAndroid.git
synced 2025-12-18 22:20:06 +08:00
Remove advanced plugin
This commit is contained in:
parent
2c3a6164bb
commit
912a0665a5
@ -191,9 +191,6 @@
|
||||
<activity
|
||||
android:name="moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity"
|
||||
android:configChanges="uiMode" />
|
||||
<activity
|
||||
android:name="moe.matsuri.nb4a.proxy.neko.NekoSettingActivity"
|
||||
android:configChanges="uiMode" />
|
||||
<activity
|
||||
android:name="moe.matsuri.nb4a.proxy.config.ConfigSettingActivity"
|
||||
android:configChanges="uiMode" />
|
||||
|
||||
@ -147,7 +147,6 @@ object Key {
|
||||
|
||||
//
|
||||
|
||||
const val NEKO_PLUGIN_MANAGED = "nekoPlugins"
|
||||
const val APP_TLS_VERSION = "appTLSVersion"
|
||||
const val ENABLE_CLASH_API = "enableClashAPI"
|
||||
}
|
||||
|
||||
@ -16,9 +16,6 @@ import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
|
||||
import io.nekohasekai.sagernet.ktx.*
|
||||
import io.nekohasekai.sagernet.ui.VpnRequestActivity
|
||||
import io.nekohasekai.sagernet.utils.Subnet
|
||||
import libcore.*
|
||||
import moe.matsuri.nb4a.net.LocalResolverImpl
|
||||
import moe.matsuri.nb4a.proxy.neko.needBypassRootUid
|
||||
import android.net.VpnService as BaseVpnService
|
||||
|
||||
class VpnService : BaseVpnService(),
|
||||
@ -135,7 +132,7 @@ class VpnService : BaseVpnService(),
|
||||
var bypass = DataStore.bypass
|
||||
val workaroundSYSTEM = false /* DataStore.tunImplementation == TunImplementation.SYSTEM */
|
||||
val needBypassRootUid = workaroundSYSTEM || data.proxy!!.config.trafficMap.values.any {
|
||||
it[0].nekoBean?.needBypassRootUid() == true || it[0].hysteriaBean?.protocol == HysteriaBean.PROTOCOL_FAKETCP
|
||||
it[0].hysteriaBean?.protocol == HysteriaBean.PROTOCOL_FAKETCP
|
||||
}
|
||||
|
||||
if (proxyApps || needBypassRootUid) {
|
||||
|
||||
@ -21,11 +21,6 @@ import io.nekohasekai.sagernet.plugin.PluginManager
|
||||
import kotlinx.coroutines.*
|
||||
import libcore.BoxInstance
|
||||
import libcore.Libcore
|
||||
import moe.matsuri.nb4a.plugin.NekoPluginManager
|
||||
import moe.matsuri.nb4a.proxy.neko.NekoBean
|
||||
import moe.matsuri.nb4a.proxy.neko.NekoJSInterface
|
||||
import moe.matsuri.nb4a.proxy.neko.updateAllConfig
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
abstract class BoxInstance(
|
||||
@ -53,7 +48,6 @@ abstract class BoxInstance(
|
||||
}
|
||||
|
||||
protected open suspend fun loadConfig() {
|
||||
NekoJSInterface.Default.destroyAllJsi()
|
||||
box = Libcore.newSingBoxInstance(config.config)
|
||||
}
|
||||
|
||||
@ -88,17 +82,6 @@ abstract class BoxInstance(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is NekoBean -> {
|
||||
// check if plugin binary can be loaded
|
||||
initPlugin(bean.plgId)
|
||||
|
||||
// build config and check if succeed
|
||||
bean.updateAllConfig(port)
|
||||
if (bean.allConfig == null) {
|
||||
throw NekoPluginManager.PluginInternalException(bean.protocolId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -211,44 +194,6 @@ abstract class BoxInstance(
|
||||
|
||||
processes.start(commands)
|
||||
}
|
||||
|
||||
bean is NekoBean -> {
|
||||
// config built from JS
|
||||
val nekoRunConfigs = bean.allConfig.optJSONArray("nekoRunConfigs")
|
||||
val configs = mutableMapOf<String, String>()
|
||||
|
||||
nekoRunConfigs?.forEach { _, any ->
|
||||
any as JSONObject
|
||||
|
||||
val name = any.getString("name")
|
||||
val configFile = File(cacheDir, name)
|
||||
configFile.parentFile?.mkdirs()
|
||||
val content = any.getString("content")
|
||||
configFile.writeText(content)
|
||||
|
||||
cacheFiles.add(configFile)
|
||||
configs[name] = configFile.absolutePath
|
||||
|
||||
Logs.d(name + "\n\n" + content)
|
||||
}
|
||||
|
||||
val nekoCommands = bean.allConfig.getJSONArray("nekoCommands")
|
||||
val commands = mutableListOf<String>()
|
||||
|
||||
nekoCommands.forEach { _, any ->
|
||||
if (any is String) {
|
||||
if (configs.containsKey(any)) {
|
||||
commands.add(configs[any]!!)
|
||||
} else if (any == "%exe%") {
|
||||
commands.add(initPlugin(bean.plgId).path)
|
||||
} else {
|
||||
commands.add(any)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processes.start(commands)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +81,6 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||
return groups.find { it.type == GroupType.BASIC }!!.id
|
||||
}
|
||||
|
||||
var nekoPlugins by configurationStore.string(Key.NEKO_PLUGIN_MANAGED)
|
||||
var appTLSVersion by configurationStore.string(Key.APP_TLS_VERSION)
|
||||
var enableClashAPI by configurationStore.boolean(Key.ENABLE_CLASH_API)
|
||||
var showBottomBar by configurationStore.boolean(Key.SHOW_BOTTOM_BAR)
|
||||
|
||||
@ -239,7 +239,7 @@ data class ProxyEntity(
|
||||
is SSHBean -> false
|
||||
is WireGuardBean -> false
|
||||
is ShadowTLSBean -> false
|
||||
is NekoBean -> nekoBean!!.haveStandardLink()
|
||||
is NekoBean -> false
|
||||
is ConfigBean -> false
|
||||
else -> true
|
||||
}
|
||||
@ -257,7 +257,7 @@ data class ProxyEntity(
|
||||
is HysteriaBean -> toUri()
|
||||
is TuicBean -> toUri()
|
||||
is AnyTLSBean -> toUri()
|
||||
is NekoBean -> shareLink()
|
||||
is NekoBean -> ""
|
||||
else -> toUniversalLink()
|
||||
}
|
||||
}
|
||||
@ -470,7 +470,6 @@ data class ProxyEntity(
|
||||
TYPE_SHADOWTLS -> ShadowTLSSettingsActivity::class.java
|
||||
TYPE_ANYTLS -> AnyTLSSettingsActivity::class.java
|
||||
TYPE_CHAIN -> ChainSettingsActivity::class.java
|
||||
TYPE_NEKO -> NekoSettingActivity::class.java
|
||||
TYPE_CONFIG -> ConfigSettingActivity::class.java
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
@ -54,11 +54,6 @@ public abstract class StandardV2RayBean extends AbstractBean {
|
||||
|
||||
public String echConfig;
|
||||
|
||||
// sing-box 不再使用
|
||||
public Boolean enablePqSignature;
|
||||
|
||||
public Boolean disabledDRS;
|
||||
|
||||
// --------------------------------------- Mux
|
||||
|
||||
public Boolean enableMux;
|
||||
@ -108,8 +103,6 @@ public abstract class StandardV2RayBean extends AbstractBean {
|
||||
|
||||
if (enableECH == null) enableECH = false;
|
||||
if (JavaUtil.isNullOrBlank(echConfig)) echConfig = "";
|
||||
if (enablePqSignature == null) enablePqSignature = false;
|
||||
if (disabledDRS == null) disabledDRS = false;
|
||||
|
||||
if (enableMux == null) enableMux = false;
|
||||
if (muxPadding == null) muxPadding = false;
|
||||
@ -119,7 +112,7 @@ public abstract class StandardV2RayBean extends AbstractBean {
|
||||
|
||||
@Override
|
||||
public void serialize(ByteBufferOutput output) {
|
||||
output.writeInt(2);
|
||||
output.writeInt(3);
|
||||
super.serialize(output);
|
||||
output.writeString(uuid);
|
||||
output.writeString(encryption);
|
||||
@ -167,11 +160,7 @@ public abstract class StandardV2RayBean extends AbstractBean {
|
||||
}
|
||||
|
||||
output.writeBoolean(enableECH);
|
||||
if (enableECH) {
|
||||
output.writeBoolean(enablePqSignature);
|
||||
output.writeBoolean(disabledDRS);
|
||||
output.writeString(echConfig);
|
||||
}
|
||||
output.writeString(echConfig);
|
||||
|
||||
output.writeInt(packetEncoding);
|
||||
|
||||
@ -229,16 +218,18 @@ public abstract class StandardV2RayBean extends AbstractBean {
|
||||
realityShortId = input.readString();
|
||||
}
|
||||
|
||||
if (version >= 1) { // 从老版本升级上来
|
||||
if (version >= 1) {
|
||||
enableECH = input.readBoolean();
|
||||
if (enableECH) {
|
||||
enablePqSignature = input.readBoolean();
|
||||
disabledDRS = input.readBoolean();
|
||||
if (version >= 3) {
|
||||
echConfig = input.readString();
|
||||
} else {
|
||||
if (enableECH) {
|
||||
input.readBoolean();
|
||||
input.readBoolean();
|
||||
echConfig = input.readString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (version == 0) {
|
||||
} else if (version == 0) {
|
||||
// 从老版本升级上来但是 version == 0, 可能有 enableECH 也可能没有,需要做判断
|
||||
int position = input.getByteBuffer().position(); // 当前位置
|
||||
|
||||
@ -250,8 +241,8 @@ public abstract class StandardV2RayBean extends AbstractBean {
|
||||
if (tmpPacketEncoding != 1 && tmpPacketEncoding != 2) {
|
||||
enableECH = tmpEnableECH;
|
||||
if (enableECH) {
|
||||
enablePqSignature = input.readBoolean();
|
||||
disabledDRS = input.readBoolean();
|
||||
input.readBoolean();
|
||||
input.readBoolean();
|
||||
echConfig = input.readString();
|
||||
}
|
||||
} // 否则后一位就是 packetEncoding
|
||||
@ -275,7 +266,6 @@ public abstract class StandardV2RayBean extends AbstractBean {
|
||||
bean.utlsFingerprint = utlsFingerprint;
|
||||
bean.packetEncoding = packetEncoding;
|
||||
bean.enableECH = enableECH;
|
||||
bean.disabledDRS = disabledDRS;
|
||||
bean.echConfig = echConfig;
|
||||
bean.enableMux = enableMux;
|
||||
bean.muxPadding = muxPadding;
|
||||
|
||||
@ -16,7 +16,12 @@ public class VMessBean extends StandardV2RayBean {
|
||||
super.initializeDefaultValues();
|
||||
|
||||
alterId = alterId != null ? alterId : 0;
|
||||
encryption = JavaUtil.isNotBlank(encryption) ? encryption : "auto";
|
||||
|
||||
if (alterId == -1) {
|
||||
encryption = JavaUtil.isNotBlank(encryption) ? encryption : "";
|
||||
} else {
|
||||
encryption = JavaUtil.isNotBlank(encryption) ? encryption : "auto";
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@ -14,10 +14,7 @@ import io.nekohasekai.sagernet.fmt.trojan.parseTrojan
|
||||
import io.nekohasekai.sagernet.fmt.tuic.parseTuic
|
||||
import io.nekohasekai.sagernet.fmt.trojan_go.parseTrojanGo
|
||||
import io.nekohasekai.sagernet.fmt.v2ray.parseV2Ray
|
||||
import moe.matsuri.nb4a.plugin.NekoPluginManager
|
||||
import moe.matsuri.nb4a.proxy.anytls.parseAnytls
|
||||
import moe.matsuri.nb4a.proxy.neko.NekoJSInterface
|
||||
import moe.matsuri.nb4a.proxy.neko.parseShareLink
|
||||
import moe.matsuri.nb4a.utils.JavaUtil.gson
|
||||
import moe.matsuri.nb4a.utils.Util
|
||||
import org.json.JSONArray
|
||||
@ -112,7 +109,7 @@ suspend fun parseProxies(text: String): List<AbstractBean> {
|
||||
val entities = ArrayList<AbstractBean>()
|
||||
val entitiesByLine = ArrayList<AbstractBean>()
|
||||
|
||||
suspend fun String.parseLink(entities: ArrayList<AbstractBean>) {
|
||||
fun String.parseLink(entities: ArrayList<AbstractBean>) {
|
||||
if (startsWith("clash://install-config?") || startsWith("sn://subscription?")) {
|
||||
throw SubscriptionFoundException(this)
|
||||
}
|
||||
@ -211,22 +208,6 @@ suspend fun parseProxies(text: String): List<AbstractBean> {
|
||||
}.onFailure {
|
||||
Logs.w(it)
|
||||
}
|
||||
} else { // Neko Plugins
|
||||
NekoPluginManager.getProtocols().forEach { obj ->
|
||||
obj.protocolConfig.optJSONArray("links")?.forEach { _, any ->
|
||||
if (any is String && startsWith(any)) {
|
||||
runCatching {
|
||||
entities.add(
|
||||
parseShareLink(
|
||||
obj.plgId, obj.protocolId, this@parseLink
|
||||
)
|
||||
)
|
||||
}.onFailure {
|
||||
Logs.w(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,7 +227,6 @@ suspend fun parseProxies(text: String): List<AbstractBean> {
|
||||
}
|
||||
}
|
||||
}
|
||||
NekoJSInterface.Default.destroyAllJsi()
|
||||
return if (entities.size > entitiesByLine.size) entities else entitiesByLine
|
||||
}
|
||||
|
||||
|
||||
@ -75,9 +75,11 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
|
||||
}
|
||||
|
||||
return MaterialAboutList.Builder()
|
||||
.addCard(MaterialAboutCard.Builder()
|
||||
.addCard(
|
||||
MaterialAboutCard.Builder()
|
||||
.outline(false)
|
||||
.addItem(MaterialAboutActionItem.Builder()
|
||||
.addItem(
|
||||
MaterialAboutActionItem.Builder()
|
||||
.icon(R.drawable.ic_baseline_update_24)
|
||||
.text(R.string.app_version)
|
||||
.subText(versionName)
|
||||
@ -87,13 +89,15 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
|
||||
)
|
||||
}
|
||||
.build())
|
||||
.addItem(MaterialAboutActionItem.Builder()
|
||||
.addItem(
|
||||
MaterialAboutActionItem.Builder()
|
||||
.icon(R.drawable.ic_baseline_layers_24)
|
||||
.text(getString(R.string.version_x, "sing-box"))
|
||||
.subText(Libcore.versionBox())
|
||||
.setOnClickAction { }
|
||||
.build())
|
||||
.addItem(MaterialAboutActionItem.Builder()
|
||||
.addItem(
|
||||
MaterialAboutActionItem.Builder()
|
||||
.icon(R.drawable.ic_baseline_card_giftcard_24)
|
||||
.text(R.string.donate)
|
||||
.subText(R.string.donate_info)
|
||||
@ -107,9 +111,11 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
|
||||
PackageCache.awaitLoadSync()
|
||||
for ((_, pkg) in PackageCache.installedPluginPackages) {
|
||||
try {
|
||||
val pluginId = pkg.providers?.get(0)?.loadString(Plugins.METADATA_KEY_ID)
|
||||
if (pluginId.isNullOrBlank() || pluginId.startsWith(Plugins.AUTHORITIES_PREFIX_NEKO_PLUGIN)) continue
|
||||
addItem(MaterialAboutActionItem.Builder()
|
||||
val pluginId =
|
||||
pkg.providers?.get(0)?.loadString(Plugins.METADATA_KEY_ID)
|
||||
if (pluginId.isNullOrBlank()) continue
|
||||
addItem(
|
||||
MaterialAboutActionItem.Builder()
|
||||
.icon(R.drawable.ic_baseline_nfc_24)
|
||||
.text(
|
||||
getString(
|
||||
@ -137,7 +143,8 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val pm = app.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (!pm.isIgnoringBatteryOptimizations(app.packageName)) {
|
||||
addItem(MaterialAboutActionItem.Builder()
|
||||
addItem(
|
||||
MaterialAboutActionItem.Builder()
|
||||
.icon(R.drawable.ic_baseline_running_with_errors_24)
|
||||
.text(R.string.ignore_battery_optimizations)
|
||||
.subText(R.string.ignore_battery_optimizations_sum)
|
||||
@ -154,10 +161,12 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
|
||||
}
|
||||
}
|
||||
.build())
|
||||
.addCard(MaterialAboutCard.Builder()
|
||||
.addCard(
|
||||
MaterialAboutCard.Builder()
|
||||
.outline(false)
|
||||
.title(R.string.project)
|
||||
.addItem(MaterialAboutActionItem.Builder()
|
||||
.addItem(
|
||||
MaterialAboutActionItem.Builder()
|
||||
.icon(R.drawable.ic_baseline_sanitizer_24)
|
||||
.text(R.string.github)
|
||||
.setOnClickAction {
|
||||
@ -167,7 +176,8 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
|
||||
)
|
||||
}
|
||||
.build())
|
||||
.addItem(MaterialAboutActionItem.Builder()
|
||||
.addItem(
|
||||
MaterialAboutActionItem.Builder()
|
||||
.icon(R.drawable.ic_qu_shadowsocks_foreground)
|
||||
.text(R.string.telegram)
|
||||
.setOnClickAction {
|
||||
|
||||
@ -15,12 +15,9 @@ import android.view.ViewGroup
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.util.contains
|
||||
import androidx.core.util.set
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
@ -29,27 +26,20 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
import io.nekohasekai.sagernet.BuildConfig
|
||||
import io.nekohasekai.sagernet.Key
|
||||
import io.nekohasekai.sagernet.R
|
||||
import io.nekohasekai.sagernet.SagerNet
|
||||
import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.databinding.LayoutAppListBinding
|
||||
import io.nekohasekai.sagernet.databinding.LayoutAppsItemBinding
|
||||
import io.nekohasekai.sagernet.ktx.crossFadeFrom
|
||||
import io.nekohasekai.sagernet.ktx.launchCustomTab
|
||||
import io.nekohasekai.sagernet.ktx.onMainDispatcher
|
||||
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
|
||||
import io.nekohasekai.sagernet.ktx.runOnIoDispatcher
|
||||
import io.nekohasekai.sagernet.utils.PackageCache
|
||||
import io.nekohasekai.sagernet.widget.ListListener
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import moe.matsuri.nb4a.plugin.NekoPluginManager
|
||||
import moe.matsuri.nb4a.plugin.Plugins
|
||||
import moe.matsuri.nb4a.proxy.neko.NekoJSInterface
|
||||
import moe.matsuri.nb4a.ui.Dialogs
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class AppListActivity : ThemedActivity() {
|
||||
@ -81,35 +71,7 @@ class AppListActivity : ThemedActivity() {
|
||||
item = app
|
||||
binding.itemicon.setImageDrawable(app.icon)
|
||||
binding.title.text = app.name
|
||||
if (forNeko) {
|
||||
val packageName = app.packageName
|
||||
val ver = getCachedApps()[packageName]?.versionName ?: ""
|
||||
binding.desc.text = "$packageName ($ver)"
|
||||
//
|
||||
binding.button.isVisible = true
|
||||
binding.button.setImageDrawable(
|
||||
AppCompatResources.getDrawable(
|
||||
this@AppListActivity,
|
||||
R.drawable.ic_baseline_info_24
|
||||
)
|
||||
)
|
||||
binding.button.setOnClickListener {
|
||||
runOnIoDispatcher {
|
||||
val jsi = NekoJSInterface(packageName)
|
||||
jsi.init()
|
||||
val about = jsi.getAbout()
|
||||
jsi.destorySuspend()
|
||||
Dialogs.message(
|
||||
this@AppListActivity, app.name as String,
|
||||
"PackageName: ${packageName}\n" +
|
||||
"Version: ${ver}\n" +
|
||||
"--------\n" + about
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.desc.text = "${app.packageName} (${app.uid})"
|
||||
}
|
||||
binding.desc.text = "${app.packageName} (${app.uid})"
|
||||
handlePayload(listOf(SWITCH))
|
||||
}
|
||||
|
||||
@ -117,7 +79,6 @@ class AppListActivity : ThemedActivity() {
|
||||
if (payloads.contains(SWITCH)) {
|
||||
val selected = isProxiedApp(item)
|
||||
binding.itemcheck.isChecked = selected
|
||||
binding.button.isVisible = forNeko && selected
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,23 +86,6 @@ class AppListActivity : ThemedActivity() {
|
||||
if (isProxiedApp(item)) proxiedUids.delete(item.uid) else proxiedUids[item.uid] = true
|
||||
DataStore.routePackages = apps.filter { isProxiedApp(it) }
|
||||
.joinToString("\n") { it.packageName }
|
||||
|
||||
if (forNeko) {
|
||||
if (isProxiedApp(item)) {
|
||||
runOnIoDispatcher {
|
||||
try {
|
||||
NekoPluginManager.installPlugin(item.packageName)
|
||||
} catch (e: Exception) {
|
||||
// failed UI
|
||||
runOnUiThread { onClick(v) }
|
||||
Dialogs.logExceptionAndShow(this@AppListActivity, e) { }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NekoPluginManager.removeManagedPlugin(item.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
|
||||
}
|
||||
}
|
||||
@ -234,11 +178,8 @@ class AppListActivity : ThemedActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private var forNeko = false
|
||||
|
||||
fun getCachedApps(): MutableMap<String, PackageInfo> {
|
||||
val packages =
|
||||
if (forNeko) PackageCache.installedPluginPackages else PackageCache.installedPackages
|
||||
val packages = PackageCache.installedPackages
|
||||
return packages.toMutableMap().apply {
|
||||
remove(BuildConfig.APPLICATION_ID)
|
||||
}
|
||||
@ -246,7 +187,6 @@ class AppListActivity : ThemedActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
forNeko = intent?.hasExtra(Key.NEKO_PLUGIN_MANAGED) == true
|
||||
|
||||
binding = LayoutAppListBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@ -275,28 +215,13 @@ class AppListActivity : ThemedActivity() {
|
||||
appsAdapter.filter.filter(binding.search.text?.toString() ?: "")
|
||||
}
|
||||
|
||||
if (forNeko) {
|
||||
DataStore.routePackages = DataStore.nekoPlugins
|
||||
binding.search.setText(Plugins.AUTHORITIES_PREFIX_NEKO_PLUGIN)
|
||||
}
|
||||
|
||||
binding.searchLayout.isGone = forNeko
|
||||
binding.hintNekoPlugin.isGone = !forNeko
|
||||
binding.actionLearnMore.setOnClickListener {
|
||||
launchCustomTab("https://matsuridayo.github.io/m-plugin/")
|
||||
}
|
||||
|
||||
loadApps()
|
||||
}
|
||||
|
||||
private var sysApps = false
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
if (forNeko) {
|
||||
menuInflater.inflate(R.menu.app_list_neko_menu, menu)
|
||||
} else {
|
||||
menuInflater.inflate(R.menu.app_list_menu, menu)
|
||||
}
|
||||
menuInflater.inflate(R.menu.app_list_menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -368,9 +293,6 @@ class AppListActivity : ThemedActivity() {
|
||||
proxiedUids.clear()
|
||||
DataStore.routePackages = ""
|
||||
apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))
|
||||
NekoPluginManager.plugins.forEach {
|
||||
NekoPluginManager.removeManagedPlugin(it)
|
||||
}
|
||||
onMainDispatcher {
|
||||
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
|
||||
}
|
||||
@ -394,7 +316,6 @@ class AppListActivity : ThemedActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
loader?.cancel()
|
||||
if (forNeko) DataStore.nekoPlugins = DataStore.routePackages
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,12 +10,15 @@ import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
import android.text.format.Formatter
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.*
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@ -32,43 +35,82 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import io.nekohasekai.sagernet.*
|
||||
import io.nekohasekai.sagernet.GroupOrder
|
||||
import io.nekohasekai.sagernet.GroupType
|
||||
import io.nekohasekai.sagernet.Key
|
||||
import io.nekohasekai.sagernet.R
|
||||
import io.nekohasekai.sagernet.SagerNet
|
||||
import io.nekohasekai.sagernet.aidl.TrafficData
|
||||
import io.nekohasekai.sagernet.bg.BaseService
|
||||
import io.nekohasekai.sagernet.bg.proto.UrlTest
|
||||
import io.nekohasekai.sagernet.database.*
|
||||
import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.database.GroupManager
|
||||
import io.nekohasekai.sagernet.database.ProfileManager
|
||||
import io.nekohasekai.sagernet.database.ProxyEntity
|
||||
import io.nekohasekai.sagernet.database.ProxyGroup
|
||||
import io.nekohasekai.sagernet.database.SagerDatabase
|
||||
import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
|
||||
import io.nekohasekai.sagernet.databinding.LayoutAppsItemBinding
|
||||
import io.nekohasekai.sagernet.databinding.LayoutProfileListBinding
|
||||
import io.nekohasekai.sagernet.databinding.LayoutProgressListBinding
|
||||
import io.nekohasekai.sagernet.fmt.AbstractBean
|
||||
import io.nekohasekai.sagernet.fmt.toUniversalLink
|
||||
import io.nekohasekai.sagernet.group.GroupUpdater
|
||||
import io.nekohasekai.sagernet.group.RawUpdater
|
||||
import io.nekohasekai.sagernet.ktx.*
|
||||
import io.nekohasekai.sagernet.ktx.FixedLinearLayoutManager
|
||||
import io.nekohasekai.sagernet.ktx.Logs
|
||||
import io.nekohasekai.sagernet.ktx.SubscriptionFoundException
|
||||
import io.nekohasekai.sagernet.ktx.alert
|
||||
import io.nekohasekai.sagernet.ktx.app
|
||||
import io.nekohasekai.sagernet.ktx.dp2px
|
||||
import io.nekohasekai.sagernet.ktx.getColorAttr
|
||||
import io.nekohasekai.sagernet.ktx.getColour
|
||||
import io.nekohasekai.sagernet.ktx.isIpAddress
|
||||
import io.nekohasekai.sagernet.ktx.onMainDispatcher
|
||||
import io.nekohasekai.sagernet.ktx.readableMessage
|
||||
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
|
||||
import io.nekohasekai.sagernet.ktx.runOnLifecycleDispatcher
|
||||
import io.nekohasekai.sagernet.ktx.runOnMainDispatcher
|
||||
import io.nekohasekai.sagernet.ktx.scrollTo
|
||||
import io.nekohasekai.sagernet.ktx.showAllowingStateLoss
|
||||
import io.nekohasekai.sagernet.ktx.snackbar
|
||||
import io.nekohasekai.sagernet.ktx.startFilesForResult
|
||||
import io.nekohasekai.sagernet.ktx.tryToShow
|
||||
import io.nekohasekai.sagernet.plugin.PluginManager
|
||||
import io.nekohasekai.sagernet.ui.profile.*
|
||||
import io.nekohasekai.sagernet.utils.PackageCache
|
||||
import io.nekohasekai.sagernet.ui.profile.ChainSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.HttpSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.HysteriaSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.MieruSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.NaiveSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.SSHSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.TrojanGoSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.TrojanSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.TuicSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.VMessSettingsActivity
|
||||
import io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity
|
||||
import io.nekohasekai.sagernet.widget.QRCodeDialog
|
||||
import io.nekohasekai.sagernet.widget.UndoSnackbarManager
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.newFixedThreadPoolContext
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import moe.matsuri.nb4a.Protocols
|
||||
import moe.matsuri.nb4a.Protocols.getProtocolColor
|
||||
import moe.matsuri.nb4a.plugin.NekoPluginManager
|
||||
import moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity
|
||||
import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity
|
||||
import moe.matsuri.nb4a.proxy.neko.NekoJSInterface
|
||||
import moe.matsuri.nb4a.proxy.neko.NekoSettingActivity
|
||||
import moe.matsuri.nb4a.proxy.neko.canShare
|
||||
import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity
|
||||
import okhttp3.internal.closeQuietly
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.net.UnknownHostException
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.ZipInputStream
|
||||
@ -403,38 +445,6 @@ class ConfigurationFragment @JvmOverloads constructor(
|
||||
startActivity(Intent(requireActivity(), ChainSettingsActivity::class.java))
|
||||
}
|
||||
|
||||
R.id.action_new_neko -> {
|
||||
val context = requireContext()
|
||||
lateinit var dialog: AlertDialog
|
||||
val linearLayout = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
|
||||
NekoPluginManager.getProtocols().forEach { obj ->
|
||||
LayoutAppsItemBinding.inflate(layoutInflater, this, true).apply {
|
||||
itemcheck.isGone = true
|
||||
button.isGone = false
|
||||
itemicon.setImageDrawable(
|
||||
PackageCache.installedApps[obj.plgId]?.loadIcon(
|
||||
context.packageManager
|
||||
)
|
||||
)
|
||||
title.text = obj.protocolId
|
||||
desc.text = obj.plgId
|
||||
button.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
val intent = Intent(context, NekoSettingActivity::class.java)
|
||||
intent.putExtra("plgId", obj.plgId)
|
||||
intent.putExtra("protocolId", obj.protocolId)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog = MaterialAlertDialogBuilder(context).setTitle(R.string.neko_plugin)
|
||||
.setView(linearLayout)
|
||||
.show()
|
||||
}
|
||||
|
||||
R.id.action_update_subscription -> {
|
||||
val group = DataStore.currentGroup()
|
||||
if (group.type != GroupType.SUBSCRIPTION) {
|
||||
@ -870,7 +880,6 @@ class ConfigurationFragment @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
GroupManager.postReload(DataStore.currentGroupId())
|
||||
NekoJSInterface.Default.destroyAllJsi()
|
||||
mainJob.cancel()
|
||||
testJobs.forEach { it.cancel() }
|
||||
}
|
||||
@ -1591,7 +1600,7 @@ class ConfigurationFragment @JvmOverloads constructor(
|
||||
removeButton.isGone = select
|
||||
|
||||
proxyEntity.nekoBean?.apply {
|
||||
shareLayout.isGone = !canShare()
|
||||
shareLayout.isGone = true
|
||||
}
|
||||
|
||||
runOnDefaultDispatcher {
|
||||
|
||||
@ -16,13 +16,11 @@ import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers
|
||||
import io.nekohasekai.sagernet.ktx.*
|
||||
import io.nekohasekai.sagernet.utils.Theme
|
||||
import io.nekohasekai.sagernet.widget.AppListPreference
|
||||
import moe.matsuri.nb4a.ui.*
|
||||
|
||||
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private lateinit var isProxyApps: SwitchPreference
|
||||
private lateinit var nekoPlugins: AppListPreference
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@ -40,16 +38,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
DataStore.initGlobal()
|
||||
addPreferencesFromResource(R.xml.global_preferences)
|
||||
|
||||
DataStore.routePackages = DataStore.nekoPlugins
|
||||
nekoPlugins = findPreference(Key.NEKO_PLUGIN_MANAGED)!!
|
||||
nekoPlugins.setOnPreferenceClickListener {
|
||||
// borrow from route app settings
|
||||
startActivity(Intent(
|
||||
context, AppListActivity::class.java
|
||||
).apply { putExtra(Key.NEKO_PLUGIN_MANAGED, true) })
|
||||
true
|
||||
}
|
||||
|
||||
val appTheme = findPreference<ColorPickerPreference>(Key.APP_THEME)!!
|
||||
appTheme.setOnPreferenceChangeListener { _, newTheme ->
|
||||
if (DataStore.serviceState.started) {
|
||||
@ -183,9 +171,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
if (::isProxyApps.isInitialized) {
|
||||
isProxyApps.isChecked = DataStore.proxyApps
|
||||
}
|
||||
if (::nekoPlugins.isInitialized) {
|
||||
nekoPlugins.postUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -51,7 +51,7 @@ object PackageCache {
|
||||
}.associateBy { it.packageName }
|
||||
|
||||
installedPluginPackages = rawPackageInfo.filter {
|
||||
Plugins.isExeOrPlugin(it)
|
||||
Plugins.isExe(it)
|
||||
}.associateBy { it.packageName }
|
||||
|
||||
val installed = app.packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
package moe.matsuri.nb4a.plugin
|
||||
|
||||
import io.nekohasekai.sagernet.R
|
||||
import io.nekohasekai.sagernet.SagerNet
|
||||
import io.nekohasekai.sagernet.bg.BaseService
|
||||
import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.ktx.forEach
|
||||
import io.nekohasekai.sagernet.utils.PackageCache
|
||||
import moe.matsuri.nb4a.proxy.neko.NekoJSInterface
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.zip.CRC32
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
object NekoPluginManager {
|
||||
const val managerVersion = 2
|
||||
|
||||
val plugins get() = DataStore.nekoPlugins.split("\n").filter { it.isNotBlank() }
|
||||
|
||||
// plgID to plgConfig object
|
||||
fun getManagedPlugins(): Map<String, JSONObject> {
|
||||
val ret = mutableMapOf<String, JSONObject>()
|
||||
plugins.forEach {
|
||||
tryGetPlgConfig(it)?.apply {
|
||||
ret[it] = this
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
class Protocol(
|
||||
val protocolId: String, val plgId: String, val protocolConfig: JSONObject
|
||||
)
|
||||
|
||||
fun getProtocols(): List<Protocol> {
|
||||
val ret = mutableListOf<Protocol>()
|
||||
getManagedPlugins().forEach { (t, u) ->
|
||||
u.optJSONArray("protocols")?.forEach { _, any ->
|
||||
if (any is JSONObject) {
|
||||
val name = any.optString("protocolId")
|
||||
ret.add(Protocol(name, t, any))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
fun findProtocol(protocolId: String): Protocol? {
|
||||
getManagedPlugins().forEach { (t, u) ->
|
||||
u.optJSONArray("protocols")?.forEach { _, any ->
|
||||
if (any is JSONObject) {
|
||||
if (protocolId == any.optString("protocolId")) {
|
||||
return Protocol(protocolId, t, any)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun removeManagedPlugin(plgId: String) {
|
||||
DataStore.configurationStore.remove(plgId)
|
||||
val dir = File(SagerNet.application.filesDir.absolutePath + "/plugins/" + plgId)
|
||||
if (dir.exists()) {
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
fun extractPlugin(plgId: String, install: Boolean) {
|
||||
val app = PackageCache.installedApps[plgId] ?: return
|
||||
val apk = File(app.publicSourceDir)
|
||||
if (!apk.exists()) {
|
||||
return
|
||||
}
|
||||
if (!install && !plugins.contains(plgId)) {
|
||||
return
|
||||
}
|
||||
|
||||
val zipFile = ZipFile(apk)
|
||||
val unzipDir = File(SagerNet.application.filesDir.absolutePath + "/plugins/" + plgId)
|
||||
unzipDir.mkdirs()
|
||||
for (entry in zipFile.entries()) {
|
||||
if (entry.name.startsWith("assets/")) {
|
||||
val relativePath = entry.name.removePrefix("assets/")
|
||||
val outFile = File(unzipDir, relativePath)
|
||||
if (entry.isDirectory) {
|
||||
outFile.mkdirs()
|
||||
continue
|
||||
}
|
||||
|
||||
if (outFile.isDirectory) {
|
||||
outFile.delete()
|
||||
} else if (outFile.exists()) {
|
||||
val checksum = CRC32()
|
||||
checksum.update(outFile.readBytes())
|
||||
if (checksum.value == entry.crc) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
val input = zipFile.getInputStream(entry)
|
||||
outFile.outputStream().use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
zipFile.closeQuietly()
|
||||
}
|
||||
|
||||
suspend fun installPlugin(plgId: String) {
|
||||
if (plgId == "moe.matsuri.plugin.singbox" || plgId == "moe.matsuri.plugin.xray") {
|
||||
throw Exception("This plugin is deprecated")
|
||||
}
|
||||
extractPlugin(plgId, true)
|
||||
NekoJSInterface.Default.destroyJsi(plgId)
|
||||
NekoJSInterface.Default.requireJsi(plgId).init()
|
||||
NekoJSInterface.Default.destroyJsi(plgId)
|
||||
}
|
||||
|
||||
const val PLUGIN_APP_VERSION = "_v_vc"
|
||||
const val PLUGIN_APP_VERSION_NAME = "_v_vn"
|
||||
|
||||
// Return null if not managed
|
||||
fun tryGetPlgConfig(plgId: String): JSONObject? {
|
||||
return try {
|
||||
JSONObject(DataStore.configurationStore.getString(plgId)!!)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePlgConfig(plgId: String, plgConfig: JSONObject) {
|
||||
PackageCache.installedPluginPackages[plgId]?.apply {
|
||||
// longVersionCode requires API 28
|
||||
// plgConfig.put(PLUGIN_APP_VERSION, versionCode)
|
||||
plgConfig.put(PLUGIN_APP_VERSION_NAME, versionName)
|
||||
}
|
||||
DataStore.configurationStore.putString(plgId, plgConfig.toString())
|
||||
}
|
||||
|
||||
fun htmlPath(plgId: String): String {
|
||||
val htmlFile = File(SagerNet.application.filesDir.absolutePath + "/plugins/" + plgId)
|
||||
return htmlFile.absolutePath
|
||||
}
|
||||
|
||||
class PluginInternalException(val protocolId: String) : Exception(),
|
||||
BaseService.ExpectedException {
|
||||
override fun getLocalizedMessage() =
|
||||
SagerNet.application.getString(R.string.neko_plugin_internal_error, protocolId)
|
||||
}
|
||||
|
||||
}
|
||||
@ -14,20 +14,18 @@ import io.nekohasekai.sagernet.utils.PackageCache
|
||||
object Plugins {
|
||||
const val AUTHORITIES_PREFIX_SEKAI_EXE = "io.nekohasekai.sagernet.plugin."
|
||||
const val AUTHORITIES_PREFIX_NEKO_EXE = "moe.matsuri.exe."
|
||||
const val AUTHORITIES_PREFIX_NEKO_PLUGIN = "moe.matsuri.plugin."
|
||||
|
||||
const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN"
|
||||
|
||||
const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id"
|
||||
const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path"
|
||||
|
||||
fun isExeOrPlugin(pkg: PackageInfo): Boolean {
|
||||
fun isExe(pkg: PackageInfo): Boolean {
|
||||
if (pkg.providers?.isEmpty() == true) return false
|
||||
val provider = pkg.providers?.get(0) ?: return false
|
||||
val auth = provider.authority ?: return false
|
||||
return auth.startsWith(AUTHORITIES_PREFIX_SEKAI_EXE)
|
||||
|| auth.startsWith(AUTHORITIES_PREFIX_NEKO_EXE)
|
||||
|| auth.startsWith(AUTHORITIES_PREFIX_NEKO_PLUGIN)
|
||||
}
|
||||
|
||||
fun preferExePrefix(): String {
|
||||
|
||||
@ -8,18 +8,12 @@ import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import io.nekohasekai.sagernet.R;
|
||||
import io.nekohasekai.sagernet.SagerNet;
|
||||
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||
import io.nekohasekai.sagernet.ktx.Logs;
|
||||
import moe.matsuri.nb4a.plugin.NekoPluginManager;
|
||||
|
||||
public class NekoBean extends AbstractBean {
|
||||
|
||||
// BoxInstance use this
|
||||
public JSONObject allConfig = null;
|
||||
|
||||
public String plgId;
|
||||
public String protocolId;
|
||||
public JSONObject sharedStorage = new JSONObject();
|
||||
@ -62,31 +56,22 @@ public class NekoBean extends AbstractBean {
|
||||
}
|
||||
|
||||
public String displayType() {
|
||||
NekoPluginManager.Protocol p = NekoPluginManager.INSTANCE.findProtocol(protocolId);
|
||||
String neko = SagerNet.application.getResources().getString(R.string.neko_plugin);
|
||||
if (p == null) return neko;
|
||||
return p.getProtocolId();
|
||||
return "invalid";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canMapping() {
|
||||
NekoPluginManager.Protocol p = NekoPluginManager.INSTANCE.findProtocol(protocolId);
|
||||
if (p == null) return false;
|
||||
return p.getProtocolConfig().optBoolean("canMapping");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canICMPing() {
|
||||
NekoPluginManager.Protocol p = NekoPluginManager.INSTANCE.findProtocol(protocolId);
|
||||
if (p == null) return false;
|
||||
return p.getProtocolConfig().optBoolean("canICMPing");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTCPing() {
|
||||
NekoPluginManager.Protocol p = NekoPluginManager.INSTANCE.findProtocol(protocolId);
|
||||
if (p == null) return false;
|
||||
return p.getProtocolConfig().optBoolean("canTCPing");
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@ -1,123 +0,0 @@
|
||||
package moe.matsuri.nb4a.proxy.neko
|
||||
|
||||
import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.ktx.Logs
|
||||
import io.nekohasekai.sagernet.ktx.getStr
|
||||
import io.nekohasekai.sagernet.ktx.runOnIoDispatcher
|
||||
import libcore.Libcore
|
||||
import moe.matsuri.nb4a.Protocols
|
||||
import moe.matsuri.nb4a.plugin.NekoPluginManager
|
||||
import org.json.JSONObject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend fun parseShareLink(plgId: String, protocolId: String, link: String): NekoBean =
|
||||
suspendCoroutine {
|
||||
runOnIoDispatcher {
|
||||
val jsi = NekoJSInterface.Default.requireJsi(plgId)
|
||||
jsi.lock()
|
||||
|
||||
try {
|
||||
jsi.init()
|
||||
|
||||
val jsip = jsi.switchProtocol(protocolId)
|
||||
val sharedStorage = jsip.parseShareLink(link)
|
||||
|
||||
// NekoBean from link
|
||||
val bean = NekoBean()
|
||||
bean.plgId = plgId
|
||||
bean.protocolId = protocolId
|
||||
bean.sharedStorage = NekoBean.tryParseJSON(sharedStorage)
|
||||
bean.onSharedStorageSet()
|
||||
|
||||
it.resume(bean)
|
||||
} catch (e: Exception) {
|
||||
Logs.e(e)
|
||||
it.resume(NekoBean().apply {
|
||||
this.plgId = plgId
|
||||
this.protocolId = protocolId
|
||||
})
|
||||
}
|
||||
|
||||
jsi.unlock()
|
||||
// destroy when all link parsed
|
||||
}
|
||||
}
|
||||
|
||||
fun NekoBean.shareLink(): String {
|
||||
return sharedStorage.optString("shareLink")
|
||||
}
|
||||
|
||||
// Only run in bg process
|
||||
// seems no concurrent
|
||||
suspend fun NekoBean.updateAllConfig(port: Int) = suspendCoroutine<Unit> {
|
||||
allConfig = null
|
||||
|
||||
runOnIoDispatcher {
|
||||
val jsi = NekoJSInterface.Default.requireJsi(plgId)
|
||||
jsi.lock()
|
||||
|
||||
try {
|
||||
jsi.init()
|
||||
val jsip = jsi.switchProtocol(protocolId)
|
||||
|
||||
// runtime arguments
|
||||
val otherArgs = mutableMapOf<String, Any>()
|
||||
otherArgs["finalAddress"] = finalAddress
|
||||
otherArgs["finalPort"] = finalPort
|
||||
// otherArgs["muxEnabled"] = Protocols.shouldEnableMux(protocolId)
|
||||
// otherArgs["muxConcurrency"] = DataStore.muxConcurrency
|
||||
|
||||
val ret = jsip.buildAllConfig(port, this@updateAllConfig, otherArgs)
|
||||
|
||||
// result
|
||||
allConfig = JSONObject(ret)
|
||||
} catch (e: Exception) {
|
||||
Logs.e(e)
|
||||
}
|
||||
|
||||
jsi.unlock()
|
||||
it.resume(Unit)
|
||||
// destroy when config generated / all tests finished
|
||||
}
|
||||
}
|
||||
|
||||
fun NekoBean.cacheGet(id: String): String? {
|
||||
return DataStore.profileCacheStore.getString("neko_${hash()}_$id")
|
||||
}
|
||||
|
||||
fun NekoBean.cacheSet(id: String, value: String) {
|
||||
DataStore.profileCacheStore.putString("neko_${hash()}_$id", value)
|
||||
}
|
||||
|
||||
fun NekoBean.hash(): String {
|
||||
var a = plgId
|
||||
a += protocolId
|
||||
a += sharedStorage.toString()
|
||||
return Libcore.sha256Hex(a.toByteArray())
|
||||
}
|
||||
|
||||
// must call it to update something like serverAddress
|
||||
fun NekoBean.onSharedStorageSet() {
|
||||
serverAddress = sharedStorage.getStr("serverAddress")
|
||||
serverPort = sharedStorage.getStr("serverPort")?.toInt() ?: 1080
|
||||
if (serverAddress == null || serverAddress.isBlank()) {
|
||||
serverAddress = "127.0.0.1"
|
||||
}
|
||||
name = sharedStorage.optString("name")
|
||||
}
|
||||
|
||||
fun NekoBean.needBypassRootUid(): Boolean {
|
||||
val p = NekoPluginManager.findProtocol(protocolId) ?: return false
|
||||
return p.protocolConfig.optBoolean("needBypassRootUid")
|
||||
}
|
||||
|
||||
fun NekoBean.haveStandardLink(): Boolean {
|
||||
val p = NekoPluginManager.findProtocol(protocolId) ?: return false
|
||||
return p.protocolConfig.optBoolean("haveStandardLink")
|
||||
}
|
||||
|
||||
fun NekoBean.canShare(): Boolean {
|
||||
val p = NekoPluginManager.findProtocol(protocolId) ?: return false
|
||||
return p.protocolConfig.optBoolean("canShare")
|
||||
}
|
||||
@ -1,388 +0,0 @@
|
||||
package moe.matsuri.nb4a.proxy.neko
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.webkit.*
|
||||
import android.widget.Toast
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import io.nekohasekai.sagernet.BuildConfig
|
||||
import io.nekohasekai.sagernet.SagerNet
|
||||
import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.ktx.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withContext
|
||||
import moe.matsuri.nb4a.plugin.NekoPluginManager
|
||||
import moe.matsuri.nb4a.ui.SimpleMenuPreference
|
||||
import moe.matsuri.nb4a.utils.JavaUtil
|
||||
import moe.matsuri.nb4a.utils.Util
|
||||
import moe.matsuri.nb4a.utils.WebViewUtil
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class NekoJSInterface(val plgId: String) {
|
||||
|
||||
private val mutex = Mutex()
|
||||
private var webView: WebView? = null
|
||||
val jsObject = JsObject()
|
||||
var plgConfig: JSONObject? = null
|
||||
var plgConfigException: Exception? = null
|
||||
val protocols = mutableMapOf<String, NekoProtocol>()
|
||||
val loaded = AtomicBoolean()
|
||||
|
||||
suspend fun lock() {
|
||||
mutex.lock(null)
|
||||
}
|
||||
|
||||
fun unlock() {
|
||||
mutex.unlock(null)
|
||||
}
|
||||
|
||||
// load webview and js
|
||||
// Return immediately when already loaded
|
||||
// Return plgConfig or throw exception
|
||||
suspend fun init() = withContext(Dispatchers.Main) {
|
||||
initInternal()
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private suspend fun initInternal() = suspendCoroutine<JSONObject> {
|
||||
if (loaded.get()) {
|
||||
plgConfig?.apply {
|
||||
it.resume(this)
|
||||
return@suspendCoroutine
|
||||
}
|
||||
plgConfigException?.apply {
|
||||
it.resumeWithException(this)
|
||||
return@suspendCoroutine
|
||||
}
|
||||
it.resumeWithException(Exception("wtf"))
|
||||
return@suspendCoroutine
|
||||
}
|
||||
|
||||
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
||||
NekoPluginManager.extractPlugin(plgId, false)
|
||||
|
||||
webView = WebView(SagerNet.application.applicationContext)
|
||||
webView!!.settings.javaScriptEnabled = true
|
||||
webView!!.addJavascriptInterface(jsObject, "neko")
|
||||
webView!!.webViewClient = object : WebViewClient() {
|
||||
// provide files
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?, request: WebResourceRequest?
|
||||
): WebResourceResponse {
|
||||
return WebViewUtil.interceptRequest(
|
||||
{ res ->
|
||||
val f = File(NekoPluginManager.htmlPath(plgId), res)
|
||||
if (f.exists()) {
|
||||
FileInputStream(f)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
view,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView?, request: WebResourceRequest?, error: WebResourceError?
|
||||
) {
|
||||
WebViewUtil.onReceivedError(view, request, error)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
if (loaded.getAndSet(true)) return
|
||||
|
||||
runOnIoDispatcher {
|
||||
// Process nekoInit
|
||||
var ret = ""
|
||||
try {
|
||||
ret = nekoInit()
|
||||
val obj = JSONObject(ret)
|
||||
if (!obj.getBoolean("ok")) {
|
||||
throw Exception("plugin refuse to run: ${obj.optString("reason")}")
|
||||
}
|
||||
val min = obj.getInt("minVersion")
|
||||
if (min > NekoPluginManager.managerVersion) {
|
||||
throw Exception("manager version ${NekoPluginManager.managerVersion} too old, this plugin requires >= $min")
|
||||
}
|
||||
plgConfig = obj
|
||||
NekoPluginManager.updatePlgConfig(plgId, obj)
|
||||
it.resume(obj)
|
||||
} catch (e: Exception) {
|
||||
val e2 = Exception("nekoInit: " + e.readableMessage + "\n\n" + ret)
|
||||
plgConfigException = e2
|
||||
it.resumeWithException(e2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
webView!!.loadUrl("http://$plgId/plugin.html")
|
||||
}
|
||||
|
||||
// Android call JS
|
||||
|
||||
private suspend fun callJS(script: String): String = suspendCoroutine {
|
||||
val jsLatch = CountDownLatch(1)
|
||||
var jsReceivedValue = ""
|
||||
|
||||
runOnMainDispatcher {
|
||||
if (webView != null) {
|
||||
webView!!.evaluateJavascript(script) { value ->
|
||||
jsReceivedValue = value
|
||||
jsLatch.countDown()
|
||||
}
|
||||
} else {
|
||||
jsReceivedValue = "webView is null"
|
||||
jsLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
jsLatch.await(5, TimeUnit.SECONDS)
|
||||
|
||||
// evaluateJavascript escapes Javascript's String
|
||||
jsReceivedValue = JavaUtil.unescapeString(jsReceivedValue.removeSurrounding("\""))
|
||||
if (BuildConfig.DEBUG) Logs.d("$script: $jsReceivedValue")
|
||||
it.resume(jsReceivedValue)
|
||||
}
|
||||
|
||||
// call once
|
||||
private suspend fun nekoInit(): String {
|
||||
val sendData = JSONObject()
|
||||
sendData.put("lang", Locale.getDefault().toLanguageTag())
|
||||
sendData.put("plgId", plgId)
|
||||
sendData.put("managerVersion", NekoPluginManager.managerVersion)
|
||||
|
||||
return callJS(
|
||||
"nekoInit(\"${
|
||||
Util.b64EncodeUrlSafe(
|
||||
sendData.toString().toByteArray()
|
||||
)
|
||||
}\")"
|
||||
)
|
||||
}
|
||||
|
||||
fun switchProtocol(id: String): NekoProtocol {
|
||||
lateinit var p: NekoProtocol
|
||||
if (protocols.containsKey(id)) {
|
||||
p = protocols[id]!!
|
||||
} else {
|
||||
p = NekoProtocol(id) { callJS(it) }
|
||||
protocols[id] = p
|
||||
}
|
||||
jsObject.protocol = p
|
||||
return p
|
||||
}
|
||||
|
||||
suspend fun getAbout(): String {
|
||||
return callJS("nekoAbout()")
|
||||
}
|
||||
|
||||
inner class NekoProtocol(val protocolId: String, val callJS: suspend (String) -> String) {
|
||||
private suspend fun callProtocol(method: String, b64Str: String?): String {
|
||||
var arg = ""
|
||||
if (b64Str != null) {
|
||||
arg = "\"" + b64Str + "\""
|
||||
}
|
||||
return callJS("nekoProtocol(\"$protocolId\").$method($arg)")
|
||||
}
|
||||
|
||||
suspend fun buildAllConfig(
|
||||
port: Int, bean: NekoBean, otherArgs: Map<String, Any>?
|
||||
): String {
|
||||
val sendData = JSONObject()
|
||||
sendData.put("port", port)
|
||||
sendData.put(
|
||||
"sharedStorage",
|
||||
Util.b64EncodeUrlSafe(bean.sharedStorage.toString().toByteArray())
|
||||
)
|
||||
otherArgs?.forEach { (t, u) -> sendData.put(t, u) }
|
||||
|
||||
return callProtocol(
|
||||
"buildAllConfig", Util.b64EncodeUrlSafe(sendData.toString().toByteArray())
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun parseShareLink(shareLink: String): String {
|
||||
val sendData = JSONObject()
|
||||
sendData.put("shareLink", shareLink)
|
||||
|
||||
return callProtocol(
|
||||
"parseShareLink", Util.b64EncodeUrlSafe(sendData.toString().toByteArray())
|
||||
)
|
||||
}
|
||||
|
||||
// UI Interface
|
||||
|
||||
suspend fun setSharedStorage(sharedStorage: String) {
|
||||
callProtocol(
|
||||
"setSharedStorage",
|
||||
Util.b64EncodeUrlSafe(sharedStorage.toByteArray())
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun requireSetProfileCache() {
|
||||
callProtocol("requireSetProfileCache", null)
|
||||
}
|
||||
|
||||
suspend fun requirePreferenceScreenConfig(): String {
|
||||
return callProtocol("requirePreferenceScreenConfig", null)
|
||||
}
|
||||
|
||||
suspend fun sharedStorageFromProfileCache(): String {
|
||||
return callProtocol("sharedStorageFromProfileCache", null)
|
||||
}
|
||||
|
||||
suspend fun onPreferenceCreated() {
|
||||
callProtocol("onPreferenceCreated", null)
|
||||
}
|
||||
|
||||
suspend fun onPreferenceChanged(key: String, v: Any) {
|
||||
val sendData = JSONObject()
|
||||
sendData.put("key", key)
|
||||
sendData.put("newValue", v)
|
||||
|
||||
callProtocol(
|
||||
"onPreferenceChanged",
|
||||
Util.b64EncodeUrlSafe(sendData.toString().toByteArray())
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inner class JsObject {
|
||||
var preferenceScreen: PreferenceScreen? = null
|
||||
var protocol: NekoProtocol? = null
|
||||
|
||||
// JS call Android
|
||||
|
||||
@JavascriptInterface
|
||||
fun toast(s: String) {
|
||||
Toast.makeText(SagerNet.application.applicationContext, s, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun logError(s: String) {
|
||||
Logs.e("logError: $s")
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun setPreferenceVisibility(key: String, isVisible: Boolean) {
|
||||
runBlockingOnMainDispatcher {
|
||||
preferenceScreen?.findPreference<Preference>(key)?.isVisible = isVisible
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun setPreferenceTitle(key: String, title: String) {
|
||||
runBlockingOnMainDispatcher {
|
||||
preferenceScreen?.findPreference<Preference>(key)?.title = title
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun setMenu(key: String, entries: String) {
|
||||
runBlockingOnMainDispatcher {
|
||||
preferenceScreen?.findPreference<SimpleMenuPreference>(key)?.apply {
|
||||
NekoPreferenceInflater.setMenu(this, JSONObject(entries))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun listenOnPreferenceChanged(key: String) {
|
||||
preferenceScreen?.findPreference<Preference>(key)
|
||||
?.setOnPreferenceChangeListener { preference, newValue ->
|
||||
runOnIoDispatcher {
|
||||
protocol?.onPreferenceChanged(preference.key, newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun setKV(type: Int, key: String, jsonStr: String) {
|
||||
try {
|
||||
val v = JSONObject(jsonStr)
|
||||
when (type) {
|
||||
0 -> DataStore.profileCacheStore.putBoolean(key, v.getBoolean("v"))
|
||||
1 -> DataStore.profileCacheStore.putFloat(key, v.getDouble("v").toFloat())
|
||||
2 -> DataStore.profileCacheStore.putInt(key, v.getInt("v"))
|
||||
3 -> DataStore.profileCacheStore.putLong(key, v.getLong("v"))
|
||||
4 -> DataStore.profileCacheStore.putString(key, v.getString("v"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logs.e("setKV: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getKV(type: Int, key: String): String {
|
||||
val v = JSONObject()
|
||||
try {
|
||||
when (type) {
|
||||
0 -> v.put("v", DataStore.profileCacheStore.getBoolean(key))
|
||||
1 -> v.put("v", DataStore.profileCacheStore.getFloat(key))
|
||||
2 -> v.put("v", DataStore.profileCacheStore.getInt(key))
|
||||
3 -> v.put("v", DataStore.profileCacheStore.getLong(key))
|
||||
4 -> v.put("v", DataStore.profileCacheStore.getString(key))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logs.e("getKV: $e")
|
||||
}
|
||||
return v.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
webView?.onPause()
|
||||
webView?.removeAllViews()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
|
||||
suspend fun destorySuspend() = withContext(Dispatchers.Main) {
|
||||
destroy()
|
||||
}
|
||||
|
||||
object Default {
|
||||
val map = mutableMapOf<String, NekoJSInterface>()
|
||||
|
||||
suspend fun destroyJsi(plgId: String) = withContext(Dispatchers.Main) {
|
||||
if (map.containsKey(plgId)) {
|
||||
map[plgId]!!.destroy()
|
||||
map.remove(plgId)
|
||||
}
|
||||
}
|
||||
|
||||
// now it's manually managed
|
||||
suspend fun destroyAllJsi() = withContext(Dispatchers.Main) {
|
||||
map.forEach { (t, u) ->
|
||||
u.destroy()
|
||||
map.remove(t)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requireJsi(plgId: String): NekoJSInterface = withContext(Dispatchers.Main) {
|
||||
lateinit var jsi: NekoJSInterface
|
||||
if (map.containsKey(plgId)) {
|
||||
jsi = map[plgId]!!
|
||||
} else {
|
||||
jsi = NekoJSInterface(plgId)
|
||||
map[plgId] = jsi
|
||||
}
|
||||
return@withContext jsi
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
package moe.matsuri.nb4a.proxy.neko
|
||||
|
||||
import androidx.preference.*
|
||||
import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers
|
||||
import io.nekohasekai.sagernet.ktx.forEach
|
||||
import io.nekohasekai.sagernet.ktx.getStr
|
||||
import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import moe.matsuri.nb4a.ui.SimpleMenuPreference
|
||||
import moe.matsuri.nb4a.utils.getDrawableByName
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
object NekoPreferenceInflater {
|
||||
suspend fun inflate(pref: JSONArray, preferenceScreen: PreferenceScreen) =
|
||||
withContext(Dispatchers.Main) {
|
||||
val context = preferenceScreen.context
|
||||
pref.forEach { _, category ->
|
||||
category as JSONObject
|
||||
|
||||
val preferenceCategory = PreferenceCategory(context)
|
||||
preferenceScreen.addPreference(preferenceCategory)
|
||||
|
||||
category.getStr("key")?.apply { preferenceCategory.key = this }
|
||||
category.getStr("title")?.apply { preferenceCategory.title = this }
|
||||
|
||||
category.optJSONArray("preferences")?.forEach { _, any ->
|
||||
if (any is JSONObject) {
|
||||
lateinit var p: Preference
|
||||
// Create Preference
|
||||
when (any.getStr("type")) {
|
||||
"EditTextPreference" -> {
|
||||
p = EditTextPreference(context).apply {
|
||||
when (any.getStr("summaryProvider")) {
|
||||
null -> summaryProvider =
|
||||
EditTextPreference.SimpleSummaryProvider.getInstance()
|
||||
"PasswordSummaryProvider" -> summaryProvider =
|
||||
ProfileSettingsActivity.PasswordSummaryProvider
|
||||
}
|
||||
when (any.getStr("EditTextPreferenceModifiers")) {
|
||||
"Monospace" -> setOnBindEditTextListener(
|
||||
EditTextPreferenceModifiers.Monospace
|
||||
)
|
||||
"Hosts" -> setOnBindEditTextListener(
|
||||
EditTextPreferenceModifiers.Hosts
|
||||
)
|
||||
"Port" -> setOnBindEditTextListener(
|
||||
EditTextPreferenceModifiers.Port
|
||||
)
|
||||
"Number" -> setOnBindEditTextListener(
|
||||
EditTextPreferenceModifiers.Number
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
"SwitchPreference" -> {
|
||||
p = SwitchPreference(context)
|
||||
}
|
||||
"SimpleMenuPreference" -> {
|
||||
p = SimpleMenuPreference(context).apply {
|
||||
val entries = any.optJSONObject("entries")
|
||||
if (entries != null) setMenu(this, entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set key & title
|
||||
p.key = any.getString("key")
|
||||
any.getStr("title")?.apply { p.title = this }
|
||||
// Set icon
|
||||
any.getStr("icon")?.apply {
|
||||
p.icon = context.getDrawableByName(this)
|
||||
}
|
||||
// Set summary
|
||||
any.getStr("summary")?.apply {
|
||||
p.summary = this
|
||||
}
|
||||
// Add to category
|
||||
preferenceCategory.addPreference(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMenu(p: SimpleMenuPreference, entries: JSONObject) {
|
||||
val menuEntries = mutableListOf<String>()
|
||||
val menuEntryValues = mutableListOf<String>()
|
||||
entries.forEach { s, b ->
|
||||
menuEntryValues.add(s)
|
||||
menuEntries.add(b as String)
|
||||
}
|
||||
entries.apply {
|
||||
p.entries = menuEntries.toTypedArray()
|
||||
p.entryValues = menuEntryValues.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
package moe.matsuri.nb4a.proxy.neko
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import io.nekohasekai.sagernet.Key
|
||||
import io.nekohasekai.sagernet.R
|
||||
import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.ktx.runOnIoDispatcher
|
||||
import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity
|
||||
import moe.matsuri.nb4a.ui.Dialogs
|
||||
import org.json.JSONArray
|
||||
|
||||
class NekoSettingActivity : ProfileSettingsActivity<NekoBean>() {
|
||||
|
||||
lateinit var jsi: NekoJSInterface
|
||||
lateinit var jsip: NekoJSInterface.NekoProtocol
|
||||
lateinit var plgId: String
|
||||
lateinit var protocolId: String
|
||||
var loaded = false
|
||||
|
||||
override fun createEntity() = NekoBean()
|
||||
|
||||
override fun NekoBean.init() {
|
||||
if (!this@NekoSettingActivity::plgId.isInitialized) this@NekoSettingActivity.plgId = plgId
|
||||
if (!this@NekoSettingActivity::protocolId.isInitialized) this@NekoSettingActivity.protocolId = protocolId
|
||||
DataStore.profileCacheStore.putString("name", name)
|
||||
DataStore.sharedStorage = sharedStorage.toString()
|
||||
}
|
||||
|
||||
override fun NekoBean.serialize() {
|
||||
// NekoBean from input
|
||||
plgId = this@NekoSettingActivity.plgId
|
||||
protocolId = this@NekoSettingActivity.protocolId
|
||||
|
||||
sharedStorage = NekoBean.tryParseJSON(DataStore.sharedStorage)
|
||||
onSharedStorageSet()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent?.getStringExtra("plgId")?.apply { plgId = this }
|
||||
intent?.getStringExtra("protocolId")?.apply { protocolId = this }
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
listView.isVisible = false
|
||||
}
|
||||
|
||||
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
|
||||
if (loaded && key != Key.PROFILE_DIRTY) {
|
||||
DataStore.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun PreferenceFragmentCompat.createPreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
addPreferencesFromResource(R.xml.neko_preferences)
|
||||
|
||||
// Create a jsi
|
||||
jsi = NekoJSInterface(plgId)
|
||||
runOnIoDispatcher {
|
||||
try {
|
||||
jsi.init()
|
||||
jsip = jsi.switchProtocol(protocolId)
|
||||
jsi.jsObject.preferenceScreen = preferenceScreen
|
||||
|
||||
// Because of the Preference problem, first require the KV and then inflate the UI
|
||||
jsip.setSharedStorage(DataStore.sharedStorage)
|
||||
jsip.requireSetProfileCache()
|
||||
|
||||
val config = jsip.requirePreferenceScreenConfig()
|
||||
val pref = JSONArray(config)
|
||||
|
||||
NekoPreferenceInflater.inflate(pref, preferenceScreen)
|
||||
jsip.onPreferenceCreated()
|
||||
|
||||
runOnUiThread {
|
||||
loaded = true
|
||||
listView.isVisible = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Dialogs.logExceptionAndShow(this@NekoSettingActivity, e) { finish() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveAndExit() {
|
||||
DataStore.sharedStorage = jsip.sharedStorageFromProfileCache()
|
||||
super.saveAndExit() // serialize & finish
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
jsi.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
}
|
||||
@ -82,61 +82,6 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/hint_neko_plugin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/neko_plugin_summary"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/action_learn_more"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/action_learn_more"
|
||||
android:textColor="?primaryOrTextPrimary" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
@ -58,15 +58,6 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/ic_action_note_add"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/itemcheck"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@ -77,9 +77,6 @@
|
||||
android:title="@string/proxy_chain" />
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/action_new_neko"
|
||||
android:title="@string/neko_plugin" />
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
|
||||
@ -446,7 +446,5 @@
|
||||
<string name="action_switch">Cambiar</string>
|
||||
|
||||
<string name="connection_test_delete_unavailable">Borrado no disponible</string>
|
||||
<string name="neko_plugin">Complemento avanzado</string>
|
||||
<string name="neko_plugin_internal_error">%s error interno</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
||||
@ -477,9 +477,6 @@
|
||||
<string name="please_update">نسخه (%s) برنامه شما بسیار قدیمی شده است و در %s متوقف میشود. لطفاً به نسخه جدیدتر بهروزرسانی کنید</string>
|
||||
<string name="please_update_force">برنامه شما خیلی قدیمی شده است (%s). و در %s کار نمیکند. باید نسخه جدید را دانلود کنید!</string>
|
||||
<string name="connection_test_delete_unavailable">پاککردن غیرقابلدسترسها</string>
|
||||
<string name="neko_plugin">افزونه پیشرفته</string>
|
||||
<string name="neko_plugin_summary">افزونههای پیشرفته میتوانند پروتکلهایی را ارائه دهند که به صورت پیشفرض در این برنامه پشتیبانی نشدهاند.\n\n هر کسی میتواند افزونههای پیشرفته بنویسد که میتوانند NekoBox را کنترل کنند. لطفا از منابع معتبر دانلود و نصب کنید.</string>
|
||||
<string name="neko_plugin_internal_error">مشکل داخلی %s</string>
|
||||
<string name="move">حرکت</string>
|
||||
<string name="exe_prefer_provider">ارائهدهنده برگزیده افزونهها</string>
|
||||
<string name="create_shortcut">ساخت میانبر</string>
|
||||
|
||||
@ -259,10 +259,6 @@
|
||||
Если придётся использовать данную функцию, попробуйте N=2, чтобы проверить, решит ли это проблему. Настоятельно не рекомендуется использовать более 4 соединений.\""</string>
|
||||
<string name="need_reload">Перезагрузите прокси-сервис, чтобы применить изменения</string>
|
||||
<string name="need_restart">Перезапустите приложение для применения изменений</string>
|
||||
<string name="neko_plugin">Дополнительный плагин</string>
|
||||
<string name="neko_plugin_summary">"\"Дополнительные плагины могут предоставлять протоколы, которые изначально не поддерживаются.
|
||||
|
||||
Любой может написать дополнительные плагины, которые могут управлять Nekobox. Загружайте и устанавливайте их из надежных источников.\""</string>
|
||||
<string name="network">Сеть</string>
|
||||
<string name="network_change_reset_connections">Сбрасывать исходящие соединения при изменении сети</string>
|
||||
<string name="night_mode">Ночной режим</string>
|
||||
|
||||
@ -425,16 +425,11 @@
|
||||
<string name="please_update">您的 APP (%s) 太旧了喵,%s 再不更新就没的用了喵~</string>
|
||||
<string name="please_update_force">您的 APP (%s) 版本过旧,已于 %s 停止工作,请立即升级。</string>
|
||||
<string name="connection_test_delete_unavailable">清理不可用配置</string>
|
||||
<string name="neko_plugin">高级插件</string>
|
||||
<string name="neko_plugin_summary">高级插件可以提供原本不支持的协议。\n\n
|
||||
任何人都可以编写高级插件,开启相当于给予其控制 NekoBox 的权限,请从信任的来源下载安装。\n\n
|
||||
普通插件在关于页面显示,无需手动开启。</string>
|
||||
<string name="packet_encoding">包编码</string>
|
||||
<string name="action_switch">切换</string>
|
||||
<string name="acquire_wake_lock">获取唤醒锁</string>
|
||||
<string name="release_wake_lock">释放唤醒锁</string>
|
||||
<string name="acquire_wake_lock_summary">保持 CPU 开启</string>
|
||||
<string name="neko_plugin_internal_error">%s 内部错误</string>
|
||||
<string name="move">移动</string>
|
||||
<string name="subscription_expire">过期: %s</string>
|
||||
<string name="update_all_subscription">更新所有订阅</string>
|
||||
|
||||
@ -509,12 +509,6 @@
|
||||
<string name="please_update_force">Your APP is too old (%s). And has been stopped working at %s.
|
||||
Please update!</string>
|
||||
<string name="connection_test_delete_unavailable">Clear unavailable</string>
|
||||
<string name="neko_plugin">Advanced plugin</string>
|
||||
<string name="neko_plugin_summary">Advanced plugins can provide protocols that are not
|
||||
originally supported.\n\n
|
||||
Anyone can write advanced plugins, which can control NekoBox. please download and install
|
||||
from trusted sources.</string>
|
||||
<string name="neko_plugin_internal_error">%s internal error</string>
|
||||
<string name="move">Move</string>
|
||||
<string name="exe_prefer_provider">Plugin Preferred Provider</string>
|
||||
<string name="create_shortcut">Create Shortcut</string>
|
||||
|
||||
@ -89,10 +89,6 @@
|
||||
app:key="logLevel"
|
||||
app:title="@string/log_level"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<io.nekohasekai.sagernet.widget.AppListPreference
|
||||
app:icon="@drawable/ic_baseline_android_24"
|
||||
app:key="nekoPlugins"
|
||||
app:title="@string/neko_plugin" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/cag_route">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user