From 55cd7ac988a4b64a30533743940d1b2446d242bc Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 2 Aug 2026 22:16:23 +0200 Subject: [PATCH 1/2] Keep XResources out of the dex everything else shares Some framework types extend super classes that no dex contains: they are generated at runtime, so that they can inherit from whichever platform classes the device actually provides. A type whose super class does not yet exist cannot be resolved, and the runtime records the failure rather than retrying it, so the damage outlives the window that caused it. The fragility is transitive -- every class naming such a type acquires it. A whole-program optimiser spreads it beyond what the source shows. Each lambda is lowered to a class of its own, and classes of the same shape are merged afterwards, so a lambda written in a fragile class can end up sharing its class with lambdas from anywhere in the program. The reference then sits in a class that arbitrary code instantiates, and the relationship exists only in the optimiser's output. That is what happened to XResources: its two lambdas put it inside the shared Function synthetic, which commons-lang's ClassUtilsX instantiates from its static initialiser, which XposedHelpers.findClass calls. Every findClass in system_server then failed for the rest of the boot, so no module's hooks landed there (#847, #848). Both lambdas are written out long-hand now. Keep rules cannot state the invariant, since they govern the classes one writes rather than the ones an optimiser invents, so it is checked where it is decided. checkXResourcesIsolationRelease reads the optimised dex, resolves class names through the mapping file, and fails the build if any class outside resource hooking has come to name one of these types. --- gradle/libs.versions.toml | 2 + legacy/consumer-rules.pro | 11 +- .../java/android/content/res/XResources.java | 53 +++++- zygisk/build.gradle.kts | 177 ++++++++++++++++++ 4 files changed, 237 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df596a88a..59e4a1167 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,8 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } [libraries] +# Build-only: reads the optimised DEX in checkXResourcesIsolation. +smali-dexlib2 = { group = "com.android.tools.smali", name = "smali-dexlib2", version = "3.0.9" } 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" } diff --git a/legacy/consumer-rules.pro b/legacy/consumer-rules.pro index 7da43889d..2698c9b56 100644 --- a/legacy/consumer-rules.pro +++ b/legacy/consumer-rules.pro @@ -1,5 +1,14 @@ +# Keeping a class also keeps the optimiser from merging it into another one, so the resource types +# below stay classes of their own. That says nothing about the classes the optimiser *invents*: a +# lambda written anywhere becomes a class, and classes of the same shape are merged afterwards, so a +# reference can end up in a class no one wrote and no rule here names. Types whose super class is +# generated at runtime must not travel that way, since they cannot be resolved until the device has +# built the super class and the runtime never retries a failed resolution. That invariant is checked +# against the optimised dex by checkXResourcesIsolationRelease, not from this file. -keep class android.** { *; } -keep class de.robv.android.xposed.** { *; } -# Workaround to bypass verification of in-memory built class xposed.dummy.XResourcesSuperClass +# The in-memory built class xposed.dummy.XResourcesSuperClass exists only on the device, so the +# class below is a deliberate split: it isolates the reference to XResources from its owner, which +# would otherwise be verified long before that super class exists. -keepclassmembers class org.matrix.vector.legacy.LegacyDelegateImpl$ResourceProxy { *; } diff --git a/legacy/src/main/java/android/content/res/XResources.java b/legacy/src/main/java/android/content/res/XResources.java index 833ea926e..d1e53636f 100644 --- a/legacy/src/main/java/android/content/res/XResources.java +++ b/legacy/src/main/java/android/content/res/XResources.java @@ -75,7 +75,8 @@ public class XResources extends XResourcesSuperClass { private static final WeakHashMap sXmlInstanceDetails = new WeakHashMap<>(); private static final String EXTRA_XML_INSTANCE_DETAILS = "xmlInstanceDetails"; - private static final ThreadLocal> sIncludedLayouts = ThreadLocal.withInitial(() -> new LinkedList<>()); + // No lambda, and no anonymous ThreadLocal either. See the note above [includedLayouts]. + private static final ThreadLocal> sIncludedLayouts = new ThreadLocal<>(); private static final HashMap sResDirLastModified = new HashMap<>(); private static final HashMap sResDirPackageNames = new HashMap<>(); @@ -92,11 +93,53 @@ public XResources(ClassLoader classLoader, String resDir) { if (resDir != null) { synchronized (sReplacementsCacheMap) { - mReplacementsCache = sReplacementsCacheMap.computeIfAbsent(resDir, k -> new byte[128]); + // Not computeIfAbsent: its mapping function would be a lambda, which this class may + // not create. See the note above [includedLayouts]. Under this lock the two spellings + // describe the same operation. + byte[] cache = sReplacementsCacheMap.get(resDir); + if (cache == null) { + cache = new byte[128]; + sReplacementsCacheMap.put(resDir, cache); + } + mReplacementsCache = cache; } } } + /** + * The thread's stack of `LayoutInflater.parseInclude` calls, created on first use. + * + * Written the long way on purpose. `ThreadLocal.withInitial(() -> ...)`, and an anonymous + * `ThreadLocal` overriding `initialValue`, would each add a class that names `XResources`, and + * this class may not be named by classes it does not control. + * + * The reason is its super class. {@link xposed.dummy.XResourcesSuperClass} is in no dex; it is + * generated at runtime, because it has to inherit from whichever `Resources` subclass the + * platform actually provides. A type whose super class does not yet exist cannot be resolved, + * and a resolution failure is remembered: the runtime marks the class erroneous and re-throws + * for every later attempt. So the fragility is not local. It travels along references — anything + * naming `XResources` is unusable in the same window, and stays unusable afterwards. + * + * A whole-program optimiser extends that reach in a way the source cannot show. Each lambda + * becomes a class of its own, and classes of the same shape are then merged, so two lambdas + * written in unrelated files can end up as one class. A lambda here is therefore not a private + * detail of this file: it is a reference to `XResources` placed inside a class that arbitrary + * code may instantiate, and every such instantiation inherits the window above. + * + * Keep rules cannot express this — they govern names and members, not which classes an optimiser + * invents. So the rule is stated here, next to what it protects, and checked where it is really + * decided: `checkXResourcesIsolationRelease` reads the optimised dex and fails the build if any + * class outside resource hooking has come to name one of these types. + */ + private static LinkedList includedLayouts() { + LinkedList layouts = sIncludedLayouts.get(); + if (layouts == null) { + layouts = new LinkedList<>(); + sIncludedLayouts.set(layouts); + } + return layouts; + } + /** Dummy, will never be called (objects are transferred to this class only). */ // private XResources() { // throw new UnsupportedOperationException(); @@ -220,12 +263,12 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { final XC_MethodHook parseIncludeHook = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - sIncludedLayouts.get().push(param); + includedLayouts().push(param); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { - sIncludedLayouts.get().pop(); + includedLayouts().pop(); if (param.hasThrowable()) return; @@ -962,7 +1005,7 @@ public XmlResourceParser getLayout(int id) throws NotFoundException { sXmlInstanceDetails.put(result, details); // if we were called inside LayoutInflater.parseInclude, store the details for it - MethodHookParam top = sIncludedLayouts.get().peek(); + MethodHookParam top = includedLayouts().peek(); if (top != null) top.setObjectExtra(EXTRA_XML_INSTANCE_DETAILS, details); } diff --git a/zygisk/build.gradle.kts b/zygisk/build.gradle.kts index 60726a312..dcfe1846c 100644 --- a/zygisk/build.gradle.kts +++ b/zygisk/build.gradle.kts @@ -1,7 +1,20 @@ +import com.android.tools.smali.dexlib2.DexFileFactory +import com.android.tools.smali.dexlib2.Opcodes +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference import java.security.MessageDigest import org.apache.commons.codec.binary.Hex import org.apache.tools.ant.filters.ReplaceTokens +// Reading the optimised DEX needs a DEX reader; see checkXResourcesIsolation below. +buildscript { + repositories { mavenCentral() } + dependencies { classpath(libs.smali.dexlib2) } +} + plugins { alias(libs.plugins.agp.app) alias(libs.plugins.ktfmt) @@ -225,3 +238,167 @@ androidComponents { evaluationDependsOn(":manager") evaluationDependsOn(":daemon") + +/** + * Fails the build when a type whose super class is generated at runtime leaks into a class that + * does not deal with resources. + * + * Some framework types extend super classes that no DEX contains: they are built on the device, so + * that they can inherit from whichever platform classes it actually provides. A type whose super + * class does not yet exist cannot be resolved, and the runtime records the failure instead of + * retrying it, so the damage outlives the window that caused it. The fragility is therefore + * transitive — every class naming such a type acquires it, and is unusable in the same window and + * unusable afterwards. + * + * The reach is not visible in the source. A whole-program optimiser lowers each lambda to a class + * of its own and afterwards merges classes of the same shape, so a lambda written in a fragile + * class can come to share its class with lambdas from anywhere in the program; the reference then + * sits inside a class that arbitrary code instantiates. Keep rules do not help, because they govern + * the classes one writes, not the ones an optimiser invents. + * + * So the invariant is checked where it is actually decided — in the optimised DEX. Every class that + * mentions a guarded type must be one of the classes that legitimately implements it. Names are + * recovered through the mapping file, since by this point the interesting classes are called `k` + * and `n0`. Adding to [resourceOwners] is a deliberate act: it means a class has been given a + * reference that only resolves once the device has generated the super class. + */ +val guardedTypes = + listOf("Landroid/content/res/XResources;", "Landroid/content/res/XResources\$XTypedArray;") + +/** The classes allowed to name a guarded type, by their original (pre-optimiser) names. */ +val resourceOwners = + listOf( + "android.content.res.", + "de.robv.android.xposed.XposedInit", + "de.robv.android.xposed.callbacks.XC_InitPackageResources", + "de.robv.android.xposed.callbacks.XC_LayoutInflated", + "org.matrix.vector.legacy.LegacyDelegateImpl\$ResourceProxy", + ) + +/** Every type named anywhere in [cls] — its shape, its members and its instruction stream. */ +fun typesNamedBy(cls: ClassDef): Set { + val named = mutableSetOf() + cls.superclass?.let(named::add) + named += cls.interfaces + cls.fields.forEach { named += it.type } + cls.methods.forEach { method -> + named += method.returnType + method.parameterTypes.forEach { named += it.toString() } + method.implementation?.instructions?.forEach { instruction -> + val reference = + runCatching { (instruction as? ReferenceInstruction)?.reference }.getOrNull() + when (reference) { + is TypeReference -> named += reference.type + is FieldReference -> { + named += reference.definingClass + named += reference.type + } + is MethodReference -> { + named += reference.definingClass + named += reference.returnType + reference.parameterTypes.forEach { named += it.toString() } + } + else -> Unit + } + } + } + return named +} + +/** Residual class name -> original, read from the optimiser's mapping file. */ +fun originalNames(mapping: File): Map = + if (!mapping.exists()) emptyMap() + else + mapping + .readLines() + .filter { !it.startsWith(" ") && it.endsWith(":") && it.contains(" -> ") } + .associate { line -> + val (original, residual) = line.dropLast(1).split(" -> ", limit = 2) + residual to original + } + +androidComponents { + onVariants(selector().withBuildType("release")) { variant -> + val variantCapped = variant.name.replaceFirstChar { it.uppercase() } + val dexDir = + layout.buildDirectory.dir( + "intermediates/dex/${variant.name}/minify${variantCapped}WithR8" + ) + val mappingFile = layout.buildDirectory.file("outputs/mapping/${variant.name}/mapping.txt") + + val check = + tasks.register("checkXResourcesIsolation$variantCapped") { + group = "verification" + description = + "Rejects references to runtime-super-classed types from unrelated classes." + dependsOn("minify${variantCapped}WithR8") + inputs.dir(dexDir) + inputs.file(mappingFile).optional(true) + outputs.file( + layout.buildDirectory.file("reports/xresources-isolation-${variant.name}.txt") + ) + + doLast { + val names = originalNames(mappingFile.get().asFile) + val offenders = sortedMapOf>() + var scanned = 0 + + dexDir + .get() + .asFile + .walkTopDown() + .filter { it.extension == "dex" } + .forEach { dex -> + DexFileFactory.loadDexFile(dex, Opcodes.forApi(27)).classes.forEach { + cls -> + scanned++ + val named = typesNamedBy(cls) + val hits = guardedTypes.filter { it in named && it != cls.type } + if (hits.isEmpty()) return@forEach + val residual = + cls.type.removePrefix("L").removeSuffix(";").replace('/', '.') + val original = names[residual] ?: residual + if (resourceOwners.none { original.startsWith(it) }) { + offenders.getOrPut( + if (original == residual) original + else "$original ($residual)" + ) { + mutableSetOf() + } += hits + } + } + } + + if (offenders.isNotEmpty()) { + throw GradleException( + buildString { + appendLine( + "These classes name a type whose super class is generated at runtime:" + ) + offenders.forEach { (owner, types) -> + appendLine(" $owner -> ${types.joinToString()}") + } + appendLine() + append( + "Such a type cannot be resolved until the device has built its " + + "super class, and a failed resolution is permanent, so every " + + "class holding the reference is unusable for the life of the " + + "process. A lambda is the usual way one arrives here: it " + + "becomes a class, and classes of the same shape are merged. " + + "Either remove the reference, or add the class to " + + "resourceOwners in zygisk/build.gradle.kts if it genuinely " + + "implements resource hooking." + ) + } + ) + } + outputs.files.singleFile.apply { + parentFile.mkdirs() + writeText("scanned $scanned classes, no stray references\n") + } + } + } + + tasks.named("zip$variantCapped") { dependsOn(check) } + } +} From 576cd3a3168cd85fb2e4de790535c5ecd71d90da Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 2 Aug 2026 19:25:21 +0200 Subject: [PATCH 2/2] Name every saved bug report the same way, and say which build wrote it Three places produced one and named it three ways: the log panel and the troubleshooting page formatted a translated string resource, and the root export built its own name inline. They now share logArchiveName(), which puts the build type in front of the stamp -- a report from a debug build explains behaviour a release build does not have, and the file name can say so without a round trip to ask. The name is no longer a string resource. It was translated into nineteen locales, where the only thing the translations could do was disagree. The extension stays a parameter: the manager writes a zip through SAF, while the root path shells out to tar, which is what Android ships. An archive now also records which commit wrote it, since the version code is the commit count on master and every branch build at the same depth wears the number of an official build it was never made from. Each format carries it where that format allows: the log zip in its comment field, the module backup as a field of its own document, since gzip's comment is not reachable through GZIPOutputStream. tar has no such slot, so the root export still says only what its name says. --- .../matrix/vector/daemon/data/FileSystem.kt | 6 ++- .../vector/manager/data/log/LogArchiveName.kt | 42 +++++++++++++++++++ .../data/repository/BackupRepository.kt | 5 +++ .../manager/ui/screens/logs/LogsScreen.kt | 10 +---- .../ui/screens/report/TroubleshootScreen.kt | 21 ++-------- .../src/main/res/values-ar/strings_logs.xml | 1 - .../src/main/res/values-de/strings_logs.xml | 1 - .../src/main/res/values-es/strings_logs.xml | 1 - .../src/main/res/values-fa/strings_logs.xml | 1 - .../src/main/res/values-fr/strings_logs.xml | 1 - .../src/main/res/values-in/strings_logs.xml | 1 - .../src/main/res/values-it/strings_logs.xml | 1 - .../src/main/res/values-iw/strings_logs.xml | 1 - .../src/main/res/values-ja/strings_logs.xml | 1 - .../src/main/res/values-ko/strings_logs.xml | 1 - .../src/main/res/values-pl/strings_logs.xml | 1 - .../main/res/values-pt-rBR/strings_logs.xml | 1 - .../src/main/res/values-ru/strings_logs.xml | 1 - .../src/main/res/values-tr/strings_logs.xml | 1 - .../src/main/res/values-uk/strings_logs.xml | 1 - .../src/main/res/values-vi/strings_logs.xml | 1 - .../main/res/values-zh-rCN/strings_logs.xml | 1 - .../main/res/values-zh-rTW/strings_logs.xml | 1 - manager/src/main/res/values/strings_logs.xml | 1 - 24 files changed, 57 insertions(+), 46 deletions(-) create mode 100644 manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogArchiveName.kt diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt index 1a1b6930b..d61a95912 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -518,8 +518,12 @@ object FileSystem { fun getLogs(zipFd: ParcelFileDescriptor) { runCatching { ZipOutputStream(java.io.FileOutputStream(zipFd.fileDescriptor)).use { os -> + // The commit, not just the version code: the code is the commit count on master, so + // every branch build at the same depth wears the number of an official build it was + // never made from. Without it an attached archive cannot be tied to a binary. val comment = - "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" + "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} " + + "(${BuildConfig.VERSION_CODE}) ${BuildConfig.VERSION_HASH}" os.setComment(comment) os.setLevel(java.util.zip.Deflater.BEST_COMPRESSION) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogArchiveName.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogArchiveName.kt new file mode 100644 index 000000000..b6895ed03 --- /dev/null +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogArchiveName.kt @@ -0,0 +1,42 @@ +package org.matrix.vector.manager.data.log + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import org.matrix.vector.manager.BuildConfig + +/** + * What a saved bug report is called, wherever it is saved from. + * + * Three places produce one — the log panel, the troubleshooting page, and the root export that + * tars the daemon's folder — and they used to name it three ways. The name is the first thing + * anyone attaching one to an issue sees, and it has to say which build it came from: a report from + * a debug build explains behaviour that a release build does not have, and asking after the fact + * is a round trip that the file name can save. + * + * Not a string resource. It was one, translated into nineteen locales, but a file name is not + * language — a report named in Persian and one named in German are the same file, and the + * translations only made it possible for them to disagree. + * + * [extension] rather than a fixed `zip`: the manager builds a zip through SAF, while the root + * export shells out to `tar`, which is what Android actually ships. Only the extension differs. + */ +fun logArchiveName(extension: String): String = + "Vector-logs-${BuildConfig.BUILD_TYPE}-${LocalDateTime.now().format(ARCHIVE_STAMP)}.$extension" + +/** + * Which build wrote an archive, for the archive itself to carry. + * + * The name says the build type and no more, because a name has to stay short enough to read. What + * identifies a *binary* is the commit: the version code is the commit count on master, so every + * branch build at the same depth wears the number of an official build it was never made from. + * + * Where this goes depends on what the format offers. A zip has a comment field and gets this + * verbatim; a backup is our own document and carries it as a field. `tar` has no such slot at all, + * so the root export can only say what its name says. + */ +fun archiveBuildStamp(): String = + "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} " + + "(${BuildConfig.VERSION_CODE}) ${BuildConfig.VERSION_HASH}" + +/** Sortable, no separators a file manager or a shell would have to be told about. */ +private val ARCHIVE_STAMP: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss") diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt index a8c7a9fa2..091985e2b 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import org.lsposed.lspd.models.Application +import org.matrix.vector.manager.data.log.archiveBuildStamp import org.matrix.vector.manager.ipc.DaemonClient import org.matrix.vector.manager.logE import org.matrix.vector.manager.logW @@ -28,6 +29,10 @@ class BackupRepository(private val context: Context, private val daemon: DaemonC @Serializable private data class BackupFile( val version: Int = FORMAT_VERSION, + // Which build wrote this. gzip's own comment field is not reachable through + // GZIPOutputStream, and this document is ours, so it says so itself -- `zcat file | head` + // answers "where did this come from" without a restore. + val build: String = archiveBuildStamp(), val createdAt: Long, val modules: List, ) 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 2763b3cc4..5d630b7fd 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 @@ -100,9 +100,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.LayoutDirection import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter import kotlinx.coroutines.launch +import org.matrix.vector.manager.data.log.logArchiveName import org.matrix.vector.manager.ui.components.VectorAlertDialog import org.matrix.vector.manager.ui.theme.LocalizedOverlay import org.matrix.vector.manager.R @@ -154,11 +153,8 @@ fun LogsScreen( ) { uri: Uri? -> if (uri != null) viewModel.saveTo(uri) } - val fileNameTemplate = stringResource(R.string.logs_save_name) fun launchSave() { - saveLauncher.launch( - String.format(fileNameTemplate, LocalDateTime.now().format(FILE_STAMP)) - ) + saveLauncher.launch(logArchiveName("zip")) } val savingLabel = stringResource(R.string.logs_saving) @@ -1012,8 +1008,6 @@ LocalizedOverlay { } } -private val FILE_STAMP: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss") - private fun shareZip(context: Context, uri: Uri) { val intent = Intent(Intent.ACTION_SEND).apply { 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 4bf8fe0e4..346f00c5a 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 @@ -1,5 +1,4 @@ package org.matrix.vector.manager.ui.screens.report -import android.content.Context import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement @@ -43,14 +42,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.vector.manager.R import org.matrix.vector.manager.data.github.GitHubRepository +import org.matrix.vector.manager.data.log.logArchiveName import org.matrix.vector.manager.di.ServiceLocator import org.matrix.vector.manager.logE import org.matrix.vector.manager.ui.components.SnackbarTone @@ -184,17 +182,7 @@ fun TroubleshootScreen( title = stringResource(R.string.report_step_logs), body = stringResource(R.string.report_step_logs_body), ) { - Button( - onClick = { - saveLauncher.launch( - String.format( - stringResourceOf(context, R.string.logs_save_name), - LocalDateTime.now() - .format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")), - ) - ) - } - ) { + Button(onClick = { saveLauncher.launch(logArchiveName("zip")) }) { Icon( Icons.Rounded.Save, contentDescription = null, @@ -320,8 +308,6 @@ private fun Step( } } -private fun stringResourceOf(context: Context, id: Int): String = context.getString(id) - /** * Copies the daemon's log folder somewhere the user can attach it, using root. * @@ -338,8 +324,7 @@ private fun stringResourceOf(context: Context, id: Int): String = context.getStr * going to be attached to an issue. */ private fun exportWithRoot(): Result = runCatching { - val stamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")) - val target = "/sdcard/Download/vector-logs-$stamp.tar.gz" + val target = "/sdcard/Download/${logArchiveName("tar.gz")}" val process = ProcessBuilder("su", "-c", "tar -czf $target -C /data/adb/lspd log && chmod 644 $target") .redirectErrorStream(true) diff --git a/manager/src/main/res/values-ar/strings_logs.xml b/manager/src/main/res/values-ar/strings_logs.xml index c1a555251..331be0324 100644 --- a/manager/src/main/res/values-ar/strings_logs.xml +++ b/manager/src/main/res/values-ar/strings_logs.xml @@ -69,7 +69,6 @@ رفضت الخدمة بدء سجل جديد إلغاء - vector-logs-%1$s.zip جارٍ جمع السجلات وملفات tombstone وdmesg… حُفظ البلاغ تعذّر حفظ البلاغ diff --git a/manager/src/main/res/values-de/strings_logs.xml b/manager/src/main/res/values-de/strings_logs.xml index eae016b67..867975c66 100644 --- a/manager/src/main/res/values-de/strings_logs.xml +++ b/manager/src/main/res/values-de/strings_logs.xml @@ -57,7 +57,6 @@ Der Daemon hat es abgelehnt, ein neues Protokoll zu beginnen Abbrechen - vector-logs-%1$s.zip Protokolle, Tombstones und dmesg werden gesammelt… Fehlerbericht gespeichert Der Fehlerbericht konnte nicht gespeichert werden diff --git a/manager/src/main/res/values-es/strings_logs.xml b/manager/src/main/res/values-es/strings_logs.xml index 896e2b9fd..949dc3114 100644 --- a/manager/src/main/res/values-es/strings_logs.xml +++ b/manager/src/main/res/values-es/strings_logs.xml @@ -57,7 +57,6 @@ El daemon se negó a empezar un registro nuevo Cancelar - vector-logs-%1$s.zip Recogiendo registros, tombstones y dmesg… Informe de error guardado No se pudo guardar el informe de error diff --git a/manager/src/main/res/values-fa/strings_logs.xml b/manager/src/main/res/values-fa/strings_logs.xml index 144f8aa4a..8fcd99b72 100644 --- a/manager/src/main/res/values-fa/strings_logs.xml +++ b/manager/src/main/res/values-fa/strings_logs.xml @@ -57,7 +57,6 @@ سرویس از آغاز گزارش تازه سر باز زد انصراف - vector-logs-%1$s.zip در حال گردآوری گزارش‌ها، tombstone و dmesg… گزارش اشکال ذخیره شد ذخیرهٔ گزارش اشکال ممکن نشد diff --git a/manager/src/main/res/values-fr/strings_logs.xml b/manager/src/main/res/values-fr/strings_logs.xml index 6b490b410..58fa34fd2 100644 --- a/manager/src/main/res/values-fr/strings_logs.xml +++ b/manager/src/main/res/values-fr/strings_logs.xml @@ -57,7 +57,6 @@ Le démon a refusé de commencer un nouveau journal Annuler - vector-logs-%1$s.zip Collecte des journaux, des tombstones et de dmesg… Rapport de bug enregistré Impossible d\'enregistrer le rapport de bug diff --git a/manager/src/main/res/values-in/strings_logs.xml b/manager/src/main/res/values-in/strings_logs.xml index 8973886d7..d42133eb0 100644 --- a/manager/src/main/res/values-in/strings_logs.xml +++ b/manager/src/main/res/values-in/strings_logs.xml @@ -54,7 +54,6 @@ Daemon menolak memulai log baru Batal - vector-logs-%1$s.zip Mengumpulkan log, tombstone, dan dmesg… Laporan bug tersimpan Laporan bug tidak bisa disimpan diff --git a/manager/src/main/res/values-it/strings_logs.xml b/manager/src/main/res/values-it/strings_logs.xml index c0b6bf1b7..3f6ff977c 100644 --- a/manager/src/main/res/values-it/strings_logs.xml +++ b/manager/src/main/res/values-it/strings_logs.xml @@ -57,7 +57,6 @@ Il daemon si è rifiutato di cominciare un nuovo log Annulla - vector-logs-%1$s.zip Raccolta di log, tombstone e dmesg… Segnalazione di bug salvata Impossibile salvare la segnalazione di bug diff --git a/manager/src/main/res/values-iw/strings_logs.xml b/manager/src/main/res/values-iw/strings_logs.xml index 408a846ab..54ef2ba26 100644 --- a/manager/src/main/res/values-iw/strings_logs.xml +++ b/manager/src/main/res/values-iw/strings_logs.xml @@ -63,7 +63,6 @@ השירות סירב להתחיל יומן חדש ביטול - vector-logs-%1$s.zip אוסף יומנים, קובצי tombstone ו-dmesg… דיווח התקלה נשמר לא ניתן היה לשמור את דיווח התקלה diff --git a/manager/src/main/res/values-ja/strings_logs.xml b/manager/src/main/res/values-ja/strings_logs.xml index 3b3264be4..b3bb02d4c 100644 --- a/manager/src/main/res/values-ja/strings_logs.xml +++ b/manager/src/main/res/values-ja/strings_logs.xml @@ -54,7 +54,6 @@ デーモンが新しいログの開始を拒否しました キャンセル - vector-logs-%1$s.zip ログ・tombstone・dmesg を集めています… 不具合報告を保存しました 不具合報告を保存できませんでした diff --git a/manager/src/main/res/values-ko/strings_logs.xml b/manager/src/main/res/values-ko/strings_logs.xml index 6503610b8..bc78f45be 100644 --- a/manager/src/main/res/values-ko/strings_logs.xml +++ b/manager/src/main/res/values-ko/strings_logs.xml @@ -54,7 +54,6 @@ 데몬이 새 로그 시작을 거부했습니다 취소 - vector-logs-%1$s.zip 로그, tombstone, dmesg를 모으는 중… 버그 신고를 저장했습니다 버그 신고를 저장하지 못했습니다 diff --git a/manager/src/main/res/values-pl/strings_logs.xml b/manager/src/main/res/values-pl/strings_logs.xml index 83ca60450..14defcb4e 100644 --- a/manager/src/main/res/values-pl/strings_logs.xml +++ b/manager/src/main/res/values-pl/strings_logs.xml @@ -63,7 +63,6 @@ Usługa odmówiła rozpoczęcia nowego dziennika Anuluj - vector-logs-%1$s.zip Zbieranie dzienników, tombstone\'ów i dmesg… Zapisano zgłoszenie błędu Nie udało się zapisać zgłoszenia błędu diff --git a/manager/src/main/res/values-pt-rBR/strings_logs.xml b/manager/src/main/res/values-pt-rBR/strings_logs.xml index d0b9814d6..14a6ebd8c 100644 --- a/manager/src/main/res/values-pt-rBR/strings_logs.xml +++ b/manager/src/main/res/values-pt-rBR/strings_logs.xml @@ -57,7 +57,6 @@ O daemon se recusou a começar um registro novo Cancelar - vector-logs-%1$s.zip Reunindo registros, tombstones e dmesg… Relatório de erro salvo Não foi possível salvar o relatório de erro diff --git a/manager/src/main/res/values-ru/strings_logs.xml b/manager/src/main/res/values-ru/strings_logs.xml index ef5f02770..029442ad8 100644 --- a/manager/src/main/res/values-ru/strings_logs.xml +++ b/manager/src/main/res/values-ru/strings_logs.xml @@ -53,7 +53,6 @@ Новый журнал начат Служба отказалась начать новый журнал Отмена - vector-logs-%1$s.zip Сбор журналов, tombstone и dmesg… Отчёт об ошибке сохранён Не удалось сохранить отчёт об ошибке diff --git a/manager/src/main/res/values-tr/strings_logs.xml b/manager/src/main/res/values-tr/strings_logs.xml index 793cfecf3..acbe87a15 100644 --- a/manager/src/main/res/values-tr/strings_logs.xml +++ b/manager/src/main/res/values-tr/strings_logs.xml @@ -57,7 +57,6 @@ Art alan süreci yeni günlük başlatmayı reddetti Vazgeç - vector-logs-%1$s.zip Günlükler, tombstone\'lar ve dmesg toplanıyor… Hata raporu kaydedildi Hata raporu kaydedilemedi diff --git a/manager/src/main/res/values-uk/strings_logs.xml b/manager/src/main/res/values-uk/strings_logs.xml index a02226db1..604d37cf6 100644 --- a/manager/src/main/res/values-uk/strings_logs.xml +++ b/manager/src/main/res/values-uk/strings_logs.xml @@ -63,7 +63,6 @@ Служба відмовилася починати новий журнал Скасувати - vector-logs-%1$s.zip Збирання журналів, tombstone і dmesg… Звіт про помилку збережено Не вдалося зберегти звіт про помилку diff --git a/manager/src/main/res/values-vi/strings_logs.xml b/manager/src/main/res/values-vi/strings_logs.xml index 1802e40e7..40f85239e 100644 --- a/manager/src/main/res/values-vi/strings_logs.xml +++ b/manager/src/main/res/values-vi/strings_logs.xml @@ -54,7 +54,6 @@ Tiến trình nền từ chối bắt đầu nhật ký mới Huỷ - vector-logs-%1$s.zip Đang thu thập nhật ký, tombstone và dmesg… Đã lưu báo cáo lỗi Không lưu được báo cáo lỗi diff --git a/manager/src/main/res/values-zh-rCN/strings_logs.xml b/manager/src/main/res/values-zh-rCN/strings_logs.xml index 37c02e6cd..5fb01539d 100644 --- a/manager/src/main/res/values-zh-rCN/strings_logs.xml +++ b/manager/src/main/res/values-zh-rCN/strings_logs.xml @@ -54,7 +54,6 @@ 守护进程拒绝开始新的日志 取消 - vector-logs-%1$s.zip 正在收集日志、tombstone 和 dmesg… 问题报告已保存 无法保存问题报告 diff --git a/manager/src/main/res/values-zh-rTW/strings_logs.xml b/manager/src/main/res/values-zh-rTW/strings_logs.xml index 52436f609..e24218441 100644 --- a/manager/src/main/res/values-zh-rTW/strings_logs.xml +++ b/manager/src/main/res/values-zh-rTW/strings_logs.xml @@ -54,7 +54,6 @@ 常駐程式拒絕開始新的日誌 取消 - vector-logs-%1$s.zip 正在收集日誌、tombstone 與 dmesg… 問題報告已儲存 無法儲存問題報告 diff --git a/manager/src/main/res/values/strings_logs.xml b/manager/src/main/res/values/strings_logs.xml index e7bc55865..451fe83ff 100644 --- a/manager/src/main/res/values/strings_logs.xml +++ b/manager/src/main/res/values/strings_logs.xml @@ -75,7 +75,6 @@ Cancel - vector-logs-%1$s.zip Collecting logs, tombstones and dmesg… Bug report saved Could not save the bug report