Remove advanced plugin

This commit is contained in:
armv9 2025-04-09 13:36:35 +09:00
parent 2c3a6164bb
commit 912a0665a5
30 changed files with 111 additions and 1246 deletions

View File

@ -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" />

View File

@ -147,7 +147,6 @@ object Key {
//
const val NEKO_PLUGIN_MANAGED = "nekoPlugins"
const val APP_TLS_VERSION = "appTLSVersion"
const val ENABLE_CLASH_API = "enableClashAPI"
}

View File

@ -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) {

View File

@ -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)
}
}
}
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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;

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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()
}
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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")
}

View File

@ -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
}
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">