diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7fa30b7a2..60a26a03b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,7 +57,7 @@ dependencies { implementation(fileTree("libs")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0") implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.activity:activity-ktx:1.10.1") @@ -108,10 +108,10 @@ dependencies { // Unit tests (pure-JVM, run via :app:testOssDebugUnitTest — no device/libcore needed). testImplementation("junit:junit:4.13.2") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") // Robolectric is available for tests that must touch Android framework classes; // prefer extracting pure functions over using it (see Plan 007). - testImplementation("org.robolectric:robolectric:4.13") + testImplementation("org.robolectric:robolectric:4.16.1") // Instrumented tests (androidTest) — Room migration verification on a real SQLite. // Runs on a device/emulator (see .depot/workflows/android-instrumented.yml), not the JVM. diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/11.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/11.json new file mode 100644 index 000000000..f546f53ab --- /dev/null +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/11.json @@ -0,0 +1,379 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "778d6b1245bb783d1c0cb9597f84c6b7", + "entities": [ + { + "tableName": "proxy_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL, `isSelector` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL, `landingProxy` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ungrouped", + "columnName": "ungrouped", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscription", + "columnName": "subscription", + "affinity": "BLOB" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelector", + "columnName": "isSelector", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "frontProxy", + "columnName": "frontProxy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "landingProxy", + "columnName": "landingProxy", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "proxy_entities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `mieruBean` BLOB, `naiveBean` BLOB, `hysteriaBean` BLOB, `tuicBean` BLOB, `juicityBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `shadowTLSBean` BLOB, `anyTLSBean` BLOB, `chainBean` BLOB, `configBean` BLOB, `snellBean` BLOB, `masterDnsVpnBean` BLOB, `awgBean` BLOB, `olcrtcBean` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tx", + "columnName": "tx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rx", + "columnName": "rx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ping", + "columnName": "ping", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT" + }, + { + "fieldPath": "socksBean", + "columnName": "socksBean", + "affinity": "BLOB" + }, + { + "fieldPath": "httpBean", + "columnName": "httpBean", + "affinity": "BLOB" + }, + { + "fieldPath": "ssBean", + "columnName": "ssBean", + "affinity": "BLOB" + }, + { + "fieldPath": "ssrBean", + "columnName": "ssrBean", + "affinity": "BLOB" + }, + { + "fieldPath": "vmessBean", + "columnName": "vmessBean", + "affinity": "BLOB" + }, + { + "fieldPath": "trojanBean", + "columnName": "trojanBean", + "affinity": "BLOB" + }, + { + "fieldPath": "trojanGoBean", + "columnName": "trojanGoBean", + "affinity": "BLOB" + }, + { + "fieldPath": "mieruBean", + "columnName": "mieruBean", + "affinity": "BLOB" + }, + { + "fieldPath": "naiveBean", + "columnName": "naiveBean", + "affinity": "BLOB" + }, + { + "fieldPath": "hysteriaBean", + "columnName": "hysteriaBean", + "affinity": "BLOB" + }, + { + "fieldPath": "tuicBean", + "columnName": "tuicBean", + "affinity": "BLOB" + }, + { + "fieldPath": "juicityBean", + "columnName": "juicityBean", + "affinity": "BLOB" + }, + { + "fieldPath": "sshBean", + "columnName": "sshBean", + "affinity": "BLOB" + }, + { + "fieldPath": "wgBean", + "columnName": "wgBean", + "affinity": "BLOB" + }, + { + "fieldPath": "shadowTLSBean", + "columnName": "shadowTLSBean", + "affinity": "BLOB" + }, + { + "fieldPath": "anyTLSBean", + "columnName": "anyTLSBean", + "affinity": "BLOB" + }, + { + "fieldPath": "chainBean", + "columnName": "chainBean", + "affinity": "BLOB" + }, + { + "fieldPath": "configBean", + "columnName": "configBean", + "affinity": "BLOB" + }, + { + "fieldPath": "snellBean", + "columnName": "snellBean", + "affinity": "BLOB" + }, + { + "fieldPath": "masterDnsVpnBean", + "columnName": "masterDnsVpnBean", + "affinity": "BLOB" + }, + { + "fieldPath": "awgBean", + "columnName": "awgBean", + "affinity": "BLOB" + }, + { + "fieldPath": "olcrtcBean", + "columnName": "olcrtcBean", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)" + } + ] + }, + { + "tableName": "rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `config` TEXT NOT NULL DEFAULT '', `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `ruleset` TEXT NOT NULL DEFAULT '', `outbound` INTEGER NOT NULL, `packages` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domains", + "columnName": "domains", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ip", + "columnName": "ip", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourcePort", + "columnName": "sourcePort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "network", + "columnName": "network", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ruleset", + "columnName": "ruleset", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "outbound", + "columnName": "outbound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packages", + "columnName": "packages", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '778d6b1245bb783d1c0cb9597f84c6b7')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt index fb1d5cde7..9ae97e8f3 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt @@ -46,7 +46,6 @@ import moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity import moe.matsuri.nb4a.proxy.anytls.toUri import moe.matsuri.nb4a.proxy.config.ConfigBean import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity -import moe.matsuri.nb4a.proxy.neko.* import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity import io.nekohasekai.sagernet.fmt.masterdnsvpn.toUri as toMasterDnsVpnUri @@ -84,7 +83,6 @@ data class ProxyEntity( var shadowTLSBean: ShadowTLSBean? = null, var anyTLSBean: AnyTLSBean? = null, var chainBean: ChainBean? = null, - var nekoBean: NekoBean? = null, var configBean: ConfigBean? = null, var snellBean: SnellBean? = null, var masterDnsVpnBean: MasterDnsVpnBean? = null, @@ -121,6 +119,9 @@ data class ProxyEntity( const val TYPE_OLCRTC = 27 const val TYPE_CONFIG = 998 + + // 999 was the Matsuri "Neko Plugin" protocol (removed). Reserved: do not + // reuse the id; rows with this type are purged by the v11 migration. const val TYPE_NEKO = 999 const val TYPE_CHAIN = 8 @@ -205,7 +206,6 @@ data class ProxyEntity( TYPE_SHADOWTLS -> shadowTLSBean = KryoConverters.shadowTLSDeserialize(byteArray) TYPE_ANYTLS -> anyTLSBean = KryoConverters.anyTLSDeserialize(byteArray) TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray) - TYPE_NEKO -> nekoBean = KryoConverters.nekoDeserialize(byteArray) TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray) TYPE_SNELL -> snellBean = KryoConverters.snellDeserialize(byteArray) TYPE_MASTERDNSVPN -> masterDnsVpnBean = KryoConverters.masterDnsVpnDeserialize(byteArray) @@ -232,7 +232,6 @@ data class ProxyEntity( TYPE_SHADOWTLS -> "ShadowTLS" TYPE_ANYTLS -> "AnyTLS" TYPE_CHAIN -> chainName - TYPE_NEKO -> nekoBean!!.displayType() TYPE_CONFIG -> configBean!!.displayType() TYPE_SNELL -> "Snell" TYPE_MASTERDNSVPN -> "MasterDnsVPN" @@ -263,7 +262,6 @@ data class ProxyEntity( TYPE_SHADOWTLS -> shadowTLSBean TYPE_ANYTLS -> anyTLSBean TYPE_CHAIN -> chainBean - TYPE_NEKO -> nekoBean TYPE_CONFIG -> configBean TYPE_SNELL -> snellBean TYPE_MASTERDNSVPN -> masterDnsVpnBean @@ -286,7 +284,6 @@ data class ProxyEntity( is WireGuardBean -> false is AmneziaWGBean -> false is ShadowTLSBean -> false - is NekoBean -> false is ConfigBean -> false else -> true } @@ -309,7 +306,6 @@ data class ProxyEntity( is SnellBean -> toUri() is MasterDnsVpnBean -> toMasterDnsVpnUri() is OlcrtcBean -> toOlcrtcUri() - is NekoBean -> "" else -> toUniversalLink() } } @@ -363,7 +359,6 @@ data class ProxyEntity( TYPE_MASTERDNSVPN -> true TYPE_OLCRTC -> true TYPE_HYSTERIA -> !hysteriaBean!!.canUseSingBox() - TYPE_NEKO -> true else -> false } } @@ -466,7 +461,6 @@ data class ProxyEntity( anyTLSBean = null chainBean = null configBean = null - nekoBean = null snellBean = null masterDnsVpnBean = null olcrtcBean = null @@ -577,11 +571,6 @@ data class ProxyEntity( chainBean = bean } - is NekoBean -> { - type = TYPE_NEKO - nekoBean = bean - } - is ConfigBean -> { type = TYPE_CONFIG configBean = bean diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt index 245b6760b..fba6585ea 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt @@ -2,9 +2,12 @@ package io.nekohasekai.sagernet.database import androidx.room.AutoMigration import androidx.room.Database +import androidx.room.DeleteColumn import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteDatabase import io.nekohasekai.sagernet.Key import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.fmt.KryoConverters @@ -12,7 +15,7 @@ import io.nekohasekai.sagernet.fmt.gson.GsonConverters @Database( entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class], - version = 10, + version = 11, autoMigrations = [ AutoMigration(from = 3, to = 4), AutoMigration(from = 4, to = 5), @@ -21,11 +24,21 @@ import io.nekohasekai.sagernet.fmt.gson.GsonConverters AutoMigration(from = 7, to = 8), AutoMigration(from = 8, to = 9), AutoMigration(from = 9, to = 10), + AutoMigration(from = 10, to = 11, spec = SagerDatabase.RemoveNekoColumn::class), ], ) @TypeConverters(value = [KryoConverters::class, GsonConverters::class]) abstract class SagerDatabase : RoomDatabase() { + @DeleteColumn(tableName = "proxy_entities", columnName = "nekoBean") + class RemoveNekoColumn : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + // Legacy neko-plugin rows are non-functional placeholders; without the + // bean column they could no longer even render. Purge them. + db.execSQL("DELETE FROM proxy_entities WHERE type = 999") + } + } + companion object { val instance by lazy { SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs() diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java index 40000110b..5fe62cc73 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java @@ -32,7 +32,6 @@ import io.nekohasekai.sagernet.ktx.KryosKt; import io.nekohasekai.sagernet.ktx.Logs; import moe.matsuri.nb4a.proxy.config.ConfigBean; -import moe.matsuri.nb4a.proxy.neko.NekoBean; import moe.matsuri.nb4a.utils.JavaUtil; public class KryoConverters { @@ -220,12 +219,6 @@ public static ChainBean chainDeserialize(byte[] bytes) { return deserialize(new ChainBean(), bytes); } - @TypeConverter - public static NekoBean nekoDeserialize(byte[] bytes) { - if (JavaUtil.isEmpty(bytes)) return null; - return deserialize(new NekoBean(), bytes); - } - @TypeConverter public static SubscriptionBean subscriptionDeserialize(byte[] bytes) { if (JavaUtil.isEmpty(bytes)) return null; diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt index a738d142c..c14015c8f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt @@ -23,7 +23,6 @@ object TypeMap : HashMap() { this["snell"] = ProxyEntity.TYPE_SNELL this["masterdnsvpn"] = ProxyEntity.TYPE_MASTERDNSVPN this["olcrtc"] = ProxyEntity.TYPE_OLCRTC - this["neko"] = ProxyEntity.TYPE_NEKO this["config"] = ProxyEntity.TYPE_CONFIG } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt index 064876c0b..40b3d4b62 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -1831,10 +1831,6 @@ class ConfigurationFragment @JvmOverloads constructor( } } - if (proxyEntity.nekoBean != null) { - popup.menu.removeItem(R.id.action_group_configuration) - } - popup.setOnMenuItemClickListener(this) popup.show() } @@ -2078,12 +2074,6 @@ class ConfigurationFragment @JvmOverloads constructor( doubleColumnMenuButton.isGone = true } - proxyEntity.nekoBean?.apply { - if (!isDoubleColumn) { - shareLayout.isGone = true - } - } - runOnDefaultDispatcher { val selected = (selectedItem?.id ?: DataStore.selectedProxy) == proxyEntity.id val started = diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/MessageStore.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/MessageStore.kt index 0a552a215..5acbb4859 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/MessageStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/MessageStore.kt @@ -40,4 +40,4 @@ object MessageStore { fun showMessage(activity: Activity, @StringRes resId: Int, vararg formatArgs: Any) { showMessage(activity.getString(resId, *formatArgs)) } -} +} diff --git a/app/src/main/java/moe/matsuri/nb4a/Protocols.kt b/app/src/main/java/moe/matsuri/nb4a/Protocols.kt index fda7c2e26..873a15ea5 100644 --- a/app/src/main/java/moe/matsuri/nb4a/Protocols.kt +++ b/app/src/main/java/moe/matsuri/nb4a/Protocols.kt @@ -2,7 +2,6 @@ package moe.matsuri.nb4a import android.content.Context import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.database.ProxyEntity.Companion.TYPE_NEKO import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.ktx.app import io.nekohasekai.sagernet.ktx.getColorAttr @@ -42,10 +41,7 @@ object Protocols { // Display fun Context.getProtocolColor(type: Int): Int { - return when (type) { - TYPE_NEKO -> getColorAttr(android.R.attr.textColorPrimary) - else -> getColorAttr(R.attr.protocolColor) - } + return getColorAttr(R.attr.protocolColor) } // Test diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoBean.java b/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoBean.java deleted file mode 100644 index 434d54923..000000000 --- a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoBean.java +++ /dev/null @@ -1,95 +0,0 @@ -package moe.matsuri.nb4a.proxy.neko; - -import androidx.annotation.NonNull; - -import com.esotericsoftware.kryo.io.ByteBufferInput; -import com.esotericsoftware.kryo.io.ByteBufferOutput; - -import org.jetbrains.annotations.NotNull; -import org.json.JSONObject; - -import io.nekohasekai.sagernet.fmt.AbstractBean; -import io.nekohasekai.sagernet.fmt.KryoConverters; -import io.nekohasekai.sagernet.ktx.Logs; - -public class NekoBean extends AbstractBean { - - public String plgId; - public String protocolId; - public JSONObject sharedStorage = new JSONObject(); - - @Override - public void initializeDefaultValues() { - super.initializeDefaultValues(); - if (protocolId == null) protocolId = ""; - if (plgId == null) plgId = "moe.matsuri.plugin.donotexist"; - } - - @Override - public void serialize(ByteBufferOutput output) { - output.writeInt(0); - super.serialize(output); - output.writeString(plgId); - output.writeString(protocolId); - output.writeString(sharedStorage.toString()); - } - - @Override - public void deserialize(ByteBufferInput input) { - int version = input.readInt(); - super.deserialize(input); - plgId = input.readString(); - protocolId = input.readString(); - sharedStorage = tryParseJSON(input.readString()); - } - - @NotNull - public static JSONObject tryParseJSON(String input) { - JSONObject ret; - try { - ret = new JSONObject(input); - } catch (Exception e) { - ret = new JSONObject(); - Logs.INSTANCE.e(e); - } - return ret; - } - - public String displayType() { - return "invalid"; - } - - @Override - public boolean canMapping() { - return false; - } - - @Override - public boolean canICMPing() { - return false; - } - - @Override - public boolean canTCPing() { - return false; - } - - @NotNull - @Override - public NekoBean clone() { - return KryoConverters.deserialize(new NekoBean(), KryoConverters.serialize(this)); - } - - public static final Creator CREATOR = new CREATOR() { - @NonNull - @Override - public NekoBean newInstance() { - return new NekoBean(); - } - - @Override - public NekoBean[] newArray(int size) { - return new NekoBean[size]; - } - }; -} \ No newline at end of file diff --git a/app/src/test/java/io/nekohasekai/sagernet/fmt/HysteriaFmtTest.kt b/app/src/test/java/io/nekohasekai/sagernet/fmt/HysteriaFmtTest.kt index e0e41271d..743af1eb2 100644 --- a/app/src/test/java/io/nekohasekai/sagernet/fmt/HysteriaFmtTest.kt +++ b/app/src/test/java/io/nekohasekai/sagernet/fmt/HysteriaFmtTest.kt @@ -11,11 +11,11 @@ import org.robolectric.annotation.Config /** * Tests for the Hysteria 2 JSON config import (Plan 022). Uses Robolectric because org.json - * is an Android-framework class (stubbed on the bare JVM). Pinned to a Robolectric-supported - * SDK level (the project's compileSdk 36 may not be bundled in Robolectric yet). + * is an Android-framework class (stubbed on the bare JVM). Pinned to the app's targetSdk, + * which Robolectric 4.16 bundles (4.16 supports SDK 36 / Baklava). */ @RunWith(RobolectricTestRunner::class) -@Config(sdk = [33], application = android.app.Application::class) +@Config(sdk = [35], application = android.app.Application::class) class HysteriaFmtTest { @Test diff --git a/app/src/test/java/io/nekohasekai/sagernet/ui/BackupFormatV2Test.kt b/app/src/test/java/io/nekohasekai/sagernet/ui/BackupFormatV2Test.kt index d7c1c24e1..c1ca0cadc 100644 --- a/app/src/test/java/io/nekohasekai/sagernet/ui/BackupFormatV2Test.kt +++ b/app/src/test/java/io/nekohasekai/sagernet/ui/BackupFormatV2Test.kt @@ -16,7 +16,7 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) -@Config(sdk = [33], application = android.app.Application::class) +@Config(sdk = [35], application = android.app.Application::class) class BackupFormatV2Test { @Test diff --git a/build.gradle.kts b/build.gradle.kts index 5e65585bf..4c034aafe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("com.google.devtools.ksp") version "2.3.9" apply false - id("com.diffplug.spotless") version "6.25.0" + id("com.diffplug.spotless") version "7.0.4" } allprojects { diff --git a/scripts/hooks/pre-push b/scripts/hooks/pre-push new file mode 100755 index 000000000..7cd4a8b0f --- /dev/null +++ b/scripts/hooks/pre-push @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Sample pre-push hook. NOT auto-installed (git hooks live in .git/hooks, which is +# not version-controlled). To use it: +# ln -s ../../scripts/hooks/pre-push .git/hooks/pre-push +# It runs the local verification gate and blocks the push on failure. +set -euo pipefail +exec "$(git rev-parse --show-toplevel)/scripts/verify.sh" diff --git a/scripts/verify.sh b/scripts/verify.sh new file mode 100755 index 000000000..f7f1d8e96 --- /dev/null +++ b/scripts/verify.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# One-command local verification gate. Runs ONLY the JVM-safe checks allowed for +# local runs (format, compile, unit tests). Never assembles an APK (that is +# Namespace/Depot only). Always stops the Gradle daemon at the end. +# +# Optional pre-push hook install (manual, not auto-wired): +# ln -s ../../scripts/hooks/pre-push .git/hooks/pre-push +set -euo pipefail + +cd "$(dirname "$0")/.." + +cleanup() { ./gradlew --stop >/dev/null 2>&1 || true; } +trap cleanup EXIT + +echo ">> spotlessKotlinCheck" +./gradlew spotlessKotlinCheck + +echo ">> compileOssDebugKotlin" +./gradlew :app:compileOssDebugKotlin + +echo ">> testOssDebugUnitTest" +./gradlew :app:testOssDebugUnitTest + +echo "OK: local verification passed."