From 5fd68aadb8cceb9183c068a4d1795a7aeccb49cc Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 1 Aug 2026 22:39:12 +0200 Subject: [PATCH 1/2] Update the dependencies, and make a clean build say nothing The fmt and commons-lang submodules move to the tip of their upstream branches, core-splashscreen to 1.2.0, navigation3 to 1.1.5 and material3 to 1.5.0-alpha25, which is everything in the catalog that was behind. Both libxposed submodules stay where they are, since their only new commit is the API 102 RFC and that belongs with the API work. In CI, ninja moves to 1.13.2 and crowdin.yml's floating action refs are pinned to majors like everywhere else. The NDK pin moves from 29.0.13113456, which is r29 beta 1, to r29 stable at 29.0.14206865. Its clang 21 rejects the lsplant x86 build over duplicate `` definitions arriving through two module fragments, so phmap's SSE2 group scan is switched off for every native module. The rest removes the seventy or so lines a build printed when nothing was wrong. `:hiddenapi:stubs` no longer overrides its Java version to 8. Thirty-three Kotlin deprecations in the manager are migrated: ListItem's headline becomes a trailing content lambda, TabRow becomes PrimaryTabRow, rememberModalBottomSheetState becomes rememberBottomSheetState, and eight icons move to their auto-mirrored versions; two reads that have no replacement are suppressed in place. Vendored and generated Java, along with `:legacy`, compile with javac's notes off, and lsplant's string literal operator template warning is silenced at its target. The `by extra` delegate and AGP's `srcDirs`, both deprecated, are rewritten, and the apksign plugin is replaced by the signing configuration it was applying, since its last release still calls the deprecated `Project.getProperties`. Tested with a clean `./gradlew zipAll` over debug and release, which now prints only its task list. The resulting release zip is installed and running on a Pixel 6 on Android 17. Signing was checked both ways: with a keystore, the manager APK and the certificate compiled into `SignInfo.kt` match; without one, both fall back to the debug key as before. Lint is untouched. It already fails at HEAD with 556 errors, 462 of them missing translations, and no workflow runs it; that backlog needs its own change. Supersedes #824 and #835. --- .github/workflows/core.yml | 2 +- .github/workflows/crowdin.yml | 4 +- .gitignore | 2 + build.gradle.kts | 191 ++++++++++++------ daemon/build.gradle.kts | 15 +- .../kotlin/org/matrix/vector/daemon/Cli.kt | 2 +- external/CMakeLists.txt | 6 +- external/apache/build.gradle.kts | 4 +- external/apache/commons-lang | 2 +- external/axml/build.gradle.kts | 4 +- external/fmt | 2 +- gradle/libs.versions.toml | 7 +- hiddenapi/stubs/build.gradle.kts | 8 +- manager/build.gradle.kts | 48 ++++- .../vector/manager/demo/DemoActivity.kt | 3 +- .../manager/data/model/ModuleDetection.kt | 6 +- .../manager/data/repository/AppRepository.kt | 3 + .../ui/components/PackageActionMenu.kt | 14 +- .../manager/ui/components/SheetParts.kt | 6 +- .../vector/manager/ui/navigation/Route.kt | 4 +- .../manager/ui/screens/canary/CanaryScreen.kt | 6 +- .../ui/screens/home/HomeAppearanceSheet.kt | 15 +- .../manager/ui/screens/home/HomeScreen.kt | 4 +- .../manager/ui/screens/home/LanguageSheet.kt | 15 +- .../ui/screens/home/SystemStatusScreen.kt | 4 +- .../manager/ui/screens/logs/LogsScreen.kt | 28 ++- .../ui/screens/modules/ModulesScreen.kt | 12 +- .../manager/ui/screens/modules/ScopeScreen.kt | 12 +- .../ui/screens/repo/RepoDetailsScreen.kt | 15 +- .../manager/ui/screens/repo/RepoScreen.kt | 13 +- .../ui/screens/report/TroubleshootScreen.kt | 4 +- .../screens/update/FrameworkUpdateScreen.kt | 20 +- services/daemon-service/build.gradle.kts | 4 +- xposed/build.gradle.kts | 12 +- .../vector/impl/hookers/LoadedApkHookers.kt | 6 +- zygisk/build.gradle.kts | 15 +- 36 files changed, 318 insertions(+), 200 deletions(-) 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/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/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/repository/AppRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt index 270a7ac1f..06b5ab448 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 @@ -54,6 +54,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 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/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/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/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" From 021cab7f186f0045a96e6fb62844fd182a2a69c3 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 1 Aug 2026 23:32:36 +0200 Subject: [PATCH 2/2] Call only what Android 8.1 has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manager and the daemon both set a minimum of API 27 and both called methods that arrived later, which on 8.1 is a NoSuchMethodError rather than a lint opinion. Every site is now gated on the release that introduced the call, each checked against the platform's own api-versions.xml. `PackageInfo.getLongVersionCode` is API 28, and the int field it replaces was deprecated in the same release, so one of the two is always wrong on a supported device. RepoRepository already carried a private compat helper; it becomes a shared extension, and the app list, both module lists and the store's installed-version check go through it instead of reading the long form unguarded. Reading the old field loses nothing below 28, since a package installed there has no version code major to drop. `FileObserver(List, int)` is API 29, so below that the dex2oat watcher is one observer per node built from the single-path constructor those replaced, and stopping it has to reach both. `LocalServerSocket` only implements `Closeable` from 28, so its socket is closed by hand; the client socket has implemented it since 17 and keeps `use`. Two constants are now spelled out rather than referenced, because on 27 they do not exist to reference: `Telephony.Sms.Intents.SECRET_CODE_ACTION` became public in 28, and `Intent.EXTRA_PACKAGES` in 34. Both carry the same string there — 8.1 broadcasts the secret code action from its hidden `TelephonyIntents` constant, and our own daemon writes the package key as a literal. Lastly `dispatchPackageLoaded` takes the `@RequiresApi(Q)` its callback already declares, so a future caller cannot forget the guard the current one has. Lint reports no NewApi or InlinedApi finding on either variant now, down from twelve. --- .../org/matrix/vector/daemon/VectorService.kt | 14 ++- .../matrix/vector/daemon/env/Dex2OatServer.kt | 110 +++++++++++------- .../vector/manager/demo/FakeManagerService.kt | 21 +++- .../manager/data/model/PackageVersion.kt | 22 ++++ .../manager/data/repository/AppRepository.kt | 3 +- .../manager/data/repository/RepoRepository.kt | 10 +- .../vector/manager/ipc/PackageBroadcasts.kt | 15 ++- .../ui/screens/modules/ModulesViewModel.kt | 5 +- .../ui/screens/repo/RepoDetailsViewModel.kt | 3 +- .../vector/impl/VectorLifecycleManager.kt | 6 + 10 files changed, 149 insertions(+), 60 deletions(-) create mode 100644 manager/src/main/kotlin/org/matrix/vector/manager/data/model/PackageVersion.kt 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/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/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 06b5ab448..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 @@ -73,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/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/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/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,