diff --git a/CHANGELOG.md b/CHANGELOG.md
index 891eec65..33854c85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## 0.2.1, 2026-09-30
+
+- 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.
+- 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
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.
[
](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/app/build.gradle.kts b/app/build.gradle.kts
index d9825880..32f3317f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -9,10 +9,10 @@ android {
defaultConfig {
applicationId = "io.github.munzzyy.tern"
- minSdk = 29
+ minSdk = 28
targetSdk = 36
- versionCode = 200
- versionName = "0.2.0"
+ versionCode = 201
+ versionName = "0.2.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
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/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)
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/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/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/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
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/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)
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
}
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()
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/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/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()
}
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 ->
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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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-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.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 @@
+
+
+ 您選擇的位置
+
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.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
+
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))
+ }
+}
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.
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