diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 535a0f2e7..4e7c6e7b3 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -96,7 +96,7 @@ jobs: - name: Setup ninja uses: seanmiddleditch/gha-setup-ninja@v6 with: - version: 1.12.1 + version: 1.13.2 - name: Setup ccache uses: actions/cache@v6 diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 8c67d05ed..ccb4e06fe 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -18,10 +18,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@main + uses: actions/checkout@v7 - name: crowdin action - uses: crowdin/github-action@master + uses: crowdin/github-action@v2 with: upload_translations: true download_translations: false diff --git a/.gitignore b/.gitignore index 0022083f0..eb6a6826c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .cache *.iml .gradle +# Where the Kotlin plugin keeps its per-build session marker; a build leaves one behind. +.kotlin /local.properties /.idea .DS_Store diff --git a/build.gradle.kts b/build.gradle.kts index ccb7fe3c9..f8c9c30f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -68,12 +68,12 @@ abstract class GitLatestTagValueSource : ValueSource @@ -139,8 +141,8 @@ abstract class GitCommitHashValueSource : ValueSource whenever __SSE2__ is defined, + // and that header's static inline intrinsics arrive twice on the x86 ABIs: + // dex_builder.ixx and dex_helper.ixx each include phmap in their global + // module fragment, so importing dex_builder gives clang two definitions + // with the same mangled name. clang 21 (NDK r29) rejects that outright, + // where clang 20 merged them. Turning phmap's SSE2 group scan off costs + // nothing on arm, which never had it, and is set for every native module + // rather than for dex_builder alone because phmap's layout depends on the + // flag -- our own hook_bridge.cpp instantiates the same templates, and two + // group sizes in one .so would be an ODR violation the linker cannot see. + "-DPHMAP_HAVE_SSE2=0", + // phmap refuses to configure with SSSE3 but no SSE2, so both go together. + "-DPHMAP_HAVE_SSSE3=0", ) val args = @@ -302,6 +346,33 @@ subprojects { targetCompatibility = androidTargetCompatibility } } + + // Java that is not ours to modernise, and that javac otherwise comments on twice per build -- + // once per build type, in both the debug and the release halves of `zipAll`. + // + // `:external:apache` and `:external:axml` compile vendored upstream sources (commons-lang and + // ManifestEditor); `:services:*` compile the libxposed submodule and the AIDL stubs AGP + // generates, neither of which we write. `:legacy` is the de.robv API surface itself: XResources + // exists precisely to override `getColor`, `getDrawable` and the rest of the deprecated + // Resources methods, so every one of those overrides is deliberate and none can be dropped + // while the legacy API is supported. + // + // `-nowarn` alone is not enough. The "Note: Some input files use or override a deprecated API" + // line is a *mandatory* warning summary, emitted precisely because the lint is off, and only + // `-XDsuppressNotes` silences it. + val vendoredJava = + setOf( + ":external:apache", + ":external:axml", + ":legacy", + ":services:daemon-service", + ":services:manager-service", + ) + if (path in vendoredJava) { + tasks.withType(JavaCompile::class.java).configureEach { + options.compilerArgs.addAll(listOf("-nowarn", "-XDsuppressNotes")) + } + } } tasks.register("format") { diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index da92d3879..499e80978 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -11,12 +11,15 @@ import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction -val defaultManagerPackageName: String by rootProject.extra -val injectedPackageName: String by rootProject.extra -val injectedPackageUid: Int by rootProject.extra -val versionCodeProvider: Provider by rootProject.extra -val versionNameProvider: Provider by rootProject.extra -val versionHashProvider: Provider by rootProject.extra +val defaultManagerPackageName = rootProject.extra["defaultManagerPackageName"] as String +val injectedPackageName = rootProject.extra["injectedPackageName"] as String +val injectedPackageUid = rootProject.extra["injectedPackageUid"] as Int +@Suppress("UNCHECKED_CAST") +val versionCodeProvider = rootProject.extra["versionCodeProvider"] as Provider +@Suppress("UNCHECKED_CAST") +val versionNameProvider = rootProject.extra["versionNameProvider"] as Provider +@Suppress("UNCHECKED_CAST") +val versionHashProvider = rootProject.extra["versionHashProvider"] as Provider plugins { alias(libs.plugins.agp.app) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt index 0809b00b6..8786ab82e 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt @@ -357,7 +357,7 @@ class DatabaseCommand { if (!force) { print( "Are you sure you want to RESET the database? All modules and scopes will be lost. (y/N): ") - val input = readLine() + val input = readlnOrNull() if (input?.lowercase() != "y") { println("Operation cancelled.") return 0 diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt index 39ffcbde8..86e4ee744 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt @@ -10,7 +10,6 @@ import android.os.Binder import android.os.Build import android.os.Bundle import android.os.IBinder -import android.provider.Telephony import android.telephony.TelephonyManager import android.util.Log import hidden.HiddenApiBridge @@ -33,10 +32,19 @@ private const val TAG = "VectorService" object VectorService : IDaemonService.Stub() { private var bootCompleted = false - @Suppress("DEPRECATION") + + /** + * What the dialer broadcasts when a secret code is entered, which changed name in Q. + * + * The pre-Q action is spelled out rather than taken from `Telephony.Sms.Intents`, whose constant + * only became public API in 28 while this daemon runs from 27. It is the same string either way: + * on 8.1 the platform broadcast it from the hidden `TelephonyIntents.SECRET_CODE_ACTION`, which + * carries exactly this value, and the public constant that followed was deprecated in 29 when + * `TelephonyManager.ACTION_SECRET_CODE` replaced it. + */ private val ACTION_SECRET_CODE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) TelephonyManager.ACTION_SECRET_CODE - else Telephony.Sms.Intents.SECRET_CODE_ACTION + else "android.provider.Telephony.SECRET_CODE" /** Dial *#*#832867#*#* ("VECTOR" on the keypad) to open the manager. */ private const val SECRET_CODE = "832867" diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt index 005a87e58..d8a16e600 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt @@ -51,49 +51,75 @@ object Dex2OatServer { private external fun getSockPath(): String - private val selinuxObserver = - object : - FileObserver( - listOf(File("/sys/fs/selinux/enforce"), File("/sys/fs/selinux/policy")), - CLOSE_WRITE) { - override fun onEvent(event: Int, path: String?) { - synchronized(this) { - if (compatibility == DEX2OAT_CRASHED) { - stopWatching() - return + /** The nodes whose writes mean the SELinux state this daemon depends on may have moved. */ + private val SELINUX_NODES = listOf("/sys/fs/selinux/enforce", "/sys/fs/selinux/policy") + + /** + * Watches [SELINUX_NODES] on every release this daemon runs on. + * + * `FileObserver(List, int)` is API 29 and the minimum here is 27, where the only + * constructors are the single-path ones -- deprecated in 29 precisely because they were replaced + * by these. So below 29 this is one observer per node, and `stopWatching` has to reach all of + * them, which is why the two live behind an object rather than being a `FileObserver` itself. + */ + private object SelinuxObserver { + private val observers: List = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + listOf( + object : FileObserver(SELINUX_NODES.map(::File), FileObserver.CLOSE_WRITE) { + override fun onEvent(event: Int, path: String?) = onSelinuxEvent() + }) + } else { + SELINUX_NODES.map { node -> + @Suppress("DEPRECATION") + object : FileObserver(node, FileObserver.CLOSE_WRITE) { + override fun onEvent(event: Int, path: String?) = onSelinuxEvent() } + } + } - val enforcing = - runCatching { - Files.newInputStream(Paths.get("/sys/fs/selinux/enforce")).use { - it.read() == '1'.code - } - } - .getOrDefault(false) - - when { - !enforcing -> { - if (compatibility == DEX2OAT_OK) doMount(false) - compatibility = DEX2OAT_SELINUX_PERMISSIVE - } - hasSePolicyErrors() -> { - if (compatibility == DEX2OAT_OK) doMount(false) - compatibility = DEX2OAT_SEPOLICY_INCORRECT - } - compatibility != DEX2OAT_OK -> { - doMount(true) - if (notMounted()) { - doMount(false) - compatibility = DEX2OAT_MOUNT_FAILED - stopWatching() - } else { - compatibility = DEX2OAT_OK + fun startWatching() = observers.forEach(FileObserver::startWatching) + + fun stopWatching() = observers.forEach(FileObserver::stopWatching) + } + + private fun onSelinuxEvent() { + synchronized(this) { + if (compatibility == DEX2OAT_CRASHED) { + SelinuxObserver.stopWatching() + return + } + + val enforcing = + runCatching { + Files.newInputStream(Paths.get("/sys/fs/selinux/enforce")).use { + it.read() == '1'.code } } - } + .getOrDefault(false) + + when { + !enforcing -> { + if (compatibility == DEX2OAT_OK) doMount(false) + compatibility = DEX2OAT_SELINUX_PERMISSIVE + } + hasSePolicyErrors() -> { + if (compatibility == DEX2OAT_OK) doMount(false) + compatibility = DEX2OAT_SEPOLICY_INCORRECT + } + compatibility != DEX2OAT_OK -> { + doMount(true) + if (notMounted()) { + doMount(false) + compatibility = DEX2OAT_MOUNT_FAILED + SelinuxObserver.stopWatching() + } else { + compatibility = DEX2OAT_OK } } } + } + } init { // Android 10 vs 11+ path differences @@ -193,8 +219,8 @@ object Dex2OatServer { } } - selinuxObserver.startWatching() - selinuxObserver.onEvent(0, null) + SelinuxObserver.startWatching() + onSelinuxEvent() // Run the socket accept loop in an IO coroutine VectorDaemon.scope.launch { runSocketLoop() } @@ -221,7 +247,11 @@ object Dex2OatServer { SELinux.setFileContext(HOOKER64, xposedFile) runCatching { - LocalServerSocket(sockPath).use { server -> + // Closed by hand rather than with `use`: LocalServerSocket only implements Closeable + // from API 28, and this daemon runs from 27. The client socket below has implemented it + // since 17, so that one keeps `use`. + val server = LocalServerSocket(sockPath) + try { setSockCreateContext(null) while (true) { // This blocks until the C++ wrapper connects @@ -235,6 +265,8 @@ object Dex2OatServer { } } } + } finally { + server.close() } } .onFailure { diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 184ce4f25..49ad0d3f4 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -23,5 +23,9 @@ option(FMT_INSTALL OFF) add_subdirectory(dobby) add_subdirectory(fmt) add_subdirectory(lsplant/lsplant/src/main/jni) -target_compile_options(lsplant_static PUBLIC -Wno-gnu-anonymous-struct) +target_compile_options(lsplant_static PUBLIC -Wno-gnu-anonymous-struct + # lsplant.cc's `operator""_uarr()` is a string literal operator template, which is a GNU + # extension clang warns about under -Wpedantic. Upstream code we do not own, and the + # warning was printed once per ABI on every build. + -Wno-gnu-string-literal-operator-template) target_compile_definitions(fmt-header-only INTERFACE FMT_USE_LOCALE=0 FMT_USE_FLOAT=0 FMT_USE_DOUBLE=0 FMT_USE_LONG_DOUBLE=0 FMT_USE_BITINT=0) diff --git a/external/apache/build.gradle.kts b/external/apache/build.gradle.kts index 2127d2f0e..2c6dd431f 100644 --- a/external/apache/build.gradle.kts +++ b/external/apache/build.gradle.kts @@ -1,5 +1,5 @@ -val androidSourceCompatibility: JavaVersion by rootProject.extra -val androidTargetCompatibility: JavaVersion by rootProject.extra +val androidSourceCompatibility = rootProject.extra["androidSourceCompatibility"] as JavaVersion +val androidTargetCompatibility = rootProject.extra["androidTargetCompatibility"] as JavaVersion plugins { id("java-library") diff --git a/external/apache/commons-lang b/external/apache/commons-lang index fd12207b0..8f8f3b26e 160000 --- a/external/apache/commons-lang +++ b/external/apache/commons-lang @@ -1 +1 @@ -Subproject commit fd12207b0bf02b39098b413ee618a748fb3dd450 +Subproject commit 8f8f3b26e8cb81e0879fc676068db1212652dcaf diff --git a/external/axml/build.gradle.kts b/external/axml/build.gradle.kts index a650b878f..513ebc443 100644 --- a/external/axml/build.gradle.kts +++ b/external/axml/build.gradle.kts @@ -1,5 +1,5 @@ -val androidSourceCompatibility: JavaVersion by rootProject.extra -val androidTargetCompatibility: JavaVersion by rootProject.extra +val androidSourceCompatibility = rootProject.extra["androidSourceCompatibility"] as JavaVersion +val androidTargetCompatibility = rootProject.extra["androidTargetCompatibility"] as JavaVersion plugins { id("java-library") diff --git a/external/fmt b/external/fmt index cdb8dc76d..e2243a6a7 160000 --- a/external/fmt +++ b/external/fmt @@ -1 +1 @@ -Subproject commit cdb8dc76d936a12aacc20b6d283d7c24ee4307fe +Subproject commit e2243a6a7f4a8c09e9233a253f3d1f71e45d8cef diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 83ccdc3d9..df596a88a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coil = "3.5.0" # Material 3 Expressive is not in any stable material3 release; the expressive # APIs graduate through the 1.5.0 alpha line. Pinned explicitly OVER the Compose # BOM, which resolves material3 to 1.4.0. -m3 = "1.5.0-alpha24" -nav3 = "1.1.4" +m3 = "1.5.0-alpha25" +nav3 = "1.1.5" navigationevent = "1.1.2" # Governs every androidx.compose.* artifact below; none of them pin a version. compose-bom = "2026.06.01" @@ -22,14 +22,13 @@ kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } -lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.4" } [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.19.0" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version = "1.13.0" } -androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.0.1" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.2.0" } # Needed to make the in-app browser honour the app's own light/dark choice. androidx-webkit = { group = "androidx.webkit", name = "webkit", version = "1.16.0" } diff --git a/hiddenapi/stubs/build.gradle.kts b/hiddenapi/stubs/build.gradle.kts index 92fcaf36d..0c64b0bd6 100644 --- a/hiddenapi/stubs/build.gradle.kts +++ b/hiddenapi/stubs/build.gradle.kts @@ -1,6 +1,4 @@ +// No source/target override: the root build sets both to 21 for every Java project, and this one +// is `compileOnly` everywhere it is used, so nothing here ever reaches a device. Pinning it to 8 +// only earned three "source value 8 is obsolete" warnings on every build. plugins { `java-library` } - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index edde5911a..b42d4e705 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -8,7 +8,6 @@ plugins { alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ktfmt) - alias(libs.plugins.lsplugin.apksign) } ktfmt { kotlinLangStyle() } @@ -16,7 +15,7 @@ ktfmt { kotlinLangStyle() } kotlin { compilerOptions { // Material 3 Expressive has not landed in a stable material3 release; the - // expressive surface is gated behind these annotations even in 1.5.0-alpha24. + // expressive surface is gated behind these annotations even in 1.5.0-alpha25. // Opting in once here beats sprinkling @OptIn through every screen. optIn.addAll( "androidx.compose.material3.ExperimentalMaterial3Api", @@ -30,16 +29,29 @@ kotlin { // The daemon compiles this module's signing certificate into SignInfo.kt and verifies // the manager.apk it serves against it at runtime, so :manager must be signed with the // same key as the rest of the module or InstallerVerifier rejects it. -apksign { - storeFileProperty = "androidStoreFile" - storePasswordProperty = "androidStorePassword" - keyAliasProperty = "androidKeyAlias" - keyPasswordProperty = "androidKeyPassword" +// +// This is what org.lsposed.lsplugin.apksign did, written out: the same four Gradle properties, a +// store file resolved against the root project, the same signing config on every build type, and +// the same fall back to the debug key when there is no keystore -- which is every build that is +// not CI's, since the workflow appends these properties to gradle.properties itself. The plugin +// read them through Project.getProperties, deprecated in Gradle 9 and gone in Gradle 10, and 1.4 +// is its last release, so the deprecation warning it printed four times per build had no version +// to upgrade to. +val keystore = providers.gradleProperty("androidStoreFile").map { rootProject.file(it) }.orNull +val signed = keystore?.exists() == true + +if (!signed) { + // `info` rather than a print: a local build has no keystore by design and says so on every + // single run, which is noise in the one place a real warning has to be noticed. + logger.info( + "No keystore at ${keystore?.absolutePath ?: "androidStoreFile"}; signing with debug" + ) } -val defaultManagerPackageName: String by rootProject.extra -val injectedPackageName: String by rootProject.extra -val versionHashProvider: Provider by rootProject.extra +val defaultManagerPackageName = rootProject.extra["defaultManagerPackageName"] as String +val injectedPackageName = rootProject.extra["injectedPackageName"] as String +@Suppress("UNCHECKED_CAST") +val versionHashProvider = rootProject.extra["versionHashProvider"] as Provider android { namespace = defaultManagerPackageName @@ -85,7 +97,7 @@ android { // ic_launcher.xml references @drawable/ic_statue_monochrome, which lives in the // daemon's resources. Any name collision between the two resource sets becomes a // build error, so keep additions on the daemon side namespaced. - sourceSets { getByName("main") { res.srcDir("../daemon/src/main/res") } } + sourceSets { getByName("main") { res.directories.add("../daemon/src/main/res") } } packaging { resources { @@ -102,12 +114,26 @@ android { dependenciesInfo.includeInApk = false + if (signed) { + signingConfigs.create("apksign") { + storeFile = keystore + storePassword = providers.gradleProperty("androidStorePassword").orNull + keyAlias = providers.gradleProperty("androidKeyAlias").orNull + keyPassword = providers.gradleProperty("androidKeyPassword").orNull + } + } + buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles("proguard-rules.pro") } + // Every build type, not just release: the daemon reads this config back off whichever + // variant it is building to embed the certificate, so debug has to carry one too. + configureEach { + signingConfig = signingConfigs.getByName(if (signed) "apksign" else "debug") + } } } diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt index a0281c434..211671e9e 100644 --- a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt +++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt @@ -149,9 +149,8 @@ private fun ScenarioList(onPick: (DemoScenario) -> Unit) { items(DEMO_SCENARIOS, key = { it.id }) { scenario -> ListItem( modifier = Modifier.clickable { onPick(scenario) }, - headlineContent = { Text(scenario.title) }, supportingContent = { Text(scenario.summary) }, - ) + ) { Text(scenario.title) } } } } diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt index a3c79f533..193249da0 100644 --- a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt +++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt @@ -3,12 +3,14 @@ package org.matrix.vector.manager.demo import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.ResolveInfo +import android.os.Build import android.os.ParcelFileDescriptor import org.lsposed.lspd.IFrameworkInstallCallback import org.lsposed.lspd.ILSPManagerService import org.lsposed.lspd.models.Application import org.lsposed.lspd.models.UserInfo import rikka.parcelablelist.ParcelableListSlice +import org.matrix.vector.manager.data.model.versionCodeCompat /** * The daemon, as a script. @@ -162,8 +164,8 @@ class FakeManagerService( // daemon's own objects. val rewritten = actual.list.map { info -> - val baseline = baselineVersions.putIfAbsent(info.packageName, info.longVersionCode) - if (baseline != null && baseline != info.longVersionCode) { + val baseline = baselineVersions.putIfAbsent(info.packageName, info.versionCodeCompat) + if (baseline != null && baseline != info.versionCodeCompat) { // This one has genuinely changed under us since the scenario started, which // for a demo means the manager just installed it. Reporting the truth from // here is what makes this a test rather than a picture: the row has to stop @@ -173,7 +175,7 @@ class FakeManagerService( return@map info } info.also { - it.longVersionCode = 1 + it.setVersionCodeCompat(1) it.versionName = "0.1-demo" } } @@ -295,3 +297,16 @@ class FakeManagerService( override fun setIncludeNewApps(packageName: String?, enable: Boolean): Boolean = real?.setIncludeNewApps(packageName, enable) ?: false } + +/** + * The write half of [versionCodeCompat], which exists only here. + * + * `setLongVersionCode` is API 28 and the app's minimum is 27, so below that the deprecated `int` + * field is the field. Nothing in the real manager ever writes a version code -- only this fake, + * which rewrites the daemon's answers to script the demo. + */ +@Suppress("DEPRECATION") +private fun PackageInfo.setVersionCodeCompat(value: Long) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) longVersionCode = value + else versionCode = value.toInt() +} diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetection.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetection.kt index 23e0df5b0..d7af6d3c4 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetection.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetection.kt @@ -170,7 +170,11 @@ object ModuleDetection { * string-resource id. */ private fun legacyDescription(info: ApplicationInfo, packageManager: PackageManager): String { - val raw = info.metaData?.get(LEGACY_DESCRIPTION) ?: return "" + // `Bundle.get` is deprecated in favour of the type-specific getters, which is exactly what + // this read cannot use: the value's type is the module author's choice and is not known + // until it has been read. Asking for the wrong one costs a logged warning and a stack trace + // out of Bundle for every module that picked the other kind, so the untyped read stays. + @Suppress("DEPRECATION") val raw = info.metaData?.get(LEGACY_DESCRIPTION) ?: return "" return when (raw) { is String -> raw.trim() is Int -> diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/PackageVersion.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/PackageVersion.kt new file mode 100644 index 000000000..d4da762c5 --- /dev/null +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/PackageVersion.kt @@ -0,0 +1,22 @@ +package org.matrix.vector.manager.data.model + +import android.content.pm.PackageInfo +import android.os.Build + +/** + * A package's version code, on every release this app runs on. + * + * `PackageInfo.getLongVersionCode` arrived in API 28 and the `int` field it replaces was deprecated + * in the same release, so on a supported device one of the two is always the wrong one to call. The + * minimum here is API 27: reading the long unguarded is a `NoSuchMethodError` on Android 8.1, not a + * lint opinion, and it would take out the app list, the module list and the store's installed-check + * — every screen that names a version. + * + * Reading the deprecated field is not a loss below 28. The high half it drops, `versionCodeMajor`, + * cannot be set by a package installed on a release that has no concept of it. + */ +val PackageInfo.versionCodeCompat: Long + @Suppress("DEPRECATION") + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) longVersionCode + else versionCode.toLong() diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt index 270a7ac1f..134db4a13 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.matrix.vector.manager.data.model.AppInfo import org.matrix.vector.manager.data.model.ModuleDetectionCache +import org.matrix.vector.manager.data.model.versionCodeCompat import org.matrix.vector.manager.ipc.DaemonClient import org.matrix.vector.manager.logW @@ -54,6 +55,9 @@ class AppRepository( packages.mapNotNull { pkg -> val appInfo = pkg.applicationInfo ?: return@mapNotNull null val isSystem = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0 + // FLAG_IS_GAME was replaced by the category in API 26 and is deprecated, but an + // app built before that still ships it and sets no category, so both are read. + @Suppress("DEPRECATION") val isGame = appInfo.category == ApplicationInfo.CATEGORY_GAME || (appInfo.flags and ApplicationInfo.FLAG_IS_GAME) != 0 @@ -70,7 +74,7 @@ class AppRepository( isRecommended = false, lastUpdateTime = pkg.lastUpdateTime, firstInstallTime = pkg.firstInstallTime, - versionCode = pkg.longVersionCode, + versionCode = pkg.versionCodeCompat, applicationInfo = appInfo, ) } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/RepoRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/RepoRepository.kt index 158380f85..b02d00781 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/RepoRepository.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/RepoRepository.kt @@ -1,7 +1,5 @@ package org.matrix.vector.manager.data.repository -import android.content.pm.PackageInfo -import android.os.Build import com.google.gson.Gson import com.google.gson.JsonParser import com.google.gson.stream.JsonReader @@ -21,6 +19,7 @@ import okhttp3.Response import org.matrix.vector.manager.data.model.OnlineModule import org.matrix.vector.manager.data.model.RepoVersion import org.matrix.vector.manager.data.model.StoreCatalog +import org.matrix.vector.manager.data.model.versionCodeCompat import org.matrix.vector.manager.di.ServiceLocator import org.matrix.vector.manager.ipc.DaemonClient import org.matrix.vector.manager.logI @@ -181,7 +180,7 @@ class RepoRepository( .getOrNull() ?: return val versions = HashMap(packages.size) for (info in packages) { - val version = RepoVersion(info.longVersionCodeCompat(), info.versionName.orEmpty()) + val version = RepoVersion(info.versionCodeCompat, info.versionName.orEmpty()) // The daemon reports every user, so the same package arrives more than once. The // highest version wins, because that is the one an update would have to beat. val known = versions[info.packageName] @@ -291,11 +290,6 @@ class RepoRepository( .distinctBy { it.name } .toList() - @Suppress("DEPRECATION") - private fun PackageInfo.longVersionCodeCompat(): Long = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) longVersionCode - else versionCode.toLong() - private companion object { /** * The only host serving the full list. Probed rather than assumed: roughly 1.2 MB and 800 diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ipc/PackageBroadcasts.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ipc/PackageBroadcasts.kt index d67af3681..2ed5ab280 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ipc/PackageBroadcasts.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ipc/PackageBroadcasts.kt @@ -143,14 +143,14 @@ fun Context.daemonPackageEventsFlow(): Flow = callbackFlow { * * Not one of these extras can be read with the accessor its name implies, which is worth knowing * before someone corrects it: the package arrives as a single `String` under - * [Intent.EXTRA_PACKAGES], whose documented type is `String[]`, so `getStringArrayExtra` answers - * null, and the user arrives as a plain `Int` under [Intent.EXTRA_USER], whose documented type is + * [EXTRA_PACKAGES], whose documented type is `String[]`, so `getStringArrayExtra` answers null, and + * the user arrives as a plain `Int` under [Intent.EXTRA_USER], whose documented type is * `UserHandle`, so `getParcelableExtra` answers null and the id quietly becomes 0 — the one user * this whole flow exists to see past. What the platform sent is wrapped whole under * [Intent.EXTRA_INTENT], and only its action says what happened. */ private fun Intent.daemonPackageEvent(): PackageEvent? { - val packageName = getStringExtra(Intent.EXTRA_PACKAGES) ?: return null + val packageName = getStringExtra(EXTRA_PACKAGES) ?: return null val userId = getIntExtra(Intent.EXTRA_USER, 0) val wrapped = IntentCompat.getParcelableExtra(this, Intent.EXTRA_INTENT, Intent::class.java) @@ -188,3 +188,12 @@ private const val ACTION_DAEMON_NOTIFICATION = "${BuildConfig.MANAGER_PACKAGE_NA * plain int under `EXTRA_USER`, so [daemonPackageEvent] reads that key and not this one. */ private const val EXTRA_USER_HANDLE = "android.intent.extra.user_handle" + +/** + * `Intent.EXTRA_PACKAGES`, which is only public API from 34. + * + * This app runs from 27, where the constant does not exist to be referenced — and there is nothing + * to reference it for: the daemon writes this key as a literal of its own, so the two sides agree + * on the string and never on the symbol. + */ +private const val EXTRA_PACKAGES = "android.intent.extra.PACKAGES" diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt index 92404aa56..d1e1f8365 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt @@ -30,7 +30,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -176,12 +177,11 @@ fun PackageActionSheet( } val colors = MaterialTheme.colorScheme - // Left at the default, without skipPartiallyExpanded: that flag removes the half-height stop, - // which is the only thing a drag on a sheet can *do* other than dismiss it, so a sheet taller - // than half the screen would open at full height and could not be made smaller. Material adds - // the stop only when the content is actually taller than half the screen, so short sheets - // still open at their own height and nothing gains a useless drag. - val sheetState = rememberModalBottomSheetState() + // Every value left enabled, rather than dropping PartiallyExpanded: that stop is the only + // thing a drag on a sheet can *do* other than dismiss it, so a sheet taller than half the + // screen would open at full height and could not be made smaller. Material caps the stop at + // the sheet's own height, so short sheets still open at that height and gain no useless drag. + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) // `Dispatchers.Main` because [onResult] reaches a snackbar on the screen underneath, and // because that is the thread the composition scope this replaces used to resume on. diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/SheetParts.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/SheetParts.kt index 4888dc0de..b5aa0f0a3 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/SheetParts.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/SheetParts.kt @@ -116,12 +116,11 @@ fun ToggleRow( role = Role.Switch, onValueChange = onCheckedChange, ), - headlineContent = { Text(title) }, supportingContent = subtitle?.let { { Text(it) } }, leadingContent = { Icon(icon, contentDescription = null) }, trailingContent = { Switch(checked = checked, onCheckedChange = null) }, colors = sheetRowColors, - ) + ) { Text(title) } } /** @@ -178,11 +177,10 @@ fun SheetAction( ) { ListItem( modifier = Modifier.clickable(onClick = onClick), - headlineContent = { Text(title) }, supportingContent = subtitle?.let { { Text(it) } }, leadingContent = { Icon(icon, contentDescription = null, tint = tint ?: LocalContentColor.current) }, colors = sheetRowColors, - ) + ) { Text(title) } } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/navigation/Route.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/navigation/Route.kt index a01db3942..9898476b2 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/navigation/Route.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/navigation/Route.kt @@ -3,7 +3,7 @@ package org.matrix.vector.manager.ui.navigation import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Extension import androidx.compose.material.icons.rounded.Home -import androidx.compose.material.icons.rounded.ReceiptLong +import androidx.compose.material.icons.automirrored.rounded.ReceiptLong import androidx.compose.material.icons.rounded.CloudDownload import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation3.runtime.NavKey @@ -118,7 +118,7 @@ val TOP_LEVEL_DESTINATIONS: List = TopLevelDestination( "logs", TopLevelRoute.Logs, - Icons.Rounded.ReceiptLong, + Icons.AutoMirrored.Rounded.ReceiptLong, R.string.nav_logs, ), ) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/canary/CanaryScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/canary/CanaryScreen.kt index 9eee1a120..ea16b981b 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/canary/CanaryScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/canary/CanaryScreen.kt @@ -17,7 +17,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Download -import androidx.compose.material.icons.rounded.OpenInNew +import androidx.compose.material.icons.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.Science import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -88,7 +88,7 @@ fun CanaryScreen( actions = { IconButton(onClick = { onOpenUrl(GitHubRepository.CANARY_URL) }) { Icon( - Icons.Rounded.OpenInNew, + Icons.AutoMirrored.Rounded.OpenInNew, contentDescription = stringResource(R.string.canary_open_actions), ) } @@ -124,7 +124,7 @@ fun CanaryScreen( onClick = { onOpenUrl(GitHubRepository.CANARY_URL) } ) { Icon( - Icons.Rounded.OpenInNew, + Icons.AutoMirrored.Rounded.OpenInNew, contentDescription = null, modifier = Modifier.size(18.dp), ) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeAppearanceSheet.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeAppearanceSheet.kt index dfc64994a..4ad181b43 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeAppearanceSheet.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeAppearanceSheet.kt @@ -46,7 +46,8 @@ import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -126,12 +127,12 @@ fun HomeAppearanceSheet(onDismiss: () -> Unit) { val doh by settings.dohEnabled.collectAsStateWithLifecycle() val dohStatus by ServiceLocator.dns.status.collectAsStateWithLifecycle() - // The default state, deliberately. `skipPartiallyExpanded` removes the half-height stop, which - // is the only thing a drag on a sheet can *do* other than dismiss it, so a sheet taller than - // half the screen would open at full height and could not be made smaller. Left alone, Material - // adds the stop only when the content is actually taller than half the screen, so short sheets - // still open at their own height and nothing gains a useless drag. - val sheetState = rememberModalBottomSheetState() + // Every value stays enabled, deliberately. Dropping PartiallyExpanded removes the half-height + // stop, which is the only thing a drag on a sheet can *do* other than dismiss it, so a sheet + // taller than half the screen would open at full height and could not be made smaller. Left + // alone, Material caps that stop at the sheet's own height, so short sheets still open at + // their own height and nothing gains a useless drag. + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { LocalizedOverlay { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt index e9d302b2f..49d2631fc 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.CallSplit -import androidx.compose.material.icons.rounded.AddToHomeScreen +import androidx.compose.material.icons.automirrored.rounded.AddToHomeScreen import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.Bedtime import androidx.compose.material.icons.rounded.Close @@ -411,7 +411,7 @@ private fun LauncherPrompt( ) { VectorAlertDialog( onDismissRequest = onLater, - icon = { Icon(Icons.Rounded.AddToHomeScreen, contentDescription = null) }, + icon = { Icon(Icons.AutoMirrored.Rounded.AddToHomeScreen, contentDescription = null) }, title = { Text(stringResource(R.string.launcher_prompt_title)) }, text = { Text(stringResource(R.string.launcher_prompt_body)) }, confirmButton = { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/LanguageSheet.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/LanguageSheet.kt index c8b8a8392..74f7c1dd2 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/LanguageSheet.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/LanguageSheet.kt @@ -37,7 +37,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -84,12 +85,12 @@ fun LanguageSheet(onOpen: (String) -> Unit, onDismiss: () -> Unit) { val current by settings.appLocale.collectAsStateWithLifecycle() val context = LocalContext.current val locales = remember { availableLocales() } - // The default state, deliberately. `skipPartiallyExpanded` removes the half-height stop, which - // is the only thing a drag on a sheet can *do* other than dismiss it, so a sheet taller than - // half the screen would open at full height and could not be made smaller. Left alone, Material - // adds the stop only when the content is actually taller than half the screen, so short sheets - // still open at their own height and nothing gains a useless drag. - val sheetState = rememberModalBottomSheetState() + // Every value stays enabled, deliberately. Dropping PartiallyExpanded removes the half-height + // stop, which is the only thing a drag on a sheet can *do* other than dismiss it, so a sheet + // taller than half the screen would open at full height and could not be made smaller. Left + // alone, Material caps that stop at the sheet's own height, so short sheets still open at + // their own height and nothing gains a useless drag. + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { LocalizedOverlay { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt index 4b4e52300..21e44b699 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.AddToHomeScreen +import androidx.compose.material.icons.automirrored.rounded.AddToHomeScreen import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.InstallMobile import androidx.compose.material.icons.rounded.Notifications @@ -307,7 +307,7 @@ private fun OpeningVectorCard( Spacer(Modifier.height(8.dp)) RouteRow( - icon = Icons.Rounded.AddToHomeScreen, + icon = Icons.AutoMirrored.Rounded.AddToHomeScreen, label = stringResource(R.string.launcher_shortcut), done = presence.shortcutPinned, action = stringResource(R.string.launcher_shortcut_create), diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt index 3d1f1066c..2763b3cc4 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt @@ -42,7 +42,7 @@ import androidx.compose.material.icons.rounded.Tune import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material.icons.rounded.UnfoldLess import androidx.compose.material.icons.rounded.UnfoldMore -import androidx.compose.material.icons.rounded.Label +import androidx.compose.material.icons.automirrored.rounded.Label import androidx.compose.material.icons.rounded.SearchOff import androidx.compose.material.icons.rounded.VerticalAlignBottom import androidx.compose.material.icons.rounded.VerticalAlignTop @@ -71,7 +71,8 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -640,7 +641,7 @@ private fun ActiveFilterRow( label = { Text(tag, style = VectorLogLine, maxLines = 1) }, avatar = { Icon( - Icons.Rounded.Label, + Icons.AutoMirrored.Rounded.Label, contentDescription = null, modifier = Modifier.size(16.dp), ) @@ -694,11 +695,11 @@ private fun LogSettingsSheet( val enabled by viewModel.verboseEnabled.collectAsStateWithLifecycle() val enforced by viewModel.verboseEnforced.collectAsStateWithLifecycle() val inlineTraces by viewModel.tracesInline.collectAsStateWithLifecycle() - // Left at the default rather than passing skipPartiallyExpanded, which removes the half-height - // stop — the only thing a drag on a sheet can do other than dismiss it. Material adds that stop - // only when the content is taller than half the screen, so short sheets still open at their own - // height and nothing gains a useless drag. - val sheetState = rememberModalBottomSheetState() + // Every value left enabled rather than dropping PartiallyExpanded, which would remove the + // half-height stop — the only thing a drag on a sheet can do other than dismiss it. Material + // caps that stop at the sheet's own height, so short sheets still open at their own height and + // nothing gains a useless drag. + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { LocalizedOverlay { @@ -716,7 +717,6 @@ LocalizedOverlay { // a reason to take the control away — and only an older daemon can, since the // current one reports the stored preference as it stands. modifier = Modifier.clickable { viewModel.setVerbose(!enabled) }, - headlineContent = { Text(stringResource(R.string.logs_verbose_switch)) }, supportingContent = { Column { Text( @@ -744,7 +744,7 @@ LocalizedOverlay { Switch(checked = enabled, onCheckedChange = { viewModel.setVerbose(it) }) }, colors = sheetRowColors, - ) + ) { Text(stringResource(R.string.logs_verbose_switch)) } ToggleRow( title = stringResource(R.string.logs_traces_inline), @@ -758,14 +758,12 @@ LocalizedOverlay { ListItem( modifier = Modifier.clickable(onClick = onSave), - headlineContent = { Text(stringResource(R.string.logs_save)) }, supportingContent = { Text(stringResource(R.string.logs_save_summary)) }, leadingContent = { Icon(Icons.Rounded.Save, contentDescription = null) }, colors = sheetRowColors, - ) + ) { Text(stringResource(R.string.logs_save)) } ListItem( modifier = Modifier.clickable(onClick = onRotate), - headlineContent = { Text(stringResource(R.string.logs_rotate)) }, supportingContent = { Text(stringResource(R.string.logs_rotate_summary)) }, leadingContent = { Icon( @@ -775,7 +773,7 @@ LocalizedOverlay { ) }, colors = sheetRowColors, - ) + ) { Text(stringResource(R.string.logs_rotate)) } } } } @@ -940,7 +938,7 @@ private fun LogFilterSheet( onTag: (String?) -> Unit, onClear: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { LocalizedOverlay { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt index 385c42d8b..e9014e4c8 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt @@ -55,7 +55,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow +import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -96,7 +96,8 @@ import androidx.compose.material3.Button import androidx.compose.material3.Checkbox import androidx.compose.material3.ListItem import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import org.matrix.vector.manager.data.model.ReleaseAsset import org.matrix.vector.manager.data.model.RepoVersion import org.matrix.vector.manager.data.model.StoreEntry @@ -358,7 +359,7 @@ fun ModulesScreen( } if (tabs.size > 1) { - TabRow(selectedTabIndex = pagerState.currentPage) { + PrimaryTabRow(selectedTabIndex = pagerState.currentPage) { tabs.forEachIndexed { index, tab -> Tab( selected = pagerState.currentPage == index, @@ -1310,7 +1311,7 @@ private fun ModuleUpdatesSheet( onStart: (List) -> Unit, onDismiss: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) val colors = MaterialTheme.colorScheme val context = LocalContext.current @@ -1366,7 +1367,6 @@ private fun ModuleUpdatesSheet( chosen = if (checked) chosen + name else chosen - name }, ), - headlineContent = { Text(row.entry.module.title) }, supportingContent = { Text( text = @@ -1415,7 +1415,7 @@ private fun ModuleUpdatesSheet( } }, colors = sheetRowColors, - ) + ) { Text(row.entry.module.title) } } } Spacer(Modifier.height(12.dp)) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt index 9cf6dbc57..3ed1e7156 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt @@ -26,6 +26,7 @@ import org.matrix.vector.manager.data.model.StoreEntry import org.matrix.vector.manager.data.repository.ModuleUpdateQueue import org.lsposed.lspd.ILSPManagerService import org.matrix.vector.manager.data.model.XposedApi +import org.matrix.vector.manager.data.model.versionCodeCompat import org.matrix.vector.manager.di.ServiceLocator import org.matrix.vector.manager.ipc.DaemonClient import org.matrix.vector.manager.logE @@ -514,7 +515,7 @@ class ModulesViewModel( detection.inspect( appInfo, packageManager, - pkg.longVersionCode, + pkg.versionCodeCompat, pkg.lastUpdateTime, ) if (!manifest.isModule) return@mapNotNull null @@ -524,7 +525,7 @@ class ModulesViewModel( userId = appInfo.uid / PER_USER_RANGE, appName = appInfo.loadLabel(packageManager).toString(), versionName = pkg.versionName ?: "", - versionCode = pkg.longVersionCode, + versionCode = pkg.versionCodeCompat, description = manifest.description, minVersion = manifest.minApiVersion, targetVersion = manifest.targetApiVersion, diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeScreen.kt index f39d8efa1..db0b68c29 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeScreen.kt @@ -6,14 +6,15 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.rounded.AutoAwesome import androidx.compose.material.icons.rounded.DoneAll -import androidx.compose.material.icons.rounded.PlaylistAdd +import androidx.compose.material.icons.automirrored.rounded.PlaylistAdd import androidx.compose.material.icons.rounded.RemoveDone import androidx.compose.material.icons.rounded.SettingsBackupRestore import androidx.compose.material.icons.rounded.SaveAlt import androidx.compose.material.icons.rounded.SwapVert import androidx.compose.material3.FilterChip import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.ui.graphics.vector.ImageVector import org.matrix.vector.manager.ui.components.ChoiceRow import org.matrix.vector.manager.ui.components.SheetAction @@ -592,7 +593,7 @@ private fun ScopeSelectMenu( ToggleRow( title = stringResource(R.string.scope_include_new_apps), subtitle = stringResource(R.string.scope_include_new_apps_summary), - icon = Icons.Rounded.PlaylistAdd, + icon = Icons.AutoMirrored.Rounded.PlaylistAdd, checked = includeNewApps, onCheckedChange = onIncludeNewApps, ) @@ -776,7 +777,7 @@ private fun ScopeSheet( onDismiss: () -> Unit, content: @Composable () -> Unit, ) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { LocalizedOverlay { Column(Modifier.verticalScroll(rememberScrollState()).padding(bottom = 24.dp)) { @@ -919,7 +920,6 @@ private fun AppRow( Modifier.border(width = 2.dp, color = ring, shape = CircleShape).padding(4.dp), ) }, - headlineContent = { Text(app.appName) }, supportingContent = { Column { Text( @@ -952,7 +952,7 @@ private fun AppRow( ListItemDefaults.colors( containerColor = Color.Transparent ), - ) + ) { Text(app.appName) } if (menuOpen) { PackageActionSheet( packageName = app.packageName, diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoDetailsScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoDetailsScreen.kt index 32c3f43ca..a27820153 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoDetailsScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoDetailsScreen.kt @@ -5,7 +5,8 @@ import org.matrix.vector.manager.ui.components.ConfirmInstall import org.matrix.vector.manager.ui.components.ToggleRow import org.matrix.vector.manager.ui.components.SheetHeading import org.matrix.vector.manager.ui.components.sheetRowColors -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.material.icons.rounded.Tune import androidx.compose.material.icons.rounded.NotificationsOff import androidx.compose.material.icons.rounded.MoreVert @@ -64,7 +65,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow +import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -271,7 +272,7 @@ fun RepoDetailsScreen(packageName: String, onNavigateBack: () -> Unit) { } } - TabRow(selectedTabIndex = pagerState.currentPage) { + PrimaryTabRow(selectedTabIndex = pagerState.currentPage) { tabs.forEachIndexed { index, label -> Tab( selected = pagerState.currentPage == index, @@ -327,7 +328,7 @@ fun RepoDetailsScreen(packageName: String, onNavigateBack: () -> Unit) { if (optionsOpen) { val muted by viewModel.updatesMuted.collectAsStateWithLifecycle() - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) ModalBottomSheet(onDismissRequest = { optionsOpen = false }, sheetState = sheetState) { LocalizedOverlay { Column(Modifier.padding(bottom = 24.dp)) { @@ -850,10 +851,9 @@ private fun InfoRow( ) { ListItem( modifier = if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier, - headlineContent = { Text(label) }, supportingContent = { Text(value) }, leadingContent = { Icon(icon, contentDescription = null) }, - ) + ) { Text(label) } HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant) } @@ -873,7 +873,6 @@ LocalizedOverlay { release.apks.forEach { asset -> ListItem( modifier = Modifier.clickable { onPick(asset) }, - headlineContent = { Text(asset.name.orEmpty()) }, supportingContent = { val size = Formatter.formatShortFileSize(context, asset.size) val downloads = @@ -887,7 +886,7 @@ LocalizedOverlay { Text(listOfNotNull(size, downloads).joinToString(" · ")) }, colors = sheetRowColors, - ) + ) { Text(asset.name.orEmpty()) } } Spacer(Modifier.navigationBarsPadding().height(16.dp)) } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoDetailsViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoDetailsViewModel.kt index ae78e335b..4d1ae526d 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoDetailsViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoDetailsViewModel.kt @@ -19,6 +19,7 @@ import org.matrix.vector.manager.data.model.Release import org.matrix.vector.manager.data.model.ReleaseAsset import org.matrix.vector.manager.data.model.RepoVersion import org.matrix.vector.manager.data.model.StoreInstall +import org.matrix.vector.manager.data.model.versionCodeCompat import org.matrix.vector.manager.data.repository.InstallStep import org.matrix.vector.manager.data.repository.ModuleInstaller import org.matrix.vector.manager.data.repository.RepoRepository @@ -105,7 +106,7 @@ class RepoDetailsViewModel( ServiceLocator.moduleDetection.inspect( appInfo, packageManager, - info.longVersionCode, + info.versionCodeCompat, info.lastUpdateTime, ) _installedScope.value = manifest.scope diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoScreen.kt index 12b155762..ecc3cbba6 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/repo/RepoScreen.kt @@ -7,7 +7,8 @@ import androidx.compose.material.icons.rounded.LowPriority import androidx.compose.material.icons.rounded.NewReleases import androidx.compose.material3.FilterChip import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import org.matrix.vector.manager.ui.components.ChoiceRow import org.matrix.vector.manager.ui.components.SheetHeading import androidx.compose.foundation.clickable @@ -272,11 +273,11 @@ private fun StoreFilterSheet( onChannelChange: (StoreChannel) -> Unit, onDismiss: () -> Unit, ) { - // Left at the default rather than passing skipPartiallyExpanded, which removes the half-height - // stop — the only thing a drag on a sheet can do other than dismiss it. Material adds that stop - // only when the content is taller than half the screen, so short sheets still open at their own - // height and nothing gains a useless drag. - val sheetState = rememberModalBottomSheetState() + // Every value left enabled rather than dropping PartiallyExpanded, which would remove the + // half-height stop — the only thing a drag on a sheet can do other than dismiss it. Material + // caps that stop at the sheet's own height, so short sheets still open at their own height and + // nothing gains a useless drag. + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { LocalizedOverlay { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt index ee7e261b1..4bf8fe0e4 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt @@ -18,7 +18,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.Folder -import androidx.compose.material.icons.rounded.OpenInNew +import androidx.compose.material.icons.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.Save import androidx.compose.material.icons.rounded.Science import androidx.compose.material.icons.rounded.Terminal @@ -169,7 +169,7 @@ fun TroubleshootScreen( item { Step( - icon = Icons.Rounded.OpenInNew, + icon = Icons.AutoMirrored.Rounded.OpenInNew, title = stringResource(R.string.report_step_zygisk), body = stringResource(R.string.report_step_zygisk_body), titleAction = { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt index b885e22ca..af212eede 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt @@ -14,7 +14,8 @@ import java.text.DateFormat import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ListItem import androidx.compose.material.icons.rounded.RadioButtonUnchecked @@ -609,7 +610,7 @@ private fun VersionsSheet( onSelect: (FrameworkRelease) -> Unit, onDismiss: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) val colors = MaterialTheme.colorScheme val locale = currentLocale() @@ -630,13 +631,6 @@ private fun VersionsSheet( onSelect(release) onDismiss() }, - // One line each, always. What names the build is the same shape in every - // row — "Vector v2.0 canary 3060", then its date and channel — and a row - // that wraps because of what is beside it reads as a different kind of - // entry when it is not. - headlineContent = { - Text(release.title, maxLines = 1, overflow = TextOverflow.Ellipsis) - }, supportingContent = { Text( listOfNotNull( @@ -707,7 +701,13 @@ private fun VersionsSheet( } }, colors = sheetRowColors, - ) + ) { + // One line each, always. What names the build is the same shape in every + // row — "Vector v2.0 canary 3060", then its date and channel — and a row + // that wraps because of what is beside it reads as a different kind of + // entry when it is not. + Text(release.title, maxLines = 1, overflow = TextOverflow.Ellipsis) + } } } } diff --git a/services/daemon-service/build.gradle.kts b/services/daemon-service/build.gradle.kts index e583a2570..41a5df9bf 100644 --- a/services/daemon-service/build.gradle.kts +++ b/services/daemon-service/build.gradle.kts @@ -7,8 +7,8 @@ android { sourceSets { named("main") { - java.srcDirs("src/main/java", "../libxposed/service/src/main") - aidl.srcDirs("src/main/aidl", "../libxposed/interface/src/main/aidl") + java.directories.addAll(listOf("src/main/java", "../libxposed/service/src/main")) + aidl.directories.addAll(listOf("src/main/aidl", "../libxposed/interface/src/main/aidl")) } } diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index ef5cee53c..497446194 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -1,5 +1,7 @@ -val versionCodeProvider: Provider by rootProject.extra -val versionNameProvider: Provider by rootProject.extra +@Suppress("UNCHECKED_CAST") +val versionCodeProvider = rootProject.extra["versionCodeProvider"] as Provider +@Suppress("UNCHECKED_CAST") +val versionNameProvider = rootProject.extra["versionNameProvider"] as Provider plugins { alias(libs.plugins.agp.lib) @@ -20,7 +22,11 @@ android { buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) } - sourceSets { named("main") { java.srcDirs("src/main/kotlin", "libxposed/api/src/main/java") } } + sourceSets { + named("main") { + java.directories.addAll(listOf("src/main/kotlin", "libxposed/api/src/main/java")) + } + } } dependencies { diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt index e1d73ac92..02f1fc488 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt @@ -15,6 +15,12 @@ object VectorLifecycleManager { val activeModules: MutableSet = ConcurrentHashMap.newKeySet() + /** + * The API declares `onPackageLoaded` as API 29 and up, so this carries the same requirement + * rather than leaving the callers to remember it. `LoadedApkCreateAppFactoryHooker` is the only + * one, and it already dispatches from inside a Q check. + */ + @RequiresApi(Build.VERSION_CODES.Q) fun dispatchPackageLoaded( packageName: String, appInfo: ApplicationInfo, diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt index a38e2e774..a95911755 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt @@ -53,8 +53,10 @@ private object PackageContextHelper { val packageName: String val processName: String if (isFirstPackage) { - packageName = boundPackage!! - processName = boundProcess!! + // Both are smart-cast here: `isFirstPackage` is a local val that already carries the + // null checks, so K2 tracks them through it and the assertions are redundant. + packageName = boundPackage + processName = boundProcess } else { packageName = apkPackageName processName = boundPackage ?: apkPackageName diff --git a/zygisk/build.gradle.kts b/zygisk/build.gradle.kts index 83d9980e0..60726a312 100644 --- a/zygisk/build.gradle.kts +++ b/zygisk/build.gradle.kts @@ -9,12 +9,15 @@ plugins { ktfmt { kotlinLangStyle() } -val versionCodeProvider: Provider by rootProject.extra -val versionNameProvider: Provider by rootProject.extra -val versionHashProvider: Provider by rootProject.extra -val injectedPackageName: String by rootProject.extra -val injectedPackageUid: Int by rootProject.extra -val defaultManagerPackageName: String by rootProject.extra +@Suppress("UNCHECKED_CAST") +val versionCodeProvider = rootProject.extra["versionCodeProvider"] as Provider +@Suppress("UNCHECKED_CAST") +val versionNameProvider = rootProject.extra["versionNameProvider"] as Provider +@Suppress("UNCHECKED_CAST") +val versionHashProvider = rootProject.extra["versionHashProvider"] as Provider +val injectedPackageName = rootProject.extra["injectedPackageName"] as String +val injectedPackageUid = rootProject.extra["injectedPackageUid"] as Int +val defaultManagerPackageName = rootProject.extra["defaultManagerPackageName"] as String android { namespace = "org.matrix.vector"