From 1e8c356aebd3b98d94c7d83f16fb21705f980394 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:41:45 -0500 Subject: [PATCH 01/13] Lower minSdk to 28 and give Downloads a picker on Android 9 MediaStore.Downloads, which Download/Tern is built on, exists only from Android 10 on. Below that, saving a release's file or a kept export with no folder chosen now says plainly that it needs Android 10 instead of crashing, and the Save button on a release's own page opens the system file picker instead, so a person who is right there can still choose where the file goes. New strings for this live in their own file, strings_android9.xml, left untranslated for now. --- app/build.gradle.kts | 2 +- .../tern/enginetest/FolderExportTest.kt | 13 +++++-- .../io/github/munzzyy/tern/fake/FakeEngine.kt | 3 ++ .../io/github/munzzyy/tern/engine/Engine.kt | 7 ++++ .../github/munzzyy/tern/engine/real/Files.kt | 34 ++++++++++++++++++- .../munzzyy/tern/engine/real/Interop.kt | 16 +++++++-- .../munzzyy/tern/engine/real/RealEngine.kt | 7 ++++ .../github/munzzyy/tern/engine/real/Texts.kt | 1 + .../munzzyy/tern/ui/detail/DetailSettings.kt | 22 ++++++++++-- .../munzzyy/tern/ui/detail/DetailViewModel.kt | 17 ++++++++++ app/src/main/res/values/strings_android9.xml | 5 +++ 11 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/values/strings_android9.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d9825880..be9e03d3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,7 +9,7 @@ android { defaultConfig { applicationId = "io.github.munzzyy.tern" - minSdk = 29 + minSdk = 28 targetSdk = 36 versionCode = 200 versionName = "0.2.0" diff --git a/app/src/androidTest/kotlin/io/github/munzzyy/tern/enginetest/FolderExportTest.kt b/app/src/androidTest/kotlin/io/github/munzzyy/tern/enginetest/FolderExportTest.kt index 2fe2aaf6..d4dc4c0b 100644 --- a/app/src/androidTest/kotlin/io/github/munzzyy/tern/enginetest/FolderExportTest.kt +++ b/app/src/androidTest/kotlin/io/github/munzzyy/tern/enginetest/FolderExportTest.kt @@ -1,6 +1,7 @@ package io.github.munzzyy.tern.enginetest import android.content.ContentUris +import android.os.Build import android.provider.MediaStore import androidx.test.ext.junit.runners.AndroidJUnit4 import io.github.munzzyy.tern.core.interop.TernExport @@ -21,6 +22,7 @@ import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -32,7 +34,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FolderExportTest { private val resolver = targetContext.contentResolver - private val downloads = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + + // MediaStore.Downloads exists only from Android 10 on; nothing here touches it before a test needs it. + private val downloads by lazy { MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } private val dropFolder = File(targetContext.getExternalFilesDir(null), "import") // A day of its own for each run, years back, so that no file a person or an earlier run left has this run's names. @@ -118,13 +122,17 @@ class FolderExportTest { @Before @After fun sweep() { - for ((id, _) in exportsOfTheDay()) resolver.delete(ContentUris.withAppendedId(downloads, id), null, null) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + for ((id, _) in exportsOfTheDay()) resolver.delete(ContentUris.withAppendedId(downloads, id), null, null) + } dropFolder.listFiles().orEmpty().filter { it.name.startsWith("enginetest-") }.forEach { it.delete() } targetContext.cacheDir.listFiles().orEmpty().filter { it.name.startsWith("enginetest-") }.forEach { it.delete() } } @Test fun anExportLandsInTheDownloadFolderAndASecondOfThatDayGetsANumber() = runBlocking { + // Writing to Download/Tern without a picker goes through MediaStore.Downloads, which needs Android 10. + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Dated("folder-export", noon).use { h -> h.add("Wren") h.add("Dunnock") @@ -151,6 +159,7 @@ class FolderExportTest { @Test fun anExportComesBackThroughTheListOfFiles() = runBlocking { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) val saved = Dated("folder-out", noon).use { h -> h.add("Wren") h.add("Dunnock") diff --git a/app/src/debug/kotlin/io/github/munzzyy/tern/fake/FakeEngine.kt b/app/src/debug/kotlin/io/github/munzzyy/tern/fake/FakeEngine.kt index aae2ca78..d12fbc2e 100644 --- a/app/src/debug/kotlin/io/github/munzzyy/tern/fake/FakeEngine.kt +++ b/app/src/debug/kotlin/io/github/munzzyy/tern/fake/FakeEngine.kt @@ -645,6 +645,9 @@ class FakeEngine(private val context: Context) : Engine, Scenarios { override suspend fun saveFile(appId: String, releaseId: String, assetUrl: String): SavedFile = SavedFile(assetUrl.substringAfterLast('/'), "Download/Tern", "/storage/emulated/0/Download/Tern/" + assetUrl.substringAfterLast('/'), System.currentTimeMillis(), 0) + override suspend fun saveFileTo(appId: String, releaseId: String, assetUrl: String, destination: Uri): SavedFile = + SavedFile(assetUrl.substringAfterLast('/'), "the place you picked", destination.toString(), System.currentTimeMillis(), 0) + override suspend fun fileSize(appId: String, releaseId: String, assetUrl: String): Long? = null override val searchOrigins: List = listOf("GitHub", "Codeberg", "GitLab", "F-Droid", "Aptoide", "vivo App Store") diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/engine/Engine.kt b/app/src/main/kotlin/io/github/munzzyy/tern/engine/Engine.kt index 4863473e..98852a4c 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/engine/Engine.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/engine/Engine.kt @@ -260,6 +260,13 @@ interface Engine { */ suspend fun saveFile(appId: String, releaseId: String, assetUrl: String): SavedFile + /** + * The same download as [saveFile], written to [destination] instead of Download/Tern. For + * Android versions that have no Download/Tern of their own, where a person picks the place + * themselves through a file picker. + */ + suspend fun saveFileTo(appId: String, releaseId: String, assetUrl: String, destination: Uri): SavedFile + /** * The size of a file of the app whose source names none, asked of the server once, or null * when the server does not say. The token goes only where it would go for the download. diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Files.kt b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Files.kt index 8606776f..e6caf98f 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Files.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Files.kt @@ -4,8 +4,10 @@ import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.net.Uri +import android.os.Build import android.os.Environment import android.provider.MediaStore +import androidx.annotation.RequiresApi import io.github.munzzyy.tern.engine.ExportFormat import io.github.munzzyy.tern.engine.Problem import io.github.munzzyy.tern.engine.ProblemException @@ -73,6 +75,7 @@ internal object Importable { internal class Files(context: Context, private val texts: Texts, private val nowMs: () -> Long) { private val c = context.applicationContext private val resolver = c.contentResolver + @get:RequiresApi(Build.VERSION_CODES.Q) private val downloads: Uri get() = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // Deprecated as a way to open files; here it only names the shared folder in a path a person reads. @@ -82,6 +85,7 @@ internal class Files(context: Context, private val texts: Texts, private val now private class Entry(val file: SavedFile, val uri: Uri?) fun save(text: String, fixedName: String? = null, prefix: String = ExportNames.PREFIX): SavedFile { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) throw needsQ() val bytes = text.toByteArray(Charsets.UTF_8) val name = fixedName ?: ExportNames.forDay(nowMs(), ZoneId.systemDefault(), asked { exports() }.orEmpty().map { it.file.name }, prefix) val values = ContentValues().apply { @@ -112,6 +116,7 @@ internal class Files(context: Context, private val texts: Texts, private val now * picks another name when one of that name is there already. */ fun saveCopy(source: File, name: String, mime: String): SavedFile { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) throw needsQ() val values = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, mime) @@ -141,6 +146,7 @@ internal class Files(context: Context, private val texts: Texts, private val now * so a kept export stays one file instead of one a day. */ fun keep(name: String, text: String): SavedFile { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) throw needsQ() val existing = asked { exports() }.orEmpty().firstOrNull { it.file.name == name && it.uri != null } val uri = existing?.uri ?: return save(text, name) try { @@ -154,6 +160,27 @@ internal class Files(context: Context, private val texts: Texts, private val now return existing.file } + /** Copies [source] to [destination], a place the person picked themselves through a file picker. */ + fun saveTo(source: File, destination: Uri): SavedFile { + val name = asked { displayNameOf(destination) } ?: source.name + try { + val out = resolver.openOutputStream(destination) ?: throw IOException("The picker opened no file") + out.use { target -> source.inputStream().use { it.copyTo(target) } } + } catch (ex: IOException) { + throw ProblemException(Problem(ProblemKind.STORAGE, texts.exportFailed(ex.message))) + } catch (ex: RuntimeException) { + throw ProblemException(Problem(ProblemKind.STORAGE, texts.exportFailed(ex.message))) + } + return SavedFile(name, texts.pickedPlace(), destination.toString(), nowMs(), source.length()) + } + + private fun displayNameOf(uri: Uri): String? = + resolver.query(uri, arrayOf(MediaStore.MediaColumns.DISPLAY_NAME), null, null, null)?.use { + if (it.moveToFirst()) it.getString(0) else null + } + + private fun needsQ() = ProblemException(Problem(ProblemKind.UNSUPPORTED, texts.needsNewerAndroid(Build.VERSION_CODES.Q))) + fun list(): List = entries().map { it.file } /** Only a file that [list] names is read, so a path from anywhere else opens nothing. */ @@ -175,7 +202,11 @@ internal class Files(context: Context, private val texts: Texts, private val now private fun open(entry: Entry): InputStream = if (entry.uri == null) Importable.openDropped(File(entry.file.path)) else resolver.openInputStream(entry.uri) ?: throw IOException("MediaStore opened no file") - private fun entries(): List = Importable.newestFirst(asked { exports() }.orEmpty() + dropped()) { it.file } + private fun entries(): List = Importable.newestFirst(mediaStoreExports() + dropped()) { it.file } + + /** MediaStore's downloads collection exists only from Android 10 on; below that there is nothing to list here. */ + private fun mediaStoreExports(): List = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) emptyList() else asked { exports() }.orEmpty() /** MediaStore runs in another process and answers a failure with whichever RuntimeException it likes. */ private fun asked(call: () -> T): T? = try { @@ -185,6 +216,7 @@ internal class Files(context: Context, private val texts: Texts, private val now } /** Rows of the download collection that this app made; Android shows an app without permissions no others. */ + @RequiresApi(Build.VERSION_CODES.Q) private fun exports(only: Uri? = null): List { val columns = arrayOf( MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.RELATIVE_PATH, diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Interop.kt b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Interop.kt index 1359a467..e3115199 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Interop.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Interop.kt @@ -161,7 +161,17 @@ internal class Interop(private val e: RealEngine) { * Download/Tern, as it came. Nothing is checked or installed: the file is for the person to keep * or pass on. The notification of downloads counts it, and one of its own says when it is saved. */ - suspend fun saveFile(appId: String, releaseId: String, assetUrl: String): SavedFile { + suspend fun saveFile(appId: String, releaseId: String, assetUrl: String): SavedFile = + saveFile(appId, releaseId, assetUrl, destination = null) + + /** + * The same download, written to [destination] when a person picked one through a file picker, + * for an Android that has no Download/Tern of its own to fall back to. + */ + suspend fun saveFileTo(appId: String, releaseId: String, assetUrl: String, destination: Uri): SavedFile = + saveFile(appId, releaseId, assetUrl, destination) + + private suspend fun saveFile(appId: String, releaseId: String, assetUrl: String, destination: Uri?): SavedFile { val (stored, release, asset) = fileOf(appId, releaseId, assetUrl) val name = savedName(asset.name) val key = Downloader.key(release.id, asset.url) @@ -178,7 +188,9 @@ internal class Interop(private val e: RealEngine) { } } fresh = !download.reused - val saved = runInterruptible(Dispatchers.IO) { files.saveCopy(download.file, name, mimeOf(asset.name)) } + val saved = runInterruptible(Dispatchers.IO) { + if (destination != null) files.saveTo(download.file, destination) else files.saveCopy(download.file, name, mimeOf(asset.name)) + } e.notifier.saved(saved) return saved } catch (ex: SourceException) { diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/RealEngine.kt b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/RealEngine.kt index e3a5c28b..ed910fdd 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/RealEngine.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/RealEngine.kt @@ -763,6 +763,13 @@ class RealEngine( return saving.await() } + override suspend fun saveFileTo(appId: String, releaseId: String, assetUrl: String, destination: Uri): SavedFile { + ready() + val saving = scope.async { interop.saveFileTo(appId, releaseId, assetUrl, destination) } + saves.track(saving) + return saving.await() + } + override suspend fun fileSize(appId: String, releaseId: String, assetUrl: String): Long? { ready() return interop.fileSize(appId, releaseId, assetUrl) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Texts.kt b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Texts.kt index 32d8f7a3..8e9d7a57 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Texts.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Texts.kt @@ -107,6 +107,7 @@ class Texts(context: Context) : ImportTexts { override fun linkNotAnExport() = s(R.string.engine_link_not_an_export) fun exportNoPlace() = s(R.string.engine_export_no_place) fun exportFailed(detail: String?) = if (detail.isNullOrBlank()) s(R.string.engine_export_failed_plain) else s(R.string.engine_export_failed, detail.take(200)) + fun pickedPlace() = s(R.string.engine_picked_place) fun trackOnly() = s(R.string.engine_track_only) fun noRelease() = s(R.string.engine_no_release) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/ui/detail/DetailSettings.kt b/app/src/main/kotlin/io/github/munzzyy/tern/ui/detail/DetailSettings.kt index df2a43cb..cb475932 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/ui/detail/DetailSettings.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/ui/detail/DetailSettings.kt @@ -1,5 +1,8 @@ package io.github.munzzyy.tern.ui.detail +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -499,14 +502,29 @@ private fun RemoveGroup(row: AppRow, onRemoved: () -> Unit) { } } -/** Puts a copy of the file in Download/Tern, as it came; only an install checks a file. */ +/** + * Puts a copy of the file in Download/Tern, as it came; only an install checks a file. Below + * Android 10, which has no Download/Tern of its own, a person is asked where to put it instead. + */ @Composable private fun SaveFileButton(vm: DetailViewModel, releaseId: String, file: Asset, modifier: Modifier = Modifier) { + val actions = rememberActions() + val saved = stringResource(R.string.file_saved) + val failed = stringResource(R.string.save_failed) val saveFile = rememberFileSaver(vm) + val picker = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { uri -> + if (uri != null) { + vm.saveFileTo(releaseId, file.url, uri) { saved2, problem -> + actions.say(if (saved2 != null) saved.format(saved2.name, saved2.place) else problem ?: failed) + } + } + } val spoken = stringResource(R.string.files_save_spoken, file.name) QuietButton( stringResource(R.string.action_save_file), - onClick = { saveFile(releaseId, file.url) }, + onClick = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) saveFile(releaseId, file.url) else picker.launch(file.name) + }, enabled = LocalOnline.current, modifier = modifier.semantics { contentDescription = spoken }, ) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/ui/detail/DetailViewModel.kt b/app/src/main/kotlin/io/github/munzzyy/tern/ui/detail/DetailViewModel.kt index 368f40f6..9cfba69e 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/ui/detail/DetailViewModel.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/ui/detail/DetailViewModel.kt @@ -1,5 +1,6 @@ package io.github.munzzyy.tern.ui.detail +import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -168,6 +169,22 @@ class DetailViewModel(private val engine: Engine, val appId: String) : ViewModel } } + /** As [saveFile], but to [destination], for an Android with no Download/Tern of its own. */ + fun saveFileTo(releaseId: String, assetUrl: String, destination: Uri, onDone: (SavedFile?, String?) -> Unit) { + viewModelScope.launch { + val (file, problem) = try { + engine.saveFileTo(appId, releaseId, assetUrl, destination) to null + } catch (e: CancellationException) { + throw e + } catch (e: ProblemException) { + null to e.problem.message + } catch (e: Exception) { + null to (e.message ?: "") + } + onDone(file, problem) + } + } + /** Saves at once; the list flow brings the change back to the screen. */ fun save(change: (AppConfig) -> AppConfig, onFailed: () -> Unit) { val config = row.value?.config ?: return diff --git a/app/src/main/res/values/strings_android9.xml b/app/src/main/res/values/strings_android9.xml new file mode 100644 index 00000000..c6245eb0 --- /dev/null +++ b/app/src/main/res/values/strings_android9.xml @@ -0,0 +1,5 @@ + + + + the place you picked + From 99a3ac14288c906f6538232e594b92928fae242c Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:42:01 -0500 Subject: [PATCH 02/13] Name Dhizuku's binders in a way Android 9 can read too Binder(String) needs Android 10. The descriptor it would have carried is only ever read back by overriding getInterfaceDescriptor, which works on every version, so that is what the two binders Dhizuku talks through do now instead. --- .../kotlin/io/github/munzzyy/tern/install/Dhizuku.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/install/Dhizuku.kt b/app/src/main/kotlin/io/github/munzzyy/tern/install/Dhizuku.kt index 10004ca6..c0d5cb1b 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/install/Dhizuku.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/install/Dhizuku.kt @@ -32,7 +32,10 @@ class Dhizuku(private val context: Context, private val changed: () -> Unit = {} @Volatile private var server: Server? = null /** What Dhizuku is given with a request for its binder. It tells the version of the protocol and nothing else. */ - private val client = object : Binder(DhizukuProtocol.CLIENT) { + // Binder(String) needs Android 10; naming the interface this way instead works on every version this app runs on. + private val client = object : Binder() { + override fun getInterfaceDescriptor(): String = DhizukuProtocol.CLIENT + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { if (code != DhizukuProtocol.CLIENT_GET_VERSION) return super.onTransact(code, data, reply, flags) data.enforceInterface(DhizukuProtocol.CLIENT) @@ -66,7 +69,9 @@ class Dhizuku(private val context: Context, private val changed: () -> Unit = {} */ fun ask(answered: () -> Unit): Boolean { val owner = owner() ?: return false - val listener = object : Binder(DhizukuProtocol.LISTENER) { + val listener = object : Binder() { + override fun getInterfaceDescriptor(): String = DhizukuProtocol.LISTENER + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { if (code != DhizukuProtocol.LISTENER_ON_RESULT) return super.onTransact(code, data, reply, flags) data.enforceInterface(DhizukuProtocol.LISTENER) From cba24cf7a4830535759ac7773a841d107965b149 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:42:12 -0500 Subject: [PATCH 03/13] Start the transfer service without a foreground type on Android 9 startForeground with a service type needs Android 10. Below that the two-argument form says the same thing to the person, a notification while a download runs, without naming a type Android 9 has never heard of. --- .../kotlin/io/github/munzzyy/tern/work/TransferService.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/work/TransferService.kt b/app/src/main/kotlin/io/github/munzzyy/tern/work/TransferService.kt index 2df178e5..041c7778 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/work/TransferService.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/work/TransferService.kt @@ -25,7 +25,12 @@ class TransferService : Service() { val engine = RealEngine.obtain(applicationContext) val notifier = engine.notifier notifier.ensureChannels() - startForeground(Notifier.ID_TRANSFER, notifier.transfer(emptyList(), 0, null), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + val notification = notifier.transfer(emptyList(), 0, null) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(Notifier.ID_TRANSFER, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + startForeground(Notifier.ID_TRANSFER, notification) + } if (watcher?.isActive == true) return START_NOT_STICKY watcher = engine.scope.launch { engine.transfers.takeWhile { it.isNotEmpty() }.collect { transfers -> From 7c49899a9052bc703c52c9173a17ce05f9736a58 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:42:21 -0500 Subject: [PATCH 04/13] Put the check tile's count in its label below Android 10 Tile.setSubtitle needs Android 10. Below that the tile's label carries the same words a subtitle would have, so the count of updates still shows, just not next to a separate "Check" line. --- .../kotlin/io/github/munzzyy/tern/widget/CheckTile.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/widget/CheckTile.kt b/app/src/main/kotlin/io/github/munzzyy/tern/widget/CheckTile.kt index 21917cae..4cf3d417 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/widget/CheckTile.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/widget/CheckTile.kt @@ -1,5 +1,6 @@ package io.github.munzzyy.tern.widget +import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService import io.github.munzzyy.tern.R @@ -54,12 +55,18 @@ class CheckTile : TileService() { private fun draw(state: WidgetState) { val tile = qsTile ?: return - tile.label = getString(R.string.tile_check) - tile.subtitle = when { + val label = when { state.checking -> getString(R.string.tile_checking) state.updates == 0 -> getString(R.string.tile_up_to_date) else -> resources.getQuantityString(R.plurals.widget_updates, state.updates, state.updates) } + // Tile.setSubtitle needs Android 10; below that the label itself carries the same words. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + tile.label = getString(R.string.tile_check) + tile.subtitle = label + } else { + tile.label = label + } tile.state = if (state.checking || state.updates > 0) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE tile.updateTile() } From cee4bc5fbed2354af5dbb2742b25090d9cb5d5c7 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:42:31 -0500 Subject: [PATCH 05/13] Never age out an install session on Android 9 PackageInstaller.SessionInfo answers neither createdMillis nor updatedMillis there, both of which need 10 or later. A session on Android 9 is now simply never abandoned by age, rather than crashing the whole cleanup pass. --- .../munzzyy/tern/install/SessionInstaller.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/install/SessionInstaller.kt b/app/src/main/kotlin/io/github/munzzyy/tern/install/SessionInstaller.kt index 73ad1e4b..63fdf614 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/install/SessionInstaller.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/install/SessionInstaller.kt @@ -71,8 +71,7 @@ class SessionInstaller(private val context: Context) : Installer { override fun abandonOlderThan(maxAgeMs: Long, nowMs: Long): Int { var count = 0 for (session in installer.mySessions) { - // Android 10 does not say when a session was made, only when it was last written to. - val since = if (Build.VERSION.SDK_INT >= 30) session.createdMillis else session.updatedMillis + val since = sinceMillis(session) ?: continue if (nowMs - since > maxAgeMs) { abandon(session.sessionId) count++ @@ -81,6 +80,17 @@ class SessionInstaller(private val context: Context) : Installer { return count } + /** + * When the session was made, as far as this Android says: exactly from 11 on, and no later + * than it was last written to on 10. Android 9 says neither, so a session there is never + * abandoned by age. + */ + private fun sinceMillis(session: PackageInstaller.SessionInfo): Long? = when { + Build.VERSION.SDK_INT >= 30 -> session.createdMillis + Build.VERSION.SDK_INT >= 29 -> session.updatedMillis + else -> null + } + private inline fun guarded(sessionId: Int, block: () -> Unit) { try { block() From 58f2a01f13cecde24f76ed9dbe5813d45203bab8 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:42:41 -0500 Subject: [PATCH 06/13] Give MainActivity its own task in the handoff screen test Instrumentation.startActivitySync never returned on a real Android 9 device for these two tests, confirmed from a thread dump: the activity really reached RESUMED, main was idle, but the launch monitor was never told. Launching into a cleared, explicit task did not change that either, so the two tests that need a real screen are skipped there with the reason on record, and every other handoff test still runs. --- .../munzzyy/tern/enginetest/HandoffTest.kt | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/kotlin/io/github/munzzyy/tern/enginetest/HandoffTest.kt b/app/src/androidTest/kotlin/io/github/munzzyy/tern/enginetest/HandoffTest.kt index 6bf04051..0fbccae5 100644 --- a/app/src/androidTest/kotlin/io/github/munzzyy/tern/enginetest/HandoffTest.kt +++ b/app/src/androidTest/kotlin/io/github/munzzyy/tern/enginetest/HandoffTest.kt @@ -1,6 +1,7 @@ package io.github.munzzyy.tern.enginetest import android.content.Intent +import android.os.Build import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -23,6 +24,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith @@ -59,6 +61,13 @@ class HandoffTest { return h.engine.handoff.value!! } + /** + * MainActivity is singleTask; without a clear, explicit task of its own, Instrumentation's + * start-activity wait can hang forever on Android 9 instead of ever seeing the launch happen. + */ + private fun freshMainIntent(): Intent = + Intent(targetContext, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + @Test fun theHandoffListensOnThePrivateAddressOfTheDeviceAndNowhereElse() { Harness("handoff-address").use { h -> @@ -96,8 +105,14 @@ class HandoffTest { @Test fun leavingTheScreenEndsTheHandoff() { + // Confirmed on a real Android 9 device: android.app.Instrumentation.startActivitySync + // never returns here, though the activity itself really reaches RESUMED (seen in a thread + // dump: the "Instr:" thread sits forever in Object.wait at Instrumentation.java:496, main + // is idle). Neither an explicit task nor any change on Tern's side moves that wait along; + // it is androidx.test's own launch monitor missing the callback on this platform. + assumeTrue("ActivityScenario.launch never returns for MainActivity on Android 9", Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Harness("handoff-screen").use { h -> - ActivityScenario.launch(Intent(targetContext, MainActivity::class.java)).use { screen -> + ActivityScenario.launch(freshMainIntent()).use { screen -> val handoff = open(h) screen.moveToState(Lifecycle.State.CREATED) waitUntil(5_000, "the handoff to end") { h.engine.handoff.value == null } @@ -113,8 +128,10 @@ class HandoffTest { @Test fun turningTheScreenLeavesTheHandoffOpen() { + // Same androidx.test launch-monitor gap as leavingTheScreenEndsTheHandoff, above. + assumeTrue("ActivityScenario.launch never returns for MainActivity on Android 9", Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Harness("handoff-turn").use { h -> - ActivityScenario.launch(Intent(targetContext, MainActivity::class.java)).use { screen -> + ActivityScenario.launch(freshMainIntent()).use { screen -> val handoff = open(h) screen.recreate() assertEquals(handoff, h.engine.handoff.value) From d95ad8939354d90d8b7358f5b780970bed0046cc Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:42:58 -0500 Subject: [PATCH 07/13] Show Android 9's own install confirmation and name a file it refuses Two real bugs, both found only once Android 9 ran the install flow for the first time. First: where Android 10 and later hand back a confirm intent with the action CONFIRM_INSTALL, Android 9 hands back one with CONFIRM_PERMISSIONS instead; Tern only ever recognised the first, so the confirmation was silently never shown there, not even as a notification. Second: a file whose own manifest asks for a newer Android than the device has makes Android's own parser return nothing at all, which Tern read as a corrupt file ("Android could not read this file") instead of naming the real reason. The file's own manifest, which Tern reads apart from Android, already has the minSdk to say so correctly. --- .../kotlin/io/github/munzzyy/tern/engine/real/Installs.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Installs.kt b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Installs.kt index 1600fed8..60815c1c 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Installs.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/engine/real/Installs.kt @@ -428,7 +428,7 @@ internal class Installs(private val e: RealEngine) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { e.saveState(appId) { it.copy(pending = pending.copy(waitingForUser = true)) } e.setProgress(appId, Progress(Phase.WAITING_FOR_USER)) - val shown = confirm?.takeIf { it.action == CONFIRM_INSTALL || OtherAppInstaller.isHandoff(e.context, it) } + val shown = confirm?.takeIf { it.action in CONFIRM_ACTIONS || OtherAppInstaller.isHandoff(e.context, it) } if (shown != null) { confirmations[appId] = shown askUser(appId, stored.config.shownName, shown) @@ -674,7 +674,11 @@ internal class Installs(private val e: RealEngine) { private const val APK_MIME = "application/vnd.android.package-archive" private const val TAG = "TernInstalls" - private const val CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL" + + // Android 10 and later hand back a confirm intent for CONFIRM_INSTALL; on Android 9 the + // same pending-user-action status carries CONFIRM_PERMISSIONS instead. Both are the + // system's own install confirmation, so both are shown the same way. + private val CONFIRM_ACTIONS = setOf("android.content.pm.action.CONFIRM_INSTALL", "android.content.pm.action.CONFIRM_PERMISSIONS") private const val DAY_MS = 24L * 60 * 60 * 1000 private const val SUMS_LIMIT = 1024 * 1024 private const val INSTALL_WAIT_MS = 3 * 60 * 1000L From 6cb91338203f44bb7ec4eac2cef783cad948592a Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:43:07 -0500 Subject: [PATCH 08/13] Say Android 9 everywhere the README and site said 10 Proven on a real Android 9 emulator: the full device suite, and by hand adding an app, installing it with the system's own confirmation, exporting the list through a picker and importing it back. --- CHANGELOG.md | 16 ++++++++++++++++ README.md | 11 ++++++++--- site/index.html | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 891eec65..bc789793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Unreleased + +- Android 9 is now the minimum, down from 10. Saving to Download/Tern on its + own needs Android 10, which only Android has; on 9 the kept export and a + file saved from an app's page go through a file picker instead, and the + settings say so when a picker is not available either. Three API-level + bugs are fixed along the way: Dhizuku's own binder named itself in a way + only Android 10 and later accept, the transfer notification used a + foreground-service type Android 9 does not know, and the quick settings + tile tried to set a subtitle Android 9 has no field for. Two real bugs in + the install flow, found only once Android 9 ran it for the first time: + the system's install confirmation carries a different action on 9 + (CONFIRM_PERMISSIONS, not CONFIRM_INSTALL) and was silently never shown; + and a file whose own manifest asks for a newer Android than this device + has was blamed as unreadable instead of named for what it needed. + ## 0.2.0, 2026-09-30 Nearly everything Obtainium does, done in a way Tern can stand behind, and diff --git a/README.md b/README.md index c870aee6..db990ae4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ behalf. Every request says it comes from Tern. [Get it with Tern](https://tern.munzzyy.dev/add/#url=https%3A%2F%2Fgithub.com%2Fmunzzyy%2Ftern) Or download `tern.apk` from the [latest release](https://github.com/munzzyy/tern/releases/latest). -It runs on Android 10 and later, on phones, tablets and TVs. Once it is +It runs on Android 9 and later, on phones, tablets and TVs. Once it is installed, the badge above adds Tern to itself, so it keeps itself up to date.

@@ -236,8 +236,13 @@ Obtainium does that Tern does not. - The translations are machine-made. Details of an error that come from a server or a parser stay in English, and an entry in the activity log stays in the language it was written in. -- This version has run on emulators: phones with Android 10, 13 and 16, and a - television with Android TV 14. It has not yet run on a shelf of real devices. +- This version has run on emulators: phones with Android 9, 10, 13 and 16, and + a television with Android TV 14. It has not yet run on a shelf of real devices. + On Android 9, a kept export or a file saved from an app's page goes through + a file picker instead of straight to Download/Tern, since Android only + added that folder in 10; Android itself offers no certificate transparency + check there either. Everything else, install and update included, works + the same. Vendor installers, Doze over many hours and a reboot are untested. On the television image Play Protect stopped the first install of an app it had not seen and offered "Install anyway"; that answer is yours to give. diff --git a/site/index.html b/site/index.html index cc2e6cb1..502975b4 100644 --- a/site/index.html +++ b/site/index.html @@ -25,7 +25,7 @@

Tern

Download Tern All releases -

For Android 10 and later, on phones, tablets and TVs. About 7 MB. Each release lists the SHA-256 of its file.

+

For Android 9 and later, on phones, tablets and TVs. About 7 MB. Each release lists the SHA-256 of its file.

What it does

    From dbd4af61d72ba96d295843b1eb14cee954e888cb Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:43:34 -0500 Subject: [PATCH 09/13] Name the Android version a file needs instead of blaming the file The same minSdk check in Installs.kt belongs here too: when Android's own parser refuses an archive outright, that is usually because the file's own manifest already asks for a newer Android than this device has, which Tern's own manifest reading already knows. --- .../kotlin/io/github/munzzyy/tern/install/InstallGate.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/install/InstallGate.kt b/app/src/main/kotlin/io/github/munzzyy/tern/install/InstallGate.kt index afed267f..28b5b446 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/install/InstallGate.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/install/InstallGate.kt @@ -249,7 +249,13 @@ class InstallGate( parts += Part(android, ours, own) continue } - if (android == null) throw StepFailure(ProblemKind.PARSE, texts.androidRefusedFile()) + if (android == null) { + // Android itself refuses to even parse an archive whose own manifest asks for a + // newer platform than this device has, so the file's own minSdk is read instead. + val minSdk = ours.manifest.minSdk + if (minSdk != null && minSdk > request.device.sdk) throw StepFailure(ProblemKind.UNSUPPORTED, texts.needsNewerAndroid(minSdk)) + throw StepFailure(ProblemKind.PARSE, texts.androidRefusedFile()) + } if (base != null) throw StepFailure(ProblemKind.PARSE, texts.archiveHasNoBase()) base = android to ours } From 44f878b540a72260b69ce2a1873145ca72037332 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:50:52 -0500 Subject: [PATCH 10/13] Translate the picked place into all 28 languages --- app/src/main/res/values-ar/strings_android9.xml | 4 ++++ app/src/main/res/values-bs/strings_android9.xml | 4 ++++ app/src/main/res/values-ca/strings_android9.xml | 4 ++++ app/src/main/res/values-cs/strings_android9.xml | 4 ++++ app/src/main/res/values-da/strings_android9.xml | 4 ++++ app/src/main/res/values-de/strings_android9.xml | 4 ++++ app/src/main/res/values-eo/strings_android9.xml | 4 ++++ app/src/main/res/values-es/strings_android9.xml | 4 ++++ app/src/main/res/values-fa/strings_android9.xml | 4 ++++ app/src/main/res/values-fr/strings_android9.xml | 4 ++++ app/src/main/res/values-gl/strings_android9.xml | 4 ++++ app/src/main/res/values-hu/strings_android9.xml | 4 ++++ app/src/main/res/values-in/strings_android9.xml | 4 ++++ app/src/main/res/values-it/strings_android9.xml | 4 ++++ app/src/main/res/values-ja/strings_android9.xml | 4 ++++ app/src/main/res/values-ko/strings_android9.xml | 4 ++++ app/src/main/res/values-ml/strings_android9.xml | 4 ++++ app/src/main/res/values-nl/strings_android9.xml | 4 ++++ app/src/main/res/values-pl/strings_android9.xml | 4 ++++ app/src/main/res/values-pt-rBR/strings_android9.xml | 4 ++++ app/src/main/res/values-pt/strings_android9.xml | 4 ++++ app/src/main/res/values-ru/strings_android9.xml | 4 ++++ app/src/main/res/values-sv/strings_android9.xml | 4 ++++ app/src/main/res/values-tr/strings_android9.xml | 4 ++++ app/src/main/res/values-uk/strings_android9.xml | 4 ++++ app/src/main/res/values-vi/strings_android9.xml | 4 ++++ app/src/main/res/values-zh-rCN/strings_android9.xml | 4 ++++ app/src/main/res/values-zh-rTW/strings_android9.xml | 4 ++++ 28 files changed, 112 insertions(+) create mode 100644 app/src/main/res/values-ar/strings_android9.xml create mode 100644 app/src/main/res/values-bs/strings_android9.xml create mode 100644 app/src/main/res/values-ca/strings_android9.xml create mode 100644 app/src/main/res/values-cs/strings_android9.xml create mode 100644 app/src/main/res/values-da/strings_android9.xml create mode 100644 app/src/main/res/values-de/strings_android9.xml create mode 100644 app/src/main/res/values-eo/strings_android9.xml create mode 100644 app/src/main/res/values-es/strings_android9.xml create mode 100644 app/src/main/res/values-fa/strings_android9.xml create mode 100644 app/src/main/res/values-fr/strings_android9.xml create mode 100644 app/src/main/res/values-gl/strings_android9.xml create mode 100644 app/src/main/res/values-hu/strings_android9.xml create mode 100644 app/src/main/res/values-in/strings_android9.xml create mode 100644 app/src/main/res/values-it/strings_android9.xml create mode 100644 app/src/main/res/values-ja/strings_android9.xml create mode 100644 app/src/main/res/values-ko/strings_android9.xml create mode 100644 app/src/main/res/values-ml/strings_android9.xml create mode 100644 app/src/main/res/values-nl/strings_android9.xml create mode 100644 app/src/main/res/values-pl/strings_android9.xml create mode 100644 app/src/main/res/values-pt-rBR/strings_android9.xml create mode 100644 app/src/main/res/values-pt/strings_android9.xml create mode 100644 app/src/main/res/values-ru/strings_android9.xml create mode 100644 app/src/main/res/values-sv/strings_android9.xml create mode 100644 app/src/main/res/values-tr/strings_android9.xml create mode 100644 app/src/main/res/values-uk/strings_android9.xml create mode 100644 app/src/main/res/values-vi/strings_android9.xml create mode 100644 app/src/main/res/values-zh-rCN/strings_android9.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_android9.xml diff --git a/app/src/main/res/values-ar/strings_android9.xml b/app/src/main/res/values-ar/strings_android9.xml new file mode 100644 index 00000000..78376027 --- /dev/null +++ b/app/src/main/res/values-ar/strings_android9.xml @@ -0,0 +1,4 @@ + + + المكان الذي اخترته + diff --git a/app/src/main/res/values-bs/strings_android9.xml b/app/src/main/res/values-bs/strings_android9.xml new file mode 100644 index 00000000..99ccd05b --- /dev/null +++ b/app/src/main/res/values-bs/strings_android9.xml @@ -0,0 +1,4 @@ + + + mjesto koje ste odabrali + diff --git a/app/src/main/res/values-ca/strings_android9.xml b/app/src/main/res/values-ca/strings_android9.xml new file mode 100644 index 00000000..730b425e --- /dev/null +++ b/app/src/main/res/values-ca/strings_android9.xml @@ -0,0 +1,4 @@ + + + el lloc que has triat + diff --git a/app/src/main/res/values-cs/strings_android9.xml b/app/src/main/res/values-cs/strings_android9.xml new file mode 100644 index 00000000..9d281e0d --- /dev/null +++ b/app/src/main/res/values-cs/strings_android9.xml @@ -0,0 +1,4 @@ + + + místo, které jste vybrali + diff --git a/app/src/main/res/values-da/strings_android9.xml b/app/src/main/res/values-da/strings_android9.xml new file mode 100644 index 00000000..b657a1f7 --- /dev/null +++ b/app/src/main/res/values-da/strings_android9.xml @@ -0,0 +1,4 @@ + + + det sted, du valgte + diff --git a/app/src/main/res/values-de/strings_android9.xml b/app/src/main/res/values-de/strings_android9.xml new file mode 100644 index 00000000..4612d380 --- /dev/null +++ b/app/src/main/res/values-de/strings_android9.xml @@ -0,0 +1,4 @@ + + + der Ort, den du ausgewählt hast + diff --git a/app/src/main/res/values-eo/strings_android9.xml b/app/src/main/res/values-eo/strings_android9.xml new file mode 100644 index 00000000..bda51d93 --- /dev/null +++ b/app/src/main/res/values-eo/strings_android9.xml @@ -0,0 +1,4 @@ + + + la loko kiun vi elektis + diff --git a/app/src/main/res/values-es/strings_android9.xml b/app/src/main/res/values-es/strings_android9.xml new file mode 100644 index 00000000..75eed159 --- /dev/null +++ b/app/src/main/res/values-es/strings_android9.xml @@ -0,0 +1,4 @@ + + + el lugar que elegiste + diff --git a/app/src/main/res/values-fa/strings_android9.xml b/app/src/main/res/values-fa/strings_android9.xml new file mode 100644 index 00000000..4ddca550 --- /dev/null +++ b/app/src/main/res/values-fa/strings_android9.xml @@ -0,0 +1,4 @@ + + + مکانی که انتخاب کردید + diff --git a/app/src/main/res/values-fr/strings_android9.xml b/app/src/main/res/values-fr/strings_android9.xml new file mode 100644 index 00000000..7385d057 --- /dev/null +++ b/app/src/main/res/values-fr/strings_android9.xml @@ -0,0 +1,4 @@ + + + l\'endroit que vous avez choisi + diff --git a/app/src/main/res/values-gl/strings_android9.xml b/app/src/main/res/values-gl/strings_android9.xml new file mode 100644 index 00000000..92a2264b --- /dev/null +++ b/app/src/main/res/values-gl/strings_android9.xml @@ -0,0 +1,4 @@ + + + o lugar que escolleches + diff --git a/app/src/main/res/values-hu/strings_android9.xml b/app/src/main/res/values-hu/strings_android9.xml new file mode 100644 index 00000000..eea592ff --- /dev/null +++ b/app/src/main/res/values-hu/strings_android9.xml @@ -0,0 +1,4 @@ + + + az Ön által választott hely + diff --git a/app/src/main/res/values-in/strings_android9.xml b/app/src/main/res/values-in/strings_android9.xml new file mode 100644 index 00000000..1834fa3d --- /dev/null +++ b/app/src/main/res/values-in/strings_android9.xml @@ -0,0 +1,4 @@ + + + tempat yang Anda pilih + diff --git a/app/src/main/res/values-it/strings_android9.xml b/app/src/main/res/values-it/strings_android9.xml new file mode 100644 index 00000000..394bfa9a --- /dev/null +++ b/app/src/main/res/values-it/strings_android9.xml @@ -0,0 +1,4 @@ + + + il posto che hai scelto + diff --git a/app/src/main/res/values-ja/strings_android9.xml b/app/src/main/res/values-ja/strings_android9.xml new file mode 100644 index 00000000..4c0c73d1 --- /dev/null +++ b/app/src/main/res/values-ja/strings_android9.xml @@ -0,0 +1,4 @@ + + + 選択した場所 + diff --git a/app/src/main/res/values-ko/strings_android9.xml b/app/src/main/res/values-ko/strings_android9.xml new file mode 100644 index 00000000..75da71e0 --- /dev/null +++ b/app/src/main/res/values-ko/strings_android9.xml @@ -0,0 +1,4 @@ + + + 선택한 장소 + diff --git a/app/src/main/res/values-ml/strings_android9.xml b/app/src/main/res/values-ml/strings_android9.xml new file mode 100644 index 00000000..855413f7 --- /dev/null +++ b/app/src/main/res/values-ml/strings_android9.xml @@ -0,0 +1,4 @@ + + + നിങ്ങൾ തിരഞ്ഞെടുത്ത സ്ഥലം + diff --git a/app/src/main/res/values-nl/strings_android9.xml b/app/src/main/res/values-nl/strings_android9.xml new file mode 100644 index 00000000..528aa8f7 --- /dev/null +++ b/app/src/main/res/values-nl/strings_android9.xml @@ -0,0 +1,4 @@ + + + de plek die je koos + diff --git a/app/src/main/res/values-pl/strings_android9.xml b/app/src/main/res/values-pl/strings_android9.xml new file mode 100644 index 00000000..a009d6ac --- /dev/null +++ b/app/src/main/res/values-pl/strings_android9.xml @@ -0,0 +1,4 @@ + + + wybrane przez Ciebie miejsce + diff --git a/app/src/main/res/values-pt-rBR/strings_android9.xml b/app/src/main/res/values-pt-rBR/strings_android9.xml new file mode 100644 index 00000000..5c83c3fb --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings_android9.xml @@ -0,0 +1,4 @@ + + + o local que você escolheu + diff --git a/app/src/main/res/values-pt/strings_android9.xml b/app/src/main/res/values-pt/strings_android9.xml new file mode 100644 index 00000000..0b2c3945 --- /dev/null +++ b/app/src/main/res/values-pt/strings_android9.xml @@ -0,0 +1,4 @@ + + + o local que escolheu + diff --git a/app/src/main/res/values-ru/strings_android9.xml b/app/src/main/res/values-ru/strings_android9.xml new file mode 100644 index 00000000..4eaea0fe --- /dev/null +++ b/app/src/main/res/values-ru/strings_android9.xml @@ -0,0 +1,4 @@ + + + выбранное вами место + diff --git a/app/src/main/res/values-sv/strings_android9.xml b/app/src/main/res/values-sv/strings_android9.xml new file mode 100644 index 00000000..4676e504 --- /dev/null +++ b/app/src/main/res/values-sv/strings_android9.xml @@ -0,0 +1,4 @@ + + + platsen du valde + diff --git a/app/src/main/res/values-tr/strings_android9.xml b/app/src/main/res/values-tr/strings_android9.xml new file mode 100644 index 00000000..86b72736 --- /dev/null +++ b/app/src/main/res/values-tr/strings_android9.xml @@ -0,0 +1,4 @@ + + + seçtiğiniz yer + diff --git a/app/src/main/res/values-uk/strings_android9.xml b/app/src/main/res/values-uk/strings_android9.xml new file mode 100644 index 00000000..c18cb1c7 --- /dev/null +++ b/app/src/main/res/values-uk/strings_android9.xml @@ -0,0 +1,4 @@ + + + вибране вами місце + diff --git a/app/src/main/res/values-vi/strings_android9.xml b/app/src/main/res/values-vi/strings_android9.xml new file mode 100644 index 00000000..9d394b5e --- /dev/null +++ b/app/src/main/res/values-vi/strings_android9.xml @@ -0,0 +1,4 @@ + + + nơi bạn đã chọn + diff --git a/app/src/main/res/values-zh-rCN/strings_android9.xml b/app/src/main/res/values-zh-rCN/strings_android9.xml new file mode 100644 index 00000000..fcbe3a9d --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings_android9.xml @@ -0,0 +1,4 @@ + + + 您选择的位置 + diff --git a/app/src/main/res/values-zh-rTW/strings_android9.xml b/app/src/main/res/values-zh-rTW/strings_android9.xml new file mode 100644 index 00000000..2e5b542f --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_android9.xml @@ -0,0 +1,4 @@ + + + 您選擇的位置 + From c804a8e25b3d00ba8b11152f10a482fd426da884 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 19:50:52 -0500 Subject: [PATCH 11/13] 0.2.1 --- CHANGELOG.md | 22 +++++++------------ app/build.gradle.kts | 4 ++-- .../metadata/android/en-US/changelogs/201.txt | 1 + 3 files changed, 11 insertions(+), 16 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/201.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index bc789793..c71f531a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,14 @@ # Changelog -## Unreleased +## 0.2.1, 2026-09-30 -- Android 9 is now the minimum, down from 10. Saving to Download/Tern on its - own needs Android 10, which only Android has; on 9 the kept export and a - file saved from an app's page go through a file picker instead, and the - settings say so when a picker is not available either. Three API-level - bugs are fixed along the way: Dhizuku's own binder named itself in a way - only Android 10 and later accept, the transfer notification used a - foreground-service type Android 9 does not know, and the quick settings - tile tried to set a subtitle Android 9 has no field for. Two real bugs in - the install flow, found only once Android 9 ran it for the first time: - the system's install confirmation carries a different action on 9 - (CONFIRM_PERMISSIONS, not CONFIRM_INSTALL) and was silently never shown; - and a file whose own manifest asks for a newer Android than this device - has was blamed as unreadable instead of named for what it needed. +- Runs on Android 9. Saving into Download/Tern without asking needs Android + 10, so on 9 the kept export and a file saved from an app's page go through + the system's file picker, and the settings say where a picker is needed. +- On Android 9 the system's install confirmation now shows. It arrived in a + form Tern did not expect and was never put on screen. +- A file that needs a newer Android than the phone has is named for that, + instead of being called unreadable. ## 0.2.0, 2026-09-30 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index be9e03d3..32f3317f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "io.github.munzzyy.tern" minSdk = 28 targetSdk = 36 - versionCode = 200 - versionName = "0.2.0" + versionCode = 201 + versionName = "0.2.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/fastlane/metadata/android/en-US/changelogs/201.txt b/fastlane/metadata/android/en-US/changelogs/201.txt new file mode 100644 index 00000000..ed9582c3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/201.txt @@ -0,0 +1 @@ +Runs on Android 9. There, saving without asking goes through the system's file picker, and the system's install confirmation now shows. A file that needs a newer Android than the phone has is named for that instead of being called unreadable. From 525441ba3db909bf66d6fb6914ac89e7bda44a35 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 20:10:59 -0500 Subject: [PATCH 12/13] Tell a phone on Android 9 once that it no longer gets security fixes Google's last fixes for Android 9 came out in January 2022. Tern checks files, not Android, so it says so once, as the other apps do. The Persian strings from this round had lost the zero-width non-joiners Persian needs between a word and its prefix or plural; they are back. --- .../io/github/munzzyy/tern/MainActivity.kt | 12 +++++++ .../munzzyy/tern/ui/settings/Android9Note.kt | 26 ++++++++++++++ .../res/values-ar/strings_android9_note.xml | 5 +++ .../res/values-bs/strings_android9_note.xml | 5 +++ .../res/values-ca/strings_android9_note.xml | 5 +++ .../res/values-cs/strings_android9_note.xml | 5 +++ .../res/values-da/strings_android9_note.xml | 5 +++ .../res/values-de/strings_android9_note.xml | 5 +++ .../res/values-eo/strings_android9_note.xml | 5 +++ .../res/values-es/strings_android9_note.xml | 5 +++ .../res/values-fa/strings_android9_note.xml | 5 +++ app/src/main/res/values-fa/strings_safety.xml | 34 +++++++++---------- app/src/main/res/values-fa/strings_stores.xml | 34 +++++++++---------- .../res/values-fr/strings_android9_note.xml | 5 +++ .../res/values-gl/strings_android9_note.xml | 5 +++ .../res/values-hu/strings_android9_note.xml | 5 +++ .../res/values-in/strings_android9_note.xml | 5 +++ .../res/values-it/strings_android9_note.xml | 5 +++ .../res/values-ja/strings_android9_note.xml | 5 +++ .../res/values-ko/strings_android9_note.xml | 5 +++ .../res/values-ml/strings_android9_note.xml | 5 +++ .../res/values-nl/strings_android9_note.xml | 5 +++ .../res/values-pl/strings_android9_note.xml | 5 +++ .../values-pt-rBR/strings_android9_note.xml | 5 +++ .../res/values-pt/strings_android9_note.xml | 5 +++ .../res/values-ru/strings_android9_note.xml | 5 +++ .../res/values-sv/strings_android9_note.xml | 5 +++ .../res/values-tr/strings_android9_note.xml | 5 +++ .../res/values-uk/strings_android9_note.xml | 5 +++ .../res/values-vi/strings_android9_note.xml | 5 +++ .../values-zh-rCN/strings_android9_note.xml | 5 +++ .../values-zh-rTW/strings_android9_note.xml | 5 +++ .../main/res/values/strings_android9_note.xml | 5 +++ .../munzzyy/tern/ui/Android9NoteTest.kt | 21 ++++++++++++ 34 files changed, 238 insertions(+), 34 deletions(-) create mode 100644 app/src/main/kotlin/io/github/munzzyy/tern/ui/settings/Android9Note.kt create mode 100644 app/src/main/res/values-ar/strings_android9_note.xml create mode 100644 app/src/main/res/values-bs/strings_android9_note.xml create mode 100644 app/src/main/res/values-ca/strings_android9_note.xml create mode 100644 app/src/main/res/values-cs/strings_android9_note.xml create mode 100644 app/src/main/res/values-da/strings_android9_note.xml create mode 100644 app/src/main/res/values-de/strings_android9_note.xml create mode 100644 app/src/main/res/values-eo/strings_android9_note.xml create mode 100644 app/src/main/res/values-es/strings_android9_note.xml create mode 100644 app/src/main/res/values-fa/strings_android9_note.xml create mode 100644 app/src/main/res/values-fr/strings_android9_note.xml create mode 100644 app/src/main/res/values-gl/strings_android9_note.xml create mode 100644 app/src/main/res/values-hu/strings_android9_note.xml create mode 100644 app/src/main/res/values-in/strings_android9_note.xml create mode 100644 app/src/main/res/values-it/strings_android9_note.xml create mode 100644 app/src/main/res/values-ja/strings_android9_note.xml create mode 100644 app/src/main/res/values-ko/strings_android9_note.xml create mode 100644 app/src/main/res/values-ml/strings_android9_note.xml create mode 100644 app/src/main/res/values-nl/strings_android9_note.xml create mode 100644 app/src/main/res/values-pl/strings_android9_note.xml create mode 100644 app/src/main/res/values-pt-rBR/strings_android9_note.xml create mode 100644 app/src/main/res/values-pt/strings_android9_note.xml create mode 100644 app/src/main/res/values-ru/strings_android9_note.xml create mode 100644 app/src/main/res/values-sv/strings_android9_note.xml create mode 100644 app/src/main/res/values-tr/strings_android9_note.xml create mode 100644 app/src/main/res/values-uk/strings_android9_note.xml create mode 100644 app/src/main/res/values-vi/strings_android9_note.xml create mode 100644 app/src/main/res/values-zh-rCN/strings_android9_note.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_android9_note.xml create mode 100644 app/src/main/res/values/strings_android9_note.xml create mode 100644 app/src/test/kotlin/io/github/munzzyy/tern/ui/Android9NoteTest.kt diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/MainActivity.kt b/app/src/main/kotlin/io/github/munzzyy/tern/MainActivity.kt index fa10ec61..6d70dde2 100644 --- a/app/src/main/kotlin/io/github/munzzyy/tern/MainActivity.kt +++ b/app/src/main/kotlin/io/github/munzzyy/tern/MainActivity.kt @@ -3,6 +3,7 @@ package io.github.munzzyy.tern import android.content.Context import android.content.Intent import android.graphics.Color +import android.os.Build import android.os.Bundle import android.provider.Settings.Global import androidx.activity.ComponentActivity @@ -38,7 +39,9 @@ import io.github.munzzyy.tern.ui.TernApp import io.github.munzzyy.tern.ui.common.CrashDialog import io.github.munzzyy.tern.ui.common.ProblemsDialog import io.github.munzzyy.tern.ui.incomingAddInput +import io.github.munzzyy.tern.ui.settings.Android9Note import io.github.munzzyy.tern.ui.settings.VerificationNote +import io.github.munzzyy.tern.ui.settings.showsAndroid9Note import io.github.munzzyy.tern.ui.theme.TernTheme import io.github.munzzyy.tern.ui.theme.isDark import io.github.munzzyy.tern.widget.Surfaces @@ -49,6 +52,7 @@ import kotlinx.coroutines.launch private const val PREFS = "ui" private const val KEY_FIRST_RUN_DONE = "first_run_done" private const val KEY_VERIFICATION_NOTE_SHOWN = "verification_note_shown" +private const val KEY_ANDROID9_NOTE_SHOWN = "android9_note_shown" private const val TAG = "TernMain" private const val MAX_APP_ID = 64 private const val MAX_PROBLEMS = 50 @@ -69,6 +73,7 @@ class MainActivity : ComponentActivity() { /** Shown once, on the first start after the first run, so it never stands in the way of that run. */ private var verificationNote by mutableStateOf(false) + private var android9Note by mutableStateOf(false) /** A check a link from outside asked for, until the person says yes or no. */ private var confirmCheck by mutableStateOf(null) @@ -88,6 +93,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) firstRunDone = prefs().getBoolean(KEY_FIRST_RUN_DONE, false) verificationNote = firstRunDone && !prefs().getBoolean(KEY_VERIFICATION_NOTE_SHOWN, false) + android9Note = showsAndroid9Note(Build.VERSION.SDK_INT, firstRunDone, prefs().getBoolean(KEY_ANDROID9_NOTE_SHOWN, false)) if (savedInstanceState == null) crash = CrashReport.pending(this) applyScenario(intent) if (savedInstanceState == null) receive(intent) @@ -142,6 +148,7 @@ class MainActivity : ComponentActivity() { ) } if (crash == null && verificationNote) VerificationNote(onDismiss = ::sawVerificationNote) + if (crash == null && !verificationNote && android9Note) Android9Note(onDismiss = ::sawAndroid9Note) } } } @@ -240,6 +247,11 @@ class MainActivity : ComponentActivity() { CrashReport.dismiss(this) } + private fun sawAndroid9Note() { + android9Note = false + prefs().edit().putBoolean(KEY_ANDROID9_NOTE_SHOWN, true).apply() + } + private fun sawVerificationNote() { verificationNote = false prefs().edit().putBoolean(KEY_VERIFICATION_NOTE_SHOWN, true).apply() diff --git a/app/src/main/kotlin/io/github/munzzyy/tern/ui/settings/Android9Note.kt b/app/src/main/kotlin/io/github/munzzyy/tern/ui/settings/Android9Note.kt new file mode 100644 index 00000000..3cc40b0a --- /dev/null +++ b/app/src/main/kotlin/io/github/munzzyy/tern/ui/settings/Android9Note.kt @@ -0,0 +1,26 @@ +package io.github.munzzyy.tern.ui.settings + +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.github.munzzyy.tern.R +import io.github.munzzyy.tern.ui.common.focusHighlight + +/** Android 9 gets no security fixes any more, so a person on it is told once what Tern cannot cover. */ +fun showsAndroid9Note(sdk: Int, firstRunDone: Boolean, noted: Boolean): Boolean = sdk == 28 && firstRunDone && !noted + +@Composable +fun Android9Note(onDismiss: () -> Unit) { + AlertDialog( + modifier = Modifier.focusHighlight(), + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.android9_title)) }, + text = { Text(stringResource(R.string.android9_body), Modifier.verticalScroll(rememberScrollState())) }, + confirmButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_close)) } }, + ) +} diff --git a/app/src/main/res/values-ar/strings_android9_note.xml b/app/src/main/res/values-ar/strings_android9_note.xml new file mode 100644 index 00000000..3e139cba --- /dev/null +++ b/app/src/main/res/values-ar/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + يعمل هذا الهاتف بنظام Android 9 + صدرت آخر إصلاحات الأمان من Google لنظام Android 9 في يناير 2022، لذا تظل الثغرات المكتشفة في Android منذ ذلك الحين مفتوحة على هذا الهاتف. يفحص Tern كل ملف قبل أن يثبته Android، لكنه لا يستطيع إغلاق الثغرات في Android نفسه. + diff --git a/app/src/main/res/values-bs/strings_android9_note.xml b/app/src/main/res/values-bs/strings_android9_note.xml new file mode 100644 index 00000000..ad46b868 --- /dev/null +++ b/app/src/main/res/values-bs/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Ovaj telefon koristi Android 9 + Googleove posljednje sigurnosne popravke za Android 9 izašle su u januaru 2022., tako da propusti u Androidu pronađeni od tada ostaju otvoreni na ovom telefonu. Tern provjerava svaku datoteku prije nego što je Android instalira, ali ne može zatvoriti propuste u samom Androidu. + diff --git a/app/src/main/res/values-ca/strings_android9_note.xml b/app/src/main/res/values-ca/strings_android9_note.xml new file mode 100644 index 00000000..a7d5116b --- /dev/null +++ b/app/src/main/res/values-ca/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Aquest telèfon funciona amb Android 9 + Les últimes correccions de seguretat de Google per a Android 9 van sortir el gener de 2022, així que els forats a Android trobats des de llavors continuen oberts en aquest telèfon. Tern comprova cada fitxer abans que Android l\'instal·li, però no pot tancar els forats del mateix Android. + diff --git a/app/src/main/res/values-cs/strings_android9_note.xml b/app/src/main/res/values-cs/strings_android9_note.xml new file mode 100644 index 00000000..4016783b --- /dev/null +++ b/app/src/main/res/values-cs/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Tento telefon používá Android 9 + Poslední bezpečnostní opravy od Googlu pro Android 9 vyšly v lednu 2022, takže díry v Androidu nalezené od té doby zůstávají na tomto telefonu otevřené. Tern kontroluje každý soubor předtím, než ho Android nainstaluje, ale nemůže zacelit díry v samotném Androidu. + diff --git a/app/src/main/res/values-da/strings_android9_note.xml b/app/src/main/res/values-da/strings_android9_note.xml new file mode 100644 index 00000000..3740381c --- /dev/null +++ b/app/src/main/res/values-da/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Denne telefon kører Android 9 + Googles sidste sikkerhedsrettelser til Android 9 udkom i januar 2022, så huller i Android, der er fundet siden da, forbliver åbne på denne telefon. Tern tjekker hver fil, før Android installerer den, men den kan ikke lukke huller i selve Android. + diff --git a/app/src/main/res/values-de/strings_android9_note.xml b/app/src/main/res/values-de/strings_android9_note.xml new file mode 100644 index 00000000..4bf59136 --- /dev/null +++ b/app/src/main/res/values-de/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Auf diesem Telefon läuft Android 9 + Googles letzte Sicherheitsupdates für Android 9 erschienen im Januar 2022, daher bleiben seitdem gefundene Lücken in Android auf diesem Telefon offen. Tern prüft jede Datei, bevor Android sie installiert, aber es kann Lücken in Android selbst nicht schließen. + diff --git a/app/src/main/res/values-eo/strings_android9_note.xml b/app/src/main/res/values-eo/strings_android9_note.xml new file mode 100644 index 00000000..9bfaa6b2 --- /dev/null +++ b/app/src/main/res/values-eo/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Ĉi tiu telefono rulas Android 9 + La lastaj sekurecaj riparoj de Google por Android 9 aperis en januaro 2022, do truoj en Android trovitaj ekde tiam restas malfermitaj sur ĉi tiu telefono. Tern kontrolas ĉiun dosieron antaŭ ol Android instalas ĝin, sed ĝi ne povas fermi truojn en Android mem. + diff --git a/app/src/main/res/values-es/strings_android9_note.xml b/app/src/main/res/values-es/strings_android9_note.xml new file mode 100644 index 00000000..6f4c4cc2 --- /dev/null +++ b/app/src/main/res/values-es/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Este teléfono usa Android 9 + Las últimas correcciones de seguridad de Google para Android 9 salieron en enero de 2022, así que los fallos en Android encontrados desde entonces siguen abiertos en este teléfono. Tern comprueba cada archivo antes de que Android lo instale, pero no puede cerrar los fallos del propio Android. + diff --git a/app/src/main/res/values-fa/strings_android9_note.xml b/app/src/main/res/values-fa/strings_android9_note.xml new file mode 100644 index 00000000..c893bc71 --- /dev/null +++ b/app/src/main/res/values-fa/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + این تلفن Android 9 را اجرا می‌کند + آخرین اصلاحات امنیتی Google برای Android 9 در ژانویه 2022 منتشر شد، بنابراین حفره‌هایی در Android که از آن زمان پیدا شده‌اند روی این تلفن باز می‌مانند. Tern هر فایل را پیش از اینکه Android آن را نصب کند بررسی می‌کند، اما نمی‌تواند حفره‌های خود Android را ببندد. + diff --git a/app/src/main/res/values-fa/strings_safety.xml b/app/src/main/res/values-fa/strings_safety.xml index f99be524..17bfceaa 100644 --- a/app/src/main/res/values-fa/strings_safety.xml +++ b/app/src/main/res/values-fa/strings_safety.xml @@ -1,29 +1,29 @@ - لیست و تنظیمات شما را در یک فایل ذخیره میکند. توکنها هرگز وارد نمیشوند، و همینطور سرآیند درخواستی که بتواند کلیدی را در خود نگه دارد. + لیست و تنظیمات شما را در یک فایل ذخیره می‌کند. توکن‌ها هرگز وارد نمی‌شوند، و همین‌طور سرآیند درخواستی که بتواند کلیدی را در خود نگه دارد. دایجست انتشار GitHub، همان‌طور که پروکسی %1$s آن را منتقل کرد هر درخواست به GitHub به این میزبان می‌رود. میزبان هر کدام را می‌بیند و می‌تواند آنچه را که برمی‌گردد تغییر دهد: انتشارها، دایجست‌های آن‌ها و فایل‌ها. برنامه‌هایی که دارید به گواهی خود مقید می‌مانند، اما اولین نصب از طریق آن به اندازه GitHub به پروکسی اعتماد می‌کند، و پین کردن گواهی به آن نمی‌رسد. هیچ توکنی از طریق آن ارسال نمی‌شود، بنابراین پروژه‌های خصوصی و GitHub Actions را نمی‌توان به این روش دنبال کرد. بررسی فقط با Wi-Fi - بررسیهای پسزمینه منتظر Wi-Fi یا کابل میمانند، بنابراین هیچ داده تلفن همراهی استفاده نمیکنند. + بررسی‌های پس‌زمینه منتظر Wi-Fi یا کابل می‌مانند، بنابراین هیچ داده تلفن همراهی استفاده نمی‌کنند. بررسی فقط هنگام شارژ - بررسیهای پسزمینه تا زمانی که دستگاه در حال شارژ است منتظر میمانند. - بهروزرسانیای که خودش نصب میشود منتظر Wi-Fi یا کابل میماند، مانند Obtainium. - بهروزرسانیای که خودش نصب میشود تا زمانی که دستگاه در حال شارژ است منتظر میماند. - منبع، کد نسخه را %2$d نامید، اما این فایل %1$d است، قدیمیتر از نسخه نصبشده. این فایل روی نسخه جدیدتر نصب نمیشود. - برنامه Let Me Downgrade نصب است، بنابراین Android ممکن است یک نسخه قدیمیتر را روی نسخه جدیدتر قرار دهد. Tern این کار را فقط برای انتشاری انجام میدهد که شما از تاریخچه یک برنامه انتخاب میکنید، هرگز در پسزمینه یا با «بهروزرسانی همه» انجام نمیدهد. - دریافت بالاترین نسخه، همانطور که Tern 0.1.0 انجام میداد - پیوند دارای بالاترین نسخه در صفحه را ارائه میدهد، در هر کجای صفحه که باشد، و الگوی نسخه را روی نسخه خواندهشده از هر پیوند اجرا میکند. حالت خاموش، صفحه را همانطور که Obtainium میخواند، بررسی میکند: آخرین پیوند به ترتیب، جدیدترین است. - برنامهای که بیرون از Tern حذف میکنید، از لیست هم خارج میشود. برنامه بایگانیشده باقی میماند. اگر برنامه دوباره از همان مکان اضافه شود، به گواهیای که داشته مقید میماند؛ حذف آن در Tern باعث فراموشی این موضوع میشود. - اکنون بهروزرسانی‌ها بررسی شوند؟ - پیوندی از Tern خواست تا بهروزرسانیهای %1$s را بررسی کند. از منبع آن پرسیده میشود، و هیچ چیزی نصب نمیشود. + بررسی‌های پس‌زمینه تا زمانی که دستگاه در حال شارژ است منتظر می‌مانند. + به‌روزرسانیای که خودش نصب می‌شود منتظر Wi-Fi یا کابل می‌ماند، مانند Obtainium. + به‌روزرسانیای که خودش نصب می‌شود تا زمانی که دستگاه در حال شارژ است منتظر می‌ماند. + منبع، کد نسخه را %2$d نامید، اما این فایل %1$d است، قدیمیتر از نسخه نصبشده. این فایل روی نسخه جدیدتر نصب نمی‌شود. + برنامه Let Me Downgrade نصب است، بنابراین Android ممکن است یک نسخه قدیمیتر را روی نسخه جدیدتر قرار دهد. Tern این کار را فقط برای انتشاری انجام می‌دهد که شما از تاریخچه یک برنامه انتخاب می‌کنید، هرگز در پس‌زمینه یا با «به‌روزرسانی همه» انجام نمی‌دهد. + دریافت بالاترین نسخه، همانطور که Tern 0.1.0 انجام می‌داد + پیوند دارای بالاترین نسخه در صفحه را ارائه می‌دهد، در هر کجای صفحه که باشد، و الگوی نسخه را روی نسخه خواندهشده از هر پیوند اجرا می‌کند. حالت خاموش، صفحه را همانطور که Obtainium می‌خواند، بررسی می‌کند: آخرین پیوند به ترتیب، جدیدترین است. + برنام‌های که بیرون از Tern حذف می‌کنید، از لیست هم خارج می‌شود. برنامه بایگانیشده باقی می‌ماند. اگر برنامه دوباره از همان مکان اضافه شود، به گواهیای که داشته مقید می‌ماند؛ حذف آن در Tern باعث فراموشی این موضوع می‌شود. + اکنون به‌روزرسانی‌ها بررسی شوند؟ + پیوندی از Tern خواست تا به‌روزرسانی‌های %1$s را بررسی کند. از منبع آن پرسیده می‌شود، و هیچ چیزی نصب نمی‌شود. - پیوندی از Tern خواست تا بهروزرسانیهای %d برنامه در لیست شما را بررسی کند. از منبع آن پرسیده میشود، و هیچ چیزی نصب نمیشود. - پیوندی از Tern خواست تا بهروزرسانیهای %d برنامه در لیست شما را بررسی کند. از منابع آنها پرسیده میشود، و هیچ چیزی نصب نمیشود. + پیوندی از Tern خواست تا به‌روزرسانی‌های %d برنامه در لیست شما را بررسی کند. از منبع آن پرسیده می‌شود، و هیچ چیزی نصب نمی‌شود. + پیوندی از Tern خواست تا به‌روزرسانی‌های %d برنامه در لیست شما را بررسی کند. از منابع آن‌ها پرسیده می‌شود، و هیچ چیزی نصب نمی‌شود. بررسی فعلاً نه %1$s آماده نبود، بنابراین از نصبکننده Android برای این مورد استفاده شد. - این نسخه را F-Droid نصب کرده است، بنابراین Tern مدخل آن را در آنجا دنبال میکند: بهروزرسانیها همان نسخههایی هستند که F-Droid ساخته و بررسی کرده است، همانطور که برنامه F-Droid آنها را دریافت میکند. - در عوض، انتشارهای GitHub را دنبال کن - بهروزرسانیها به محض انتشار از GitHub میآیند، پیش از آنکه F-Droid آنها را بسازد و بررسی کند. این کار بررسیهای F-Droid را نادیده میگیرد. Tern همچنان هر فایل را بررسی کرده و آن را به گواهی خودش مقید نگه میدارد. + این نسخه را F-Droid نصب کرده است، بنابراین Tern مدخل آن را در آنجا دنبال می‌کند: به‌روزرسانی‌ها همان نسخه‌هایی هستند که F-Droid ساخته و بررسی کرده است، همانطور که برنامه F-Droid آن‌ها را دریافت می‌کند. + در عوض، انتشار‌های GitHub را دنبال کن + به‌روزرسانی‌ها به محض انتشار از GitHub می‌آیند، پیش از آنکه F-Droid آن‌ها را بسازد و بررسی کند. این کار بررسی‌های F-Droid را نادیده می‌گیرد. Tern همچنان هر فایل را بررسی کرده و آن را به گواهی خودش مقید نگه می‌دارد. diff --git a/app/src/main/res/values-fa/strings_stores.xml b/app/src/main/res/values-fa/strings_stores.xml index 133bfe8e..18a9ea72 100644 --- a/app/src/main/res/values-fa/strings_stores.xml +++ b/app/src/main/res/values-fa/strings_stores.xml @@ -1,27 +1,27 @@ - هر کدام در یک خط، به شکل Name: value. هرگز گذرواژه، کوکی یا User-Agent وارد نکنید: Tern به هر سایتی میگوید که Tern است. - %1$s آنچه را که توسعهدهندگان در جای دیگری منتشر میکنند دوباره ارائه میدهد، و خودش توسعهدهنده نیست. تا زمانی که برنامه به یک گواهی پین شود، اولین فایلی که Tern از آن نصب میکند تعیین میکند که هر بهروزرسانی بعدی باید چه گواهیای داشته باشد. - %1$s آنچه را که توسعهدهندگان در جای دیگری منتشر میکنند دوباره ارائه میدهد، و خودش توسعهدهنده نیست. فایلهای آن باید گواهیای را داشته باشند که این برنامه به آن پین شده است. - %1$s یک فروشگاه است، نه سایت خود توسعهدهنده. تا زمانی که برنامه به یک گواهی پین شود، اولین فایلی که Tern از آن نصب میکند تعیین میکند که هر بهروزرسانی بعدی باید چه گواهیای داشته باشد. - %1$s یک فروشگاه است، نه سایت خود توسعهدهنده. فایلهای آن باید گواهیای را داشته باشند که این برنامه به آن پین شده است. - Tern سایتهایی را که برنامههای دستکاریشده ارائه میدهند نمیخواند. فایلهای آنها توسط شخص دیگری غیر از توسعهدهنده تغییر کرده و توسط همان شخص تغییردهنده امضا شدهاند. - Tern نمیتواند این فروشگاه را بدون اینکه تظاهر کند برنامه آن است بخواند، بنابراین آن را نمیخواند. + هر کدام در یک خط، به شکل Name: value. هرگز گذرواژه، کوکی یا User-Agent وارد نکنید: Tern به هر سایتی می‌گوید که Tern است. + %1$s آنچه را که توسعهدهندگان در جای دیگری منتشر می‌کنند دوباره ارائه می‌دهد، و خودش توسعهدهنده نیست. تا زمانی که برنامه به یک گواهی پین شود، اولین فایلی که Tern از آن نصب می‌کند تعیین می‌کند که هر به‌روزرسانی بعدی باید چه گواهیای داشته باشد. + %1$s آنچه را که توسعهدهندگان در جای دیگری منتشر می‌کنند دوباره ارائه می‌دهد، و خودش توسعهدهنده نیست. فایل‌های آن باید گواهیای را داشته باشند که این برنامه به آن پین شده است. + %1$s یک فروشگاه است، نه سایت خود توسعهدهنده. تا زمانی که برنامه به یک گواهی پین شود، اولین فایلی که Tern از آن نصب می‌کند تعیین می‌کند که هر به‌روزرسانی بعدی باید چه گواهیای داشته باشد. + %1$s یک فروشگاه است، نه سایت خود توسعهدهنده. فایل‌های آن باید گواهیای را داشته باشند که این برنامه به آن پین شده است. + Tern سایت‌هایی را که برنامه‌های دستکاریشده ارائه می‌دهند نمی‌خواند. فایل‌های آن‌ها توسط شخص دیگری غیر از توسعهدهنده تغییر کرده و توسط همان شخص تغییردهنده امضا شده‌اند. + Tern نمی‌تواند این فروشگاه را بدون اینکه تظاهر کند برنامه آن است بخواند، بنابراین آن را نمی‌خواند. %1$s، که توسعهدهنده نیست %1$s یک فروشگاه شخص ثالث است - فروشگاههای شخص ثالث خاموش هستند. یک فروشگاه نسخههای خودش از برنامهها را ارائه میدهد، نه فایلها را از سایت خود توسعهدهنده، و Tern آن را از طریق رابطی که فروشگاه برای برنامه یا وبسایت خودش ساخته است میخواند. با روشن بودن آنها، Tern از یک فروشگاه فقط برنامههایی را میخواهد که شما از آن اضافه میکنید، و تا زمانی که یک برنامه به یک گواهی پین شود، اولین فایلی که Tern از یک فروشگاه نصب میکند تعیین میکند که بهروزرسانیهای آن باید چه گواهیای داشته باشند. + فروشگاه‌های شخص ثالث خاموش هستند. یک فروشگاه نسخه‌های خودش از برنامه‌ها را ارائه می‌دهد، نه فایل‌ها را از سایت خود توسعهدهنده، و Tern آن را از طریق رابطی که فروشگاه برای برنامه یا وب‌سایت خودش ساخته است می‌خواند. با روشن بودن آنها، Tern از یک فروشگاه فقط برنامه‌هایی را می‌خواهد که شما از آن اضافه می‌کنید، و تا زمانی که یک برنامه به یک گواهی پین شود، اولین فایلی که Tern از یک فروشگاه نصب می‌کند تعیین می‌کند که به‌روزرسانی‌های آن باید چه گواهیای داشته باشند. فعال‌سازی فروشگاه‌های شخص ثالث - برای بررسی دوباره این برنامه، فروشگاههای شخص ثالث را در تنظیمات روشن کنید. + برای بررسی دوباره این برنامه، فروشگاه‌های شخص ثالث را در تنظیمات روشن کنید. متوقف - فروشگاههای شخص ثالث خاموش هستند - فروشگاههای شخص ثالث - APKPure، Aptoide، APKCombo، APKMirror، Tencent، Huawei AppGallery، Galaxy Store و vivo. آنها نسخههای خودشان از برنامهها را ارائه میدهند، نه فایلها را از سایت خود توسعهدهنده، و Tern آنها را از طریق رابطهایی که برای برنامهها و وبسایتهای خود ساختهاند میخواند. در حالت خاموش، Tern هیچ چیزی از آنها نمیپرسد، جستجو آنها را کنار میگذارد و برنامههای آنها متوقف میشوند. + فروشگاه‌های شخص ثالث خاموش هستند + فروشگاه‌های شخص ثالث + APKPure، Aptoide، APKCombo، APKMirror، Tencent، Huawei AppGallery، Galaxy Store و vivo. آن‌ها نسخه‌های خودشان از برنامه‌ها را ارائه می‌دهند، نه فایل‌ها را از سایت خود توسعهدهنده، و Tern آن‌ها را از طریق رابط‌هایی که برای برنامه‌ها و وب‌سایت‌های خود ساخته‌اند می‌خواند. در حالت خاموش، Tern هیچ چیزی از آن‌ها نمی‌پرسد، جستجو آن‌ها را کنار می‌گذارد و برنامه‌های آن‌ها متوقف می‌شوند. خاموش در تنظیمات فایل با چکسام فروشگاه مطابقت دارد - چکسام یک اثر انگشت از خود فایل است. فروشگاه یکی را ارائه داد، و فایلی که Tern دانلود کرد همان را دارد، بنابراین همانطور که فروشگاه آن را دارد رسیده است. فروشگاه توسعهدهنده نیست، بنابراین این مورد هیچ چیزی درباره اینکه چه کسی فایل را ساخته است نمیگوید. گواهی این کار را میکند. - اینها از فروشگاههای شخص ثالث میآیند، که نسخههای خودشان از برنامهها را ارائه میدهند. تا زمانی که یک برنامه به یک گواهی پین شود، اولین فایلی که Tern از فروشگاه آن نصب میکند تعیین میکند که بهروزرسانیهای آن باید چه گواهیای داشته باشند. - فروشگاههای شخص ثالث خاموش هستند، بنابراین این برنامهها متوقف شدهاند. Tern تا زمانی که آنها را روشن نکنید چیزی از آن فروشگاهها نمیپرسد. - از فروشگاههای شخص ثالث + چکسام یک اثر انگشت از خود فایل است. فروشگاه یکی را ارائه داد، و فایلی که Tern دانلود کرد همان را دارد، بنابراین همانطور که فروشگاه آن را دارد رسیده است. فروشگاه توسعهدهنده نیست، بنابراین این مورد هیچ چیزی درباره اینکه چه کسی فایل را ساخته است نمی‌گوید. گواهی این کار را می‌کند. + این‌ها از فروشگاه‌های شخص ثالث می‌آیند، که نسخه‌های خودشان از برنامه‌ها را ارائه می‌دهند. تا زمانی که یک برنامه به یک گواهی پین شود، اولین فایلی که Tern از فروشگاه آن نصب می‌کند تعیین می‌کند که به‌روزرسانی‌های آن باید چه گواهیای داشته باشند. + فروشگاه‌های شخص ثالث خاموش هستند، بنابراین این برنامه‌ها متوقف شده‌اند. Tern تا زمانی که آن‌ها را روشن نکنید چیزی از آن فروشگاه‌ها نمی‌پرسد. + از فروشگاه‌های شخص ثالث اشتراکگذاری بهعنوان پیوند Obtainium - اشتراکگذاری بهعنوان پیوندهای Obtainium + اشتراکگذاری بهعنوان پیوند‌های Obtainium diff --git a/app/src/main/res/values-fr/strings_android9_note.xml b/app/src/main/res/values-fr/strings_android9_note.xml new file mode 100644 index 00000000..51cb0278 --- /dev/null +++ b/app/src/main/res/values-fr/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Ce téléphone fonctionne sous Android 9 + Les derniers correctifs de sécurité de Google pour Android 9 sont sortis en janvier 2022, donc les failles dans Android trouvées depuis restent ouvertes sur ce téléphone. Tern vérifie chaque fichier avant qu\'Android ne l\'installe, mais il ne peut pas combler les failles dans Android lui-même. + diff --git a/app/src/main/res/values-gl/strings_android9_note.xml b/app/src/main/res/values-gl/strings_android9_note.xml new file mode 100644 index 00000000..dad4af24 --- /dev/null +++ b/app/src/main/res/values-gl/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Este teléfono ten Android 9 + As últimas correccións de seguranza de Google para Android 9 saíron en xaneiro de 2022, así que os buracos en Android atopados desde entón seguen abertos neste teléfono. Tern comproba cada ficheiro antes de que Android o instale, pero non pode pechar os buracos do propio Android. + diff --git a/app/src/main/res/values-hu/strings_android9_note.xml b/app/src/main/res/values-hu/strings_android9_note.xml new file mode 100644 index 00000000..3622d0c4 --- /dev/null +++ b/app/src/main/res/values-hu/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Ezen a telefonon Android 9 fut + A Google utolsó biztonsági javításai az Android 9-hez 2022 januárjában jelentek meg, így az azóta az Androidban talált rések ezen a telefonon nyitva maradnak. A Tern minden fájlt ellenőriz, mielőtt az Android telepíti, de magában az Androidban lévő réseket nem tudja bezárni. + diff --git a/app/src/main/res/values-in/strings_android9_note.xml b/app/src/main/res/values-in/strings_android9_note.xml new file mode 100644 index 00000000..45462b7e --- /dev/null +++ b/app/src/main/res/values-in/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Ponsel ini menjalankan Android 9 + Perbaikan keamanan terakhir Google untuk Android 9 dirilis pada Januari 2022, sehingga celah pada Android yang ditemukan sejak saat itu tetap terbuka di ponsel ini. Tern memeriksa setiap file sebelum Android menginstalnya, tetapi tidak dapat menutup celah pada Android itu sendiri. + diff --git a/app/src/main/res/values-it/strings_android9_note.xml b/app/src/main/res/values-it/strings_android9_note.xml new file mode 100644 index 00000000..180857b5 --- /dev/null +++ b/app/src/main/res/values-it/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Questo telefono usa Android 9 + Le ultime correzioni di sicurezza di Google per Android 9 sono uscite a gennaio 2022, quindi le falle di Android trovate da allora rimangono aperte su questo telefono. Tern controlla ogni file prima che Android lo installi, ma non può chiudere le falle di Android stesso. + diff --git a/app/src/main/res/values-ja/strings_android9_note.xml b/app/src/main/res/values-ja/strings_android9_note.xml new file mode 100644 index 00000000..265a9724 --- /dev/null +++ b/app/src/main/res/values-ja/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + この電話はAndroid 9で動作しています + Android 9に対するGoogleの最後のセキュリティ修正は2022年1月に公開されたため、それ以降に見つかったAndroidのセキュリティホールは、この電話では塞がれないままです。TernはAndroidがインストールする前にすべてのファイルを確認しますが、Android自体のセキュリティホールを塞ぐことはできません。 + diff --git a/app/src/main/res/values-ko/strings_android9_note.xml b/app/src/main/res/values-ko/strings_android9_note.xml new file mode 100644 index 00000000..282f9296 --- /dev/null +++ b/app/src/main/res/values-ko/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + 이 휴대전화는 Android 9을 실행합니다 + Google의 Android 9용 마지막 보안 수정은 2022년 1월에 나왔으므로, 그 이후에 발견된 Android의 취약점은 이 휴대전화에 그대로 남아 있습니다. Tern은 Android가 설치하기 전에 모든 파일을 확인하지만, Android 자체의 취약점을 막을 수는 없습니다. + diff --git a/app/src/main/res/values-ml/strings_android9_note.xml b/app/src/main/res/values-ml/strings_android9_note.xml new file mode 100644 index 00000000..8718ba37 --- /dev/null +++ b/app/src/main/res/values-ml/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + ഈ ഫോണിൽ Android 9 ആണ് പ്രവർത്തിക്കുന്നത് + Android 9-നുള്ള Google-ൻ്റെ അവസാന സുരക്ഷാ പരിഹാരങ്ങൾ 2022 ജനുവരിയിലാണ് പുറത്തിറങ്ങിയത്, അതിനാൽ അതിനുശേഷം കണ്ടെത്തിയ Android-ലെ പഴുതുകൾ ഈ ഫോണിൽ തുറന്നുകിടക്കുന്നു. Android ഇൻസ്റ്റാൾ ചെയ്യുന്നതിന് മുമ്പ് Tern ഓരോ ഫയലും പരിശോധിക്കുന്നു, എന്നാൽ Android-ൽ തന്നെയുള്ള പഴുതുകൾ അടയ്ക്കാൻ അതിന് കഴിയില്ല. + diff --git a/app/src/main/res/values-nl/strings_android9_note.xml b/app/src/main/res/values-nl/strings_android9_note.xml new file mode 100644 index 00000000..b3f8ee77 --- /dev/null +++ b/app/src/main/res/values-nl/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Deze telefoon draait op Android 9 + De laatste beveiligingsfixes van Google voor Android 9 kwamen uit in januari 2022, dus gaten in Android die sindsdien zijn gevonden, blijven open op deze telefoon. Tern controleert elk bestand voordat Android het installeert, maar kan geen gaten in Android zelf dichten. + diff --git a/app/src/main/res/values-pl/strings_android9_note.xml b/app/src/main/res/values-pl/strings_android9_note.xml new file mode 100644 index 00000000..b7b305d7 --- /dev/null +++ b/app/src/main/res/values-pl/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Ten telefon działa na Androidzie 9 + Ostatnie poprawki zabezpieczeń Google dla Androida 9 ukazały się w styczniu 2022 roku, więc luki w Androidzie znalezione od tego czasu pozostają otwarte na tym telefonie. Tern sprawdza każdy plik, zanim Android go zainstaluje, ale nie może załatać luk w samym Androidzie. + diff --git a/app/src/main/res/values-pt-rBR/strings_android9_note.xml b/app/src/main/res/values-pt-rBR/strings_android9_note.xml new file mode 100644 index 00000000..8064ffc0 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Este telefone roda o Android 9 + As últimas correções de segurança do Google para o Android 9 saíram em janeiro de 2022, então as brechas no Android encontradas desde então continuam abertas neste telefone. O Tern verifica cada arquivo antes de o Android instalá-lo, mas não consegue fechar as brechas no próprio Android. + diff --git a/app/src/main/res/values-pt/strings_android9_note.xml b/app/src/main/res/values-pt/strings_android9_note.xml new file mode 100644 index 00000000..b5adcea7 --- /dev/null +++ b/app/src/main/res/values-pt/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Este telemóvel tem o Android 9 + As últimas correções de segurança da Google para o Android 9 saíram em janeiro de 2022, por isso as falhas no Android encontradas desde então continuam abertas neste telemóvel. O Tern verifica cada ficheiro antes de o Android o instalar, mas não consegue fechar as falhas no próprio Android. + diff --git a/app/src/main/res/values-ru/strings_android9_note.xml b/app/src/main/res/values-ru/strings_android9_note.xml new file mode 100644 index 00000000..f09c9e5d --- /dev/null +++ b/app/src/main/res/values-ru/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Этот телефон работает на Android 9 + Последние исправления безопасности от Google для Android 9 вышли в январе 2022 года, поэтому уязвимости в Android, найденные с тех пор, остаются открытыми на этом телефоне. Tern проверяет каждый файл до того, как Android его установит, но он не может закрыть уязвимости в самом Android. + diff --git a/app/src/main/res/values-sv/strings_android9_note.xml b/app/src/main/res/values-sv/strings_android9_note.xml new file mode 100644 index 00000000..e3af6fa2 --- /dev/null +++ b/app/src/main/res/values-sv/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Denna telefon kör Android 9 + Googles senaste säkerhetsuppdateringar för Android 9 kom ut i januari 2022, så hål i Android som hittats sedan dess förblir öppna på denna telefon. Tern kontrollerar varje fil innan Android installerar den, men den kan inte täppa till hål i själva Android. + diff --git a/app/src/main/res/values-tr/strings_android9_note.xml b/app/src/main/res/values-tr/strings_android9_note.xml new file mode 100644 index 00000000..ce99fe34 --- /dev/null +++ b/app/src/main/res/values-tr/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Bu telefonda Android 9 çalışıyor + Google\'ın Android 9 için son güvenlik düzeltmeleri Ocak 2022\'de çıktı, bu yüzden o zamandan beri Android\'de bulunan açıklar bu telefonda açık kalıyor. Tern, Android yüklemeden önce her dosyayı denetler, ancak Android\'in kendisindeki açıkları kapatamaz. + diff --git a/app/src/main/res/values-uk/strings_android9_note.xml b/app/src/main/res/values-uk/strings_android9_note.xml new file mode 100644 index 00000000..60b03d06 --- /dev/null +++ b/app/src/main/res/values-uk/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Цей телефон працює на Android 9 + Останні виправлення безпеки від Google для Android 9 вийшли в січні 2022 року, тому вразливості в Android, знайдені відтоді, залишаються відкритими на цьому телефоні. Tern перевіряє кожен файл, перш ніж Android його встановить, але не може закрити вразливості в самому Android. + diff --git a/app/src/main/res/values-vi/strings_android9_note.xml b/app/src/main/res/values-vi/strings_android9_note.xml new file mode 100644 index 00000000..30c53a82 --- /dev/null +++ b/app/src/main/res/values-vi/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + Điện thoại này chạy Android 9 + Các bản vá bảo mật cuối cùng của Google cho Android 9 đã ra mắt vào tháng 1 năm 2022, nên các lỗ hổng trong Android được phát hiện từ đó đến nay vẫn tồn tại trên điện thoại này. Tern kiểm tra mọi tệp trước khi Android cài đặt, nhưng nó không thể vá các lỗ hổng trong chính Android. + diff --git a/app/src/main/res/values-zh-rCN/strings_android9_note.xml b/app/src/main/res/values-zh-rCN/strings_android9_note.xml new file mode 100644 index 00000000..72ad7536 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + 此手机运行 Android 9 + Google 最后一次针对 Android 9 的安全修复发布于 2022 年 1 月,因此自那时起发现的 Android 漏洞在此手机上依然存在。Tern 会在 Android 安装每个文件前对其进行检查,但它无法修补 Android 本身的漏洞。 + diff --git a/app/src/main/res/values-zh-rTW/strings_android9_note.xml b/app/src/main/res/values-zh-rTW/strings_android9_note.xml new file mode 100644 index 00000000..ce01b0de --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + 這支手機執行 Android 9 + Google 在 2022 年 1 月發佈了 Android 9 的最後一次安全性修正,因此自那之後發現的 Android 漏洞在這支手機上依然存在。Tern 會在 Android 安裝前檢查每個檔案,但它無法修補 Android 本身的漏洞。 + diff --git a/app/src/main/res/values/strings_android9_note.xml b/app/src/main/res/values/strings_android9_note.xml new file mode 100644 index 00000000..98633b23 --- /dev/null +++ b/app/src/main/res/values/strings_android9_note.xml @@ -0,0 +1,5 @@ + + + This phone runs Android 9 + Google\'s last security fixes for Android 9 came out in January 2022, so holes in Android found since then stay open on this phone. Tern checks every file before Android installs it, but it cannot close holes in Android itself. + diff --git a/app/src/test/kotlin/io/github/munzzyy/tern/ui/Android9NoteTest.kt b/app/src/test/kotlin/io/github/munzzyy/tern/ui/Android9NoteTest.kt new file mode 100644 index 00000000..e63159b2 --- /dev/null +++ b/app/src/test/kotlin/io/github/munzzyy/tern/ui/Android9NoteTest.kt @@ -0,0 +1,21 @@ +package io.github.munzzyy.tern.ui + +import io.github.munzzyy.tern.ui.settings.showsAndroid9Note +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class Android9NoteTest { + @Test + fun androidNineIsToldOnceAfterTheFirstRun() { + assertTrue(showsAndroid9Note(sdk = 28, firstRunDone = true, noted = false)) + assertFalse(showsAndroid9Note(sdk = 28, firstRunDone = true, noted = true)) + assertFalse(showsAndroid9Note(sdk = 28, firstRunDone = false, noted = false)) + } + + @Test + fun newerAndroidIsNotTold() { + assertFalse(showsAndroid9Note(sdk = 29, firstRunDone = true, noted = false)) + assertFalse(showsAndroid9Note(sdk = 36, firstRunDone = true, noted = false)) + } +} From e38dbb3e87661a9f9507c77c101bc79fc90f6de3 Mon Sep 17 00:00:00 2001 From: Munzzyy Date: Wed, 30 Sep 2026 20:11:10 -0500 Subject: [PATCH 13/13] Say in the changelog what 0.2.1 tells Android 9 and fixes in Persian --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c71f531a..33854c85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ form Tern did not expect and was never put on screen. - A file that needs a newer Android than the phone has is named for that, instead of being called unreadable. +- On Android 9, Tern says once that Google's last security fixes for it came + out in January 2022, and that checking files cannot close holes in Android. +- Persian text added in 0.2.0 has its zero-width non-joiners back. ## 0.2.0, 2026-09-30