diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 68fd397..d8d3dac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,7 +3,7 @@ plugins { id("com.android.application") id("kotlin-android") - id("kotlin-kapt") + id("com.google.devtools.ksp") id("kotlin-parcelize") } @@ -13,8 +13,8 @@ android { compileOptions { isCoreLibraryDesugaringEnabled = true } - kapt.arguments { - arg("room.incremental", true) + ksp { + arg("room.incremental", "true") arg("room.schemaLocation", "$projectDir/schemas") } bundle { @@ -23,15 +23,19 @@ android { } } buildFeatures { + buildConfig = true viewBinding = true aidl = true } namespace = "io.nekohasekai.sagernet" - packagingOptions { + packaging { jniLibs { useLegacyPackaging = true } } + androidResources { + generateLocaleConfig = true + } } dependencies { @@ -74,11 +78,11 @@ dependencies { exclude(group = "androidx.appcompat") } - implementation("androidx.room:room-runtime:2.5.1") - kapt("androidx.room:room-compiler:2.5.1") - implementation("androidx.room:room-ktx:2.5.1") + implementation("androidx.room:room-runtime:2.6.1") + ksp("androidx.room:room-compiler:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") implementation("com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4") - kapt("com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4") + ksp("com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") } diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt index fa43ee9..7fb409d 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt @@ -8,6 +8,7 @@ import kotlinx.parcelize.Parcelize @Entity(tableName = "rules") @Parcelize +@TypeConverters(StringCollectionConverter::class) data class RuleEntity( @PrimaryKey(autoGenerate = true) var id: Long = 0L, var name: String = "", @@ -21,7 +22,7 @@ data class RuleEntity( var source: String = "", var protocol: String = "", var outbound: Long = 0, - var packages: List = listOf(), + var packages: Set = emptySet(), ) : Parcelable { fun displayName(): String { 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 9f63fbb..a235f2a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt @@ -27,7 +27,7 @@ abstract class SagerDatabase : RoomDatabase() { val instance by lazy { SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs() Room.databaseBuilder(SagerNet.application, SagerDatabase::class.java, Key.DB_PROFILE) - .addMigrations(*SagerDatabase_Migrations.build()) +// .addMigrations(*SagerDatabase_Migrations.build()) .allowMainThreadQueries() .enableMultiInstanceInvalidation() .fallbackToDestructiveMigration() diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/StringCollectionConverter.kt b/app/src/main/java/io/nekohasekai/sagernet/database/StringCollectionConverter.kt new file mode 100644 index 0000000..ea45467 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/database/StringCollectionConverter.kt @@ -0,0 +1,44 @@ +package io.nekohasekai.sagernet.database + +import androidx.room.TypeConverter + +class StringCollectionConverter { + companion object { + const val SPLIT_FLAG = "," + + /* + @TypeConverter + @JvmStatic + fun fromList(list: List): String = if (list.isEmpty()) { + "" + } else { + list.joinToString(SPLIT_FLAG) + } + + @TypeConverter + @JvmStatic + fun toList(str: String): List = if (str.isBlank()) { + emptyList() + } else { + str.split(SPLIT_FLAG) + } + */ + + + @TypeConverter + @JvmStatic + fun fromSet(set: Set): String = if (set.isEmpty()) { + "" + } else { + set.joinToString(SPLIT_FLAG) + } + + @TypeConverter + @JvmStatic + fun toSet(str: String): Set = if (str.isBlank()) { + emptySet() + } else { + str.split(",").toSet() + } + } +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt index 513a7f3..01af7cb 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt @@ -113,9 +113,6 @@ fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Uni }) } -val PackageInfo.signaturesCompat - get() = if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures - /** * Based on: https://stackoverflow.com/a/26348729/2245107 */ diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt index 62c6dd4..82acd67 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt @@ -107,7 +107,7 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) { PackageCache.awaitLoadSync() for ((_, pkg) in PackageCache.installedPluginPackages) { try { - val pluginId = pkg.providers[0].loadString(Plugins.METADATA_KEY_ID) + 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() .icon(R.drawable.ic_baseline_nfc_24) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt index 3c522fa..75cd8bc 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt @@ -139,9 +139,9 @@ class AppListActivity : ThemedActivity() { var filteredApps = apps suspend fun reload() { - apps = getCachedApps().map { (packageName, packageInfo) -> + apps = getCachedApps().mapNotNull { (packageName, packageInfo) -> coroutineContext[Job]!!.ensureActive() - ProxiedApp(packageManager, packageInfo.applicationInfo, packageName) + packageInfo.applicationInfo?.let { ProxiedApp(packageManager, it, packageName) } }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) } @@ -200,8 +200,11 @@ class AppListActivity : ThemedActivity() { private fun initProxiedUids(str: String = DataStore.routePackages) { proxiedUids.clear() val apps = getCachedApps() - for (line in str.lineSequence()) proxiedUids[(apps[line] - ?: continue).applicationInfo.uid] = true + for (line in str.lineSequence()) { + val app = (apps[line] ?: continue) + val uid = app.applicationInfo?.uid ?: continue + proxiedUids[uid] = true + } } private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid] @@ -306,6 +309,7 @@ class AppListActivity : ThemedActivity() { return true } + R.id.action_clear_selections -> { runOnDefaultDispatcher { proxiedUids.clear() @@ -316,6 +320,7 @@ class AppListActivity : ThemedActivity() { } } } + R.id.action_export_clipboard -> { val success = SagerNet.trySetPrimaryClip("false\n${DataStore.routePackages}") Snackbar.make( @@ -325,6 +330,7 @@ class AppListActivity : ThemedActivity() { ).show() return true } + R.id.action_import_clipboard -> { val proxiedAppString = SagerNet.clipboard.primaryClip?.getItemAt(0)?.text?.toString() @@ -344,6 +350,7 @@ class AppListActivity : ThemedActivity() { } Snackbar.make(binding.list, R.string.action_import_err, Snackbar.LENGTH_LONG).show() } + R.id.uninstall_all -> { runOnDefaultDispatcher { proxiedUids.clear() diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt index fdf3a3f..1c11666 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt @@ -103,9 +103,9 @@ class AppManagerActivity : ThemedActivity() { var filteredApps = apps suspend fun reload() { - apps = cachedApps.map { (packageName, packageInfo) -> + apps = cachedApps.mapNotNull { (packageName, packageInfo) -> coroutineContext[Job]!!.ensureActive() - ProxiedApp(packageManager, packageInfo.applicationInfo, packageName) + packageInfo.applicationInfo?.let { ProxiedApp(packageManager, it, packageName) } }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) } @@ -164,8 +164,11 @@ class AppManagerActivity : ThemedActivity() { private fun initProxiedUids(str: String = DataStore.individual) { proxiedUids.clear() val apps = cachedApps - for (line in str.lineSequence()) proxiedUids[(apps[line] - ?: continue).applicationInfo.uid] = true + for (line in str.lineSequence()) { + val app = (apps[line] ?: continue) + val uid = app.applicationInfo?.uid ?: continue + proxiedUids[uid] = true + } } private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid] @@ -325,14 +328,19 @@ class AppManagerActivity : ThemedActivity() { proxiedUids.clear() for (app in cachedApps) { val needProxy = - needProxyAppsList.contains(app.key) || app.value.applicationInfo.uid == 1000 + needProxyAppsList.contains(app.key) || (app.value.applicationInfo?.uid + ?: 0) == 1000 if (needProxy) { if (!bypass) { - proxiedUids[app.value.applicationInfo.uid] = true + app.value.applicationInfo?.apply { + proxiedUids[uid] = true + } } } else { if (bypass) { - proxiedUids[app.value.applicationInfo.uid] = true + app.value.applicationInfo?.apply { + proxiedUids[uid] = true + } } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt index cabb2cb..825d24a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt @@ -49,7 +49,7 @@ class RouteSettingsActivity( fun init(packageName: String?) { RuleEntity().apply { if (!packageName.isNullOrBlank()) { - packages = listOf(packageName) + packages = setOf(packageName) name = app.getString(R.string.route_for, PackageCache.loadLabel(packageName)) } }.init() @@ -89,7 +89,7 @@ class RouteSettingsActivity( 2 -> -2L else -> DataStore.routeOutboundRule } - packages = DataStore.routePackages.split("\n").filter { it.isNotBlank() } + packages = DataStore.routePackages.split("\n").filter { it.isNotBlank() }.toSet() if (DataStore.editingId == 0L) { enabled = true diff --git a/app/src/main/java/moe/matsuri/nb4a/plugin/Plugins.kt b/app/src/main/java/moe/matsuri/nb4a/plugin/Plugins.kt index 6ed37d0..43b80bb 100644 --- a/app/src/main/java/moe/matsuri/nb4a/plugin/Plugins.kt +++ b/app/src/main/java/moe/matsuri/nb4a/plugin/Plugins.kt @@ -22,8 +22,8 @@ object Plugins { const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path" fun isExeOrPlugin(pkg: PackageInfo): Boolean { - if (pkg.providers == null || pkg.providers.isEmpty()) return false - val provider = pkg.providers[0] ?: return false + 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) @@ -92,8 +92,8 @@ object Plugins { PackageCache.awaitLoadSync() val pkgs = PackageCache.installedPluginPackages .map { it.value } - .filter { it.providers[0].loadString(METADATA_KEY_ID) == pluginId } - return pkgs.map { it.providers[0] } + .filter { it.providers?.get(0)?.loadString(METADATA_KEY_ID) == pluginId } + return pkgs.mapNotNull { it.providers?.get(0) } } private fun buildUri(id: String, auth: String) = Uri.Builder() diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000..467b3ef --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US diff --git a/build.gradle.kts b/build.gradle.kts index fde45d6..be2380f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,3 +6,7 @@ allprojects { tasks.register("clean") { delete(rootProject.buildDir) } + +plugins { + id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 90cfc8a..b969dc7 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,5 +8,5 @@ apply(from = "../repositories.gradle.kts") dependencies { // Gradle Plugins implementation("com.android.tools.build:gradle:8.8.1") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21") } diff --git a/buildSrc/src/main/kotlin/Helpers.kt b/buildSrc/src/main/kotlin/Helpers.kt index 14c08dc..134607a 100644 --- a/buildSrc/src/main/kotlin/Helpers.kt +++ b/buildSrc/src/main/kotlin/Helpers.kt @@ -1,9 +1,5 @@ import com.android.build.api.dsl.ApplicationExtension -import com.android.build.api.dsl.LibraryExtension -import com.android.build.api.dsl.Lint import com.android.build.gradle.AbstractAppExtension -import com.android.build.gradle.AppExtension -import com.android.build.gradle.BaseExtension import com.android.build.gradle.internal.api.BaseVariantOutputImpl import org.gradle.api.JavaVersion import org.gradle.api.Project @@ -11,7 +7,9 @@ import org.gradle.api.plugins.ExtensionAware import org.gradle.kotlin.dsl.getByName import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions import java.security.MessageDigest -import java.util.* +import java.util.Base64 +import java.util.Locale +import java.util.Properties import kotlin.system.exitProcess fun sha256Hex(bytes: ByteArray): String { @@ -80,7 +78,7 @@ fun Project.requireTargetAbi(): String { var targetAbi = "" if (gradle.startParameter.taskNames.isNotEmpty()) { if (gradle.startParameter.taskNames.size == 1) { - val targetTask = gradle.startParameter.taskNames[0].toLowerCase(Locale.ROOT).trim() + val targetTask = gradle.startParameter.taskNames[0].lowercase(Locale.ROOT).trim() when { targetTask.contains("arm64") -> targetAbi = "arm64-v8a" targetTask.contains("arm") -> targetAbi = "armeabi-v7a" @@ -94,11 +92,11 @@ fun Project.requireTargetAbi(): String { fun Project.setupCommon() { android.apply { - buildToolsVersion = "30.0.3" - compileSdk = 33 + buildToolsVersion = "35.0.1" + compileSdk = 35 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 35 } buildTypes { getByName("release") { @@ -120,7 +118,7 @@ fun Project.setupCommon() { textOutput = project.file("build/lint.txt") htmlOutput = project.file("build/lint.html") } - packagingOptions { + packaging { resources.excludes.addAll( listOf( "**/*.kotlin_*", @@ -137,9 +135,6 @@ fun Project.setupCommon() { ) ) } - packagingOptions { - jniLibs.useLegacyPackaging = true - } (this as? AbstractAppExtension)?.apply { buildTypes { getByName("release") { diff --git a/gradle.properties b/gradle.properties index a9faaf7..7d843ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,5 @@ android.enableJetifier=true kotlin.code.style=official # Gradle parallel build org.gradle.parallel=true -android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false