From f82af732348d9eb3bab9d5ea94b09f1124a7f06b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 27 Mar 2026 14:31:59 -0300 Subject: [PATCH 01/30] Support a single canonical location for preloaded repos in RepoPreLoader and add tests for the entire class --- .../kotlin/org/fdroid/repo/RepoPreLoader.kt | 44 ++- .../java/org/fdroid/repo/RepoPreLoaderTest.kt | 268 ++++++++++++++++++ 2 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 app/src/test/java/org/fdroid/repo/RepoPreLoaderTest.kt diff --git a/app/src/main/kotlin/org/fdroid/repo/RepoPreLoader.kt b/app/src/main/kotlin/org/fdroid/repo/RepoPreLoader.kt index 9baed1fba..5d18b3278 100644 --- a/app/src/main/kotlin/org/fdroid/repo/RepoPreLoader.kt +++ b/app/src/main/kotlin/org/fdroid/repo/RepoPreLoader.kt @@ -10,12 +10,24 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import mu.KotlinLogging import org.fdroid.database.FDroidDatabase import org.fdroid.database.InitialRepository import org.fdroid.database.RepositoryDao @Singleton -class RepoPreLoader @Inject constructor(@param:ApplicationContext private val context: Context) { +class RepoPreLoader( + private val context: Context, + /** + * Used for unit tests, because a mocking the [File] constructor is buggy: + * https://github.com/mockk/mockk/issues/603 + */ + private val fileFactory: (String) -> File = ::File, +) { + + @Inject constructor(@ApplicationContext context: Context) : this(context, ::File) + + private val log = KotlinLogging.logger {} @get:WorkerThread val defaultRepoAddresses: Set by lazy { getDefaultRepos().map { it.address }.toSet() } @@ -26,13 +38,11 @@ class RepoPreLoader @Inject constructor(@param:ApplicationContext private val co addRepositories(db.getRepositoryDao(), getDefaultRepos()) // "system" can be removed when minSdk is 28 for (root in listOf("/system", "/system_ext", "/product", "/vendor")) { - val romReposFile = File("$root/etc/${context.packageName}/additional_repos.json") - if (romReposFile.isFile) { - val romRepos = - romReposFile.inputStream().use { inputStream -> - Json.decodeFromStream>(inputStream) - } - addRepositories(db.getRepositoryDao(), romRepos) + for (subdir in listOf(context.packageName, "fdroid")) { + val romRepos = tryLoadRepositoriesFromFile("$root/etc/$subdir/additional_repos.json") + if (romRepos.isNotEmpty()) { + addRepositories(db.getRepositoryDao(), romRepos) + } } } } @@ -44,6 +54,24 @@ class RepoPreLoader @Inject constructor(@param:ApplicationContext private val co Json.decodeFromStream>(inputStream) } + @WorkerThread + @OptIn(ExperimentalSerializationApi::class) + private fun tryLoadRepositoriesFromFile(filePath: String): List { + val file = fileFactory(filePath) + return if (file.isFile) { + try { + file.inputStream().use { inputStream -> + Json.decodeFromStream>(inputStream) + } + } catch (e: Exception) { + log.error(e) { "Failed to load repositories from $filePath: " } + emptyList() + } + } else { + emptyList() + } + } + private fun addRepositories(repositoryDao: RepositoryDao, repositories: List) { repositories.forEach { repository -> val initialRepository = diff --git a/app/src/test/java/org/fdroid/repo/RepoPreLoaderTest.kt b/app/src/test/java/org/fdroid/repo/RepoPreLoaderTest.kt new file mode 100644 index 000000000..f0f4d0d99 --- /dev/null +++ b/app/src/test/java/org/fdroid/repo/RepoPreLoaderTest.kt @@ -0,0 +1,268 @@ +package org.fdroid.repo + +import android.content.Context +import android.content.res.AssetManager +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.fdroid.database.FDroidDatabase +import org.fdroid.database.InitialRepository +import org.fdroid.database.RepositoryDao +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +internal class RepoPreLoaderTest { + + @get:Rule val tmpFolder: TemporaryFolder = TemporaryFolder() + + private val context: Context = mockk(relaxed = true) + private val assetManager: AssetManager = mockk(relaxed = true) + private val db: FDroidDatabase = mockk(relaxed = true) + private val repositoryDao: RepositoryDao = mockk(relaxed = true) + private val fileFactory: (String) -> File = mockk() + private val repoPreLoader = RepoPreLoader(context, fileFactory) + + private val packageName = "org.fdroid.basic" + + init { + every { context.assets } returns assetManager + every { context.packageName } returns packageName + every { db.getRepositoryDao() } returns repositoryDao + // By default, the asset file is empty (no repos) + every { assetManager.open("default_repos.json") } returns "[]".byteInputStream() + // By default, every path doesn't existent, so no ROM repos are loaded + every { fileFactory(any()) } returns File("does-not-exist") + } + + private val testReposJson = + """ + [ + { + "name": "CalyxOS Local Repository", + "address": "file:///product/fdroid/repo/", + "description": "This is a repository of apps to be used with CalyxOS. It is installed to the system partition, and meant to be used to initially install apps when offline. It contains a limited number of applications deemed suitable for inclusion with CalyxOS.", + "certificate": "30820503308202eba00302010202046d902e92300d06092a864886f70d01010b050030323110300e060355040b1307462d44726f6964311e301c060355040313156c6f63616c686f73742e6c6f63616c646f6d61696e301e170d3139303131303134303235355a170d3436303532383134303235355a30323110300e060355040b1307462d44726f6964311e301c060355040313156c6f63616c686f73742e6c6f63616c646f6d61696e30820222300d06092a864886f70d01010105000382020f003082020a028202010087fc14522eb8d57da8e05121574345edf69574973d64585df2292c23acf81bc42c98f1cbfdf9fe7a1976bc575d28f6b606dbf3228b110cfc7ddc823722a279cd69b0f846ae5500ecd9884556209eacbbab30159a6ea1eaf2f64967849369a10adba65c8738b2c82b676e1367bb7b43a62dc1a5438a7c46ae2d971eafebca606a4960e0b7b1795a2a314e25759d06a20755b36b7bf0d9a6868c08aae63e389dd68f450ee093b02b28e830018dbcbbfd48ac757d8cda87549f9c41836608595e9ebfa09c128acc3c7dfd1d17a67eb6a5c99281fba69652ec27f4df3406b886e00881e6d6a4feb8fd1c5b84a5c773a631b1b2d6eab5c5ebe503c599d40f15d3de313b0d0c96d3c63802ef4346036791b2b793b3874ca73a70565ba7a768c3062679aab0e289e98b9ba16a77c8747b80820618863fe2028f02afc55914c8d6c4bcc13dd0dda61834b728875b9682ee9724589bbe216cf2b0655f62976dbf07c91514e1c342e8e397ba4458eaa5aa98703517264ef47a0972458fee928d67e06c34ecdbf7307a157928567d799c34f2a657cee034bd3fbbb717387f12e70871f2dc378687b7889bb727b92d69c8a9996257b8404e93e53eb187c807a154d95b5690eb053c249613cedad9edea857b168d41864c892c33cfecbb969cc6199e7215e82dc5810a1785ebdad509e0254daa2b2acda1093bda4fb389a8a2db5f526c5b23c10203010001a321301f301d0603551d0e04160414ba8990d4010764f81c86c769df075400c198f15e300d06092a864886f70d01010b05000382020100489ce26311568b78e4fa07951f5fcf77322ff1f4e688594e2821a20e3986d8c433b092f360930fed95d9ff206cee9070120e3ddceca1ce2221c5493dc892f1df87ae3f9a45f3e3f29dd41f852daaec9cd7b9bbe754cab9c18b0a4d0f8687915befadb27cce7ca9fd4f6061b43295568792eabd82a885ddd34ced64e9b3473b82069de6571f1bf8c292e5c599fbd37ce1103f8f95c0f644b091ba227706c53a1952959a1685a410221d374924079144d8da9536a4bbab8e9af570468c81059a78a59d212b6a07883bc5f04adf019ab922e2ab1ee23ebb3cba0e4e2987e81538b385fa8dd28486a05d53f128dfc9d18ab6bd1f2b7c92abf447eab70d3f4a73279c5fac6ec0e499cb07f4c03613836361f39cffdb75a09744b4bb37bf8d54967d0bee745bb0f39a7397faab9a79cd7b81fc2ee089814a8c18198fbf3d5d7a0e7823305bfc5339e4c61ccb64eee822acf9bc6a82e79fbce091ec91daac508970ef20e8bd4b3c2aa3dc3cd5af676d0fcfa2f4339f68a52a4f81087a2807fc3aa701bbbf80f92e8e1a3e458fe558c99d34ae94de21b211f6402606daaa1791c3be5f94730b3fa9d3e99ae34fc5682127c58fd4eb4d5b1e8f8b2848b3dbb0c1556d2c6043cceee5952e0a4f2b83c2b21ed472fd596d0f3b74e56d640f65b7cf471959c1d90d46986e598b49cd799d0793d6397f8e295a908301728291a68a14df561735", + "mirrors": [], + "enabled": true + }, + { + "name": "Guardian Project", + "address": "https://guardianproject.info/fdroid/repo", + "description": "The official app repository of The Guardian Project. Applications in this repository are official binaries build by the original application developers and signed by the same key as the APKs that are released in the Google Play store.", + "certificate": "308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d01010505003081ad310b30090603550406130255533111300f06035504080c084e657720596f726b3111300f06035504070c084e657720596f726b31143012060355040b0c0b4644726f6964205265706f31193017060355040a0c10477561726469616e2050726f6a656374311d301b06035504030c14677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d0109011619726f6f7440677561726469616e70726f6a6563742e696e666f301e170d3134303632363139333931385a170d3431313131303139333931385a3081ad310b30090603550406130255533111300f06035504080c084e657720596f726b3111300f06035504070c084e657720596f726b31143012060355040b0c0b4644726f6964205265706f31193017060355040a0c10477561726469616e2050726f6a656374311d301b06035504030c14677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d0109011619726f6f7440677561726469616e70726f6a6563742e696e666f30820222300d06092a864886f70d01010105000382020f003082020a0282020100b3cd79121b9b883843be3c4482e320809106b0a23755f1dd3c7f46f7d315d7bb2e943486d61fc7c811b9294dcc6b5baac4340f8db2b0d5e14749e7f35e1fc211fdbc1071b38b4753db201c314811bef885bd8921ad86facd6cc3b8f74d30a0b6e2e6e576f906e9581ef23d9c03e926e06d1f033f28bd1e21cfa6a0e3ff5c9d8246cf108d82b488b9fdd55d7de7ebb6a7f64b19e0d6b2ab1380a6f9d42361770d1956701a7f80e2de568acd0bb4527324b1e0973e89595d91c8cc102d9248525ae092e2c9b69f7414f724195b81427f28b1d3d09a51acfe354387915fd9521e8c890c125fc41a12bf34d2a1b304067ab7251e0e9ef41833ce109e76963b0b256395b16b886bca21b831f1408f836146019e7908829e716e72b81006610a2af08301de5d067c9e114a1e5759db8a6be6a3cc2806bcfe6fafd41b5bc9ddddb3dc33d6f605b1ca7d8a9e0ecdd6390d38906649e68a90a717bea80fa220170eea0c86fc78a7e10dac7b74b8e62045a3ecca54e035281fdc9fe5920a855fde3c0be522e3aef0c087524f13d973dff3768158b01a5800a060c06b451ec98d627dd052eda804d0556f60dbc490d94e6e9dea62ffcafb5beffbd9fc38fb2f0d7050004fe56b4dda0a27bc47554e1e0a7d764e17622e71f83a475db286bc7862deee1327e2028955d978272ea76bf0b88e70a18621aba59ff0c5993ef5f0e5d6b6b98e68b70203010001300d06092a864886f70d0101050500038202010079c79c8ef408a20d243d8bd8249fb9a48350dc19663b5e0fce67a8dbcb7de296c5ae7bbf72e98a2020fb78f2db29b54b0e24b181aa1c1d333cc0303685d6120b03216a913f96b96eb838f9bff125306ae3120af838c9fc07ebb5100125436bd24ec6d994d0bff5d065221871f8410daf536766757239bf594e61c5432c9817281b985263bada8381292e543a49814061ae11c92a316e7dc100327b59e3da90302c5ada68c6a50201bda1fcce800b53f381059665dbabeeb0b50eb22b2d7d2d9b0aa7488ca70e67ac6c518adb8e78454a466501e89d81a45bf1ebc350896f2c3ae4b6679ecfbf9d32960d4f5b493125c7876ef36158562371193f600bc511000a67bdb7c664d018f99d9e589868d103d7e0994f166b2ba18ff7e67d8c4da749e44dfae1d930ae5397083a51675c409049dfb626a96246c0015ca696e94ebb767a20147834bf78b07fece3f0872b057c1c519ff882501995237d8206b0b3832f78753ebd8dcbd1d3d9f5ba733538113af6b407d960ec4353c50eb38ab29888238da843cd404ed8f4952f59e4bbc0035fc77a54846a9d419179c46af1b4a3b7fc98e4d312aaa29b9b7d79e739703dc0fa41c7280d5587709277ffa11c3620f5fba985b82c238ba19b17ebd027af9424be0941719919f620dd3bb3c3f11638363708aa11f858e153cf3a69bce69978b90e4a273836100aa1e617ba455cd00426847f", + "mirrors": ["https://s3.amazonaws.com/guardianproject/fdroid/repo"], + "enabled": false + } + ] + """ + .trimIndent() + + private val allRomRepoPaths = + listOf("/system", "/system_ext", "/product", "/vendor").flatMap { root -> + listOf(packageName, "fdroid").map { subdir -> "$root/etc/$subdir/additional_repos.json" } + } + + @Test + fun `adds default repos from assets`() { + // Override default empty JSON with two real repos + every { assetManager.open("default_repos.json") } returns testReposJson.byteInputStream() + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + val names = capturedRepoList.map { it.name } + assertTrue("CalyxOS Local Repository" in names) + assertTrue("Guardian Project" in names) + } + + @Test + fun `picks up file from system etc fdroid`() { + mockFileAtPath("/system/etc/fdroid/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + assertTrue(capturedRepoList.any { it.name == "CalyxOS Local Repository" }) + assertTrue(capturedRepoList.any { it.name == "Guardian Project" }) + } + + @Test + fun `picks up file from system etc package name`() { + mockFileAtPath("/system/etc/$packageName/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + assertTrue(capturedRepoList.any { it.name == "CalyxOS Local Repository" }) + assertTrue(capturedRepoList.any { it.name == "Guardian Project" }) + } + + @Test + fun `picks up file from system_ext etc fdroid`() { + mockFileAtPath("/system_ext/etc/fdroid/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + assertTrue(capturedRepoList.any { it.name == "CalyxOS Local Repository" }) + } + + @Test + fun `picks up file from system_ext etc package name`() { + mockFileAtPath("/system_ext/etc/$packageName/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + assertTrue(capturedRepoList.any { it.name == "Guardian Project" }) + } + + @Test + fun `picks up file from product etc fdroid`() { + mockFileAtPath("/product/etc/fdroid/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + assertTrue(capturedRepoList.any { it.name == "CalyxOS Local Repository" }) + } + + @Test + fun `picks up file from product etc package name`() { + mockFileAtPath("/product/etc/$packageName/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + assertTrue(capturedRepoList.any { it.name == "Guardian Project" }) + } + + @Test + fun `picks up file from vendor etc fdroid`() { + mockFileAtPath("/vendor/etc/fdroid/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + assertTrue(capturedRepoList.any { it.name == "CalyxOS Local Repository" }) + } + + @Test + fun `picks up file from vendor etc package name`() { + mockFileAtPath("/vendor/etc/$packageName/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + assertTrue(capturedRepoList.any { it.name == "Guardian Project" }) + } + + @Test + fun `picks up files from all 8 possible locations`() { + mockAllRomRepoPaths() + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + // 8 locations * 2 repos per file = 16 inserts (plus 0 from empty default_repos.json) + verify(exactly = 16) { repositoryDao.insert(capture(capturedRepoList)) } + + // Every path should have contributed both repos + assertEquals(16, capturedRepoList.size) + assertEquals(8, capturedRepoList.count { it.name == "CalyxOS Local Repository" }) + assertEquals(8, capturedRepoList.count { it.name == "Guardian Project" }) + } + + @Test + fun `maps all fields from json to InitialRepository`() { + mockFileAtPath("/product/etc/fdroid/additional_repos.json") + + val capturedRepoList = mutableListOf() + repoPreLoader.addPreloadedRepositories(db) + verify(exactly = 2) { repositoryDao.insert(capture(capturedRepoList)) } + + val calyx = capturedRepoList.first { it.name == "CalyxOS Local Repository" } + assertEquals("CalyxOS Local Repository", calyx.name) + assertEquals("file:///product/fdroid/repo/", calyx.address) + assertEquals( + "This is a repository of apps to be used with CalyxOS. It is installed to the system partition, and meant to be used to initially install apps when offline. It contains a limited number of applications deemed suitable for inclusion with CalyxOS.", + calyx.description, + ) + assertEquals(emptyList(), calyx.mirrors) + assertTrue(calyx.certificate.isNotEmpty()) + assertEquals(true, calyx.enabled) + + val guardian = capturedRepoList.first { it.name == "Guardian Project" } + assertEquals("Guardian Project", guardian.name) + assertEquals("https://guardianproject.info/fdroid/repo", guardian.address) + assertEquals( + "The official app repository of The Guardian Project. Applications in this repository are official binaries build by the original application developers and signed by the same key as the APKs that are released in the Google Play store.", + guardian.description, + ) + assertEquals(listOf("https://s3.amazonaws.com/guardianproject/fdroid/repo"), guardian.mirrors) + assertTrue(guardian.certificate.isNotEmpty()) + assertEquals(false, guardian.enabled) + } + + @Test + fun `does not insert when no rom repos files are present`() { + // fileFactory returns a non-existent file for all paths and default_repos.json is empty + repoPreLoader.addPreloadedRepositories(db) + + verify(exactly = 0) { repositoryDao.insert(any()) } + } + + @Test + fun `handles invalid json gracefully without crashing`() { + val badFile = tmpFolder.newFile().apply { writeText("{ not valid json at all }") } + + every { fileFactory("/vendor/etc/fdroid/additional_repos.json") } returns badFile + + // Should not throw, the error is logged and the bad file is skipped + repoPreLoader.addPreloadedRepositories(db) + + verify(exactly = 0) { repositoryDao.insert(any()) } + } + + @Test + fun `defaultRepoAddresses returns addresses from default_repos json`() { + every { assetManager.open("default_repos.json") } returns testReposJson.byteInputStream() + + val addresses = repoPreLoader.defaultRepoAddresses + + assertEquals( + setOf("file:///product/fdroid/repo/", "https://guardianproject.info/fdroid/repo"), + addresses, + ) + } + + @Test + fun `defaultRepoAddresses is empty when default_repos json is empty`() { + // default is already "[]" from setUp + assertTrue(repoPreLoader.defaultRepoAddresses.isEmpty()) + } + + /** Mocks a file at the specific ROM repo path filled with our [testReposJson]. */ + private fun mockFileAtPath(path: String) { + val tempFile = tmpFolder.newFile().apply { writeText(testReposJson) } + every { fileFactory(path) } returns tempFile + } + + /** Mocks a file at *all* possible ROM repo paths filled with our [testReposJson]. */ + private fun mockAllRomRepoPaths() { + val tempFile = tmpFolder.newFile().apply { writeText(testReposJson) } + allRomRepoPaths.forEach { path -> every { fileFactory(path) } returns tempFile } + } +} From 21352ee64a22ceadc42265e80d3ede309a7b6ff5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 27 Mar 2026 16:09:27 -0300 Subject: [PATCH 02/30] Basic support for restoring archived apps Since noone seems to be using this feature, there's some TODOs left in the code for when this changes. --- app/src/main/AndroidManifest.xml | 8 ++ .../install/UnarchivePackageReceiver.kt | 31 ++++++ .../org/fdroid/install/UnarchiveWorker.kt | 96 +++++++++++++++++++ .../org/fdroid/updates/UpdatesModule.kt | 12 +++ 4 files changed, 147 insertions(+) create mode 100644 app/src/main/kotlin/org/fdroid/install/UnarchivePackageReceiver.kt create mode 100644 app/src/main/kotlin/org/fdroid/install/UnarchiveWorker.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ccaa1f88a..867162373 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -135,6 +135,14 @@ android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> + + + + + + diff --git a/app/src/main/kotlin/org/fdroid/install/UnarchivePackageReceiver.kt b/app/src/main/kotlin/org/fdroid/install/UnarchivePackageReceiver.kt new file mode 100644 index 000000000..e982f83e2 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/install/UnarchivePackageReceiver.kt @@ -0,0 +1,31 @@ +package org.fdroid.install + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_UNARCHIVE_PACKAGE +import android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_ALL_USERS +import android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_ID +import android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_PACKAGE_NAME +import androidx.annotation.RequiresApi +import mu.KotlinLogging + +class UnarchivePackageReceiver : BroadcastReceiver() { + + private val log = KotlinLogging.logger {} + + @RequiresApi(35) + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION_UNARCHIVE_PACKAGE) { + log.warn { "Unknown action: ${intent.action}" } + return + } + val packageName = intent.getStringExtra(EXTRA_UNARCHIVE_PACKAGE_NAME) ?: error("") + val unarchiveId = intent.getIntExtra(EXTRA_UNARCHIVE_ID, -1) + val allUsers = intent.getBooleanExtra(EXTRA_UNARCHIVE_ALL_USERS, false) + + log.info { "Intent received, un-archiving $packageName..." } + + UnarchiveWorker.updateNow(context, packageName, unarchiveId, allUsers) + } +} diff --git a/app/src/main/kotlin/org/fdroid/install/UnarchiveWorker.kt b/app/src/main/kotlin/org/fdroid/install/UnarchiveWorker.kt new file mode 100644 index 000000000..9cb8a85c5 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/install/UnarchiveWorker.kt @@ -0,0 +1,96 @@ +package org.fdroid.install + +import android.content.Context +import android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_ALL_USERS +import android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_ID +import android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_PACKAGE_NAME +import androidx.annotation.RequiresApi +import androidx.annotation.UiThread +import androidx.hilt.work.HiltWorker +import androidx.lifecycle.asFlow +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.first +import mu.KotlinLogging +import org.fdroid.database.DbUpdateChecker +import org.fdroid.database.FDroidDatabase +import org.fdroid.index.RepoManager + +@HiltWorker +@RequiresApi(35) +class UnarchiveWorker +@AssistedInject +constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + private val db: FDroidDatabase, + private val repoManager: RepoManager, + private val dbUpdateChecker: DbUpdateChecker, + private val appInstallManager: AppInstallManager, +) : CoroutineWorker(appContext, workerParams) { + + private val log = KotlinLogging.logger {} + + companion object { + @UiThread + fun updateNow(context: Context, packageName: String, unarchiveId: Int, allUsers: Boolean) { + val data = + Data.Builder() + .putString(EXTRA_UNARCHIVE_PACKAGE_NAME, packageName) + .putInt(EXTRA_UNARCHIVE_ID, unarchiveId) + .putBoolean(EXTRA_UNARCHIVE_ALL_USERS, allUsers) + .build() + val request = + OneTimeWorkRequestBuilder() + .setExpedited(RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setInputData(data) + .build() + WorkManager.getInstance(context).enqueue(request) + } + } + + override suspend fun doWork(): Result { + val packageName = inputData.getString(EXTRA_UNARCHIVE_PACKAGE_NAME) ?: error("No packageName") + // TODO actually use in the two values below + val unarchiveId = inputData.getInt(EXTRA_UNARCHIVE_ID, -1) + val allUsers = inputData.getBoolean(EXTRA_UNARCHIVE_ALL_USERS, false) + + log.info { "Unarchiving $packageName with unarchiveId $unarchiveId for allUsers=$allUsers" } + + // find suggested version for that app + val appPrefs = db.getAppPrefsDao().getAppPrefs(packageName).asFlow().first() + val version = + dbUpdateChecker.getSuggestedVersion( + packageName = packageName, + // TODO we could try to get the old signer (if still available) and search for the same + preferredSigner = null, + releaseChannels = appPrefs.releaseChannels, + onlyFromPreferredRepo = true, + ) + + // install version, if available + return if (version == null) { + log.error { "Could not find a version to unarchive for $packageName" } + Result.failure() + } else { + // install app + // TODO we could do better error handling, e.g. when metadata or repo are null + // or install state is an error, also maybe show a Toast to user on error + appInstallManager.install( + appMetadata = db.getAppDao().getApp(version.repoId, packageName)?.metadata, + version = version, + currentVersionName = null, + repo = repoManager.getRepository(version.repoId), + iconModel = null, + canAskPreApprovalNow = true, + ) + Result.success() + } + } +} diff --git a/app/src/main/kotlin/org/fdroid/updates/UpdatesModule.kt b/app/src/main/kotlin/org/fdroid/updates/UpdatesModule.kt index 4afb828a2..5ffd56641 100644 --- a/app/src/main/kotlin/org/fdroid/updates/UpdatesModule.kt +++ b/app/src/main/kotlin/org/fdroid/updates/UpdatesModule.kt @@ -11,6 +11,7 @@ import org.fdroid.CompatibilityChecker import org.fdroid.CompatibilityCheckerImpl import org.fdroid.UpdateChecker import org.fdroid.database.DbAppChecker +import org.fdroid.database.DbUpdateChecker import org.fdroid.database.FDroidDatabase @Module @@ -38,4 +39,15 @@ object UpdatesModule { ): DbAppChecker { return DbAppChecker(db, context, compatibilityChecker, updateChecker) } + + @Provides + @Singleton + fun provideDbUpdateChecker( + @ApplicationContext context: Context, + db: FDroidDatabase, + updateChecker: UpdateChecker, + compatibilityChecker: CompatibilityChecker, + ): DbUpdateChecker { + return DbUpdateChecker(db, context.packageManager, compatibilityChecker, updateChecker) + } } From ac4de23691e052db133f51204b8fecc4527abeca Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 31 Mar 2026 09:34:46 -0300 Subject: [PATCH 03/30] [index] add file to PackageVersion --- libs/index/api/jvm/index.api | 3 ++- .../src/androidHostTest/kotlin/org/fdroid/UpdateCheckerTest.kt | 2 ++ .../src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/index/api/jvm/index.api b/libs/index/api/jvm/index.api index 9356deeb6..9fcbe9b86 100644 --- a/libs/index/api/jvm/index.api +++ b/libs/index/api/jvm/index.api @@ -861,6 +861,7 @@ public final class org/fdroid/index/v2/PackageV2Kt { public abstract interface class org/fdroid/index/v2/PackageVersion { public abstract fun getAdded ()J + public abstract fun getFile ()Lorg/fdroid/index/v2/FileV1; public abstract fun getHasKnownVulnerability ()Z public abstract fun getPackageManifest ()Lorg/fdroid/index/v2/PackageManifest; public abstract fun getReleaseChannels ()Ljava/util/List; @@ -886,7 +887,7 @@ public final class org/fdroid/index/v2/PackageVersionV2 : org/fdroid/index/v2/Pa public fun equals (Ljava/lang/Object;)Z public fun getAdded ()J public final fun getAntiFeatures ()Ljava/util/Map; - public final fun getFile ()Lorg/fdroid/index/v2/FileV1; + public fun getFile ()Lorg/fdroid/index/v2/FileV1; public fun getHasKnownVulnerability ()Z public final fun getManifest ()Lorg/fdroid/index/v2/ManifestV2; public fun getPackageManifest ()Lorg/fdroid/index/v2/PackageManifest; diff --git a/libs/index/src/androidHostTest/kotlin/org/fdroid/UpdateCheckerTest.kt b/libs/index/src/androidHostTest/kotlin/org/fdroid/UpdateCheckerTest.kt index 37affbbdb..66298fa80 100644 --- a/libs/index/src/androidHostTest/kotlin/org/fdroid/UpdateCheckerTest.kt +++ b/libs/index/src/androidHostTest/kotlin/org/fdroid/UpdateCheckerTest.kt @@ -4,6 +4,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull import org.fdroid.index.RELEASE_CHANNEL_BETA +import org.fdroid.index.v2.FileV1 import org.fdroid.index.v2.PackageManifest import org.fdroid.index.v2.PackageVersion import org.fdroid.index.v2.SignerV2 @@ -190,6 +191,7 @@ internal class UpdateCheckerTest { override val targetSdkVersion: Int? = null }, override val hasKnownVulnerability: Boolean = false, + override val file: FileV1 = FileV1("foo/bar", "abcd", 23), ) : PackageVersion private data class AppPreferences( diff --git a/libs/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt b/libs/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt index 97a805e47..8047d501c 100644 --- a/libs/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt +++ b/libs/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt @@ -77,6 +77,7 @@ public data class Screenshots( public interface PackageVersion { public val versionCode: Long public val versionName: String + public val file: FileV1 public val added: Long public val size: Long? public val signer: SignerV2? @@ -90,7 +91,7 @@ public const val ANTI_FEATURE_KNOWN_VULNERABILITY: String = "KnownVuln" @Serializable public data class PackageVersionV2( override val added: Long, - val file: FileV1, + override val file: FileV1, val src: FileV2? = null, val manifest: ManifestV2, override val releaseChannels: List = emptyList(), From 8535a89bfc8db929813ed0a74e4ef73d524d761e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 31 Mar 2026 09:35:04 -0300 Subject: [PATCH 04/30] [db] add file to PackageVersion --- libs/database/src/main/java/org/fdroid/database/Version.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/database/src/main/java/org/fdroid/database/Version.kt b/libs/database/src/main/java/org/fdroid/database/Version.kt index da953cb78..d4ad1e3ec 100644 --- a/libs/database/src/main/java/org/fdroid/database/Version.kt +++ b/libs/database/src/main/java/org/fdroid/database/Version.kt @@ -45,7 +45,7 @@ internal data class Version( val packageName: String, val versionId: String, override val added: Long, - @Embedded(prefix = "file_") val file: FileV1, + @Embedded(prefix = "file_") override val file: FileV1, @Embedded(prefix = "src_") val src: FileV2? = null, @Embedded(prefix = "manifest_") val manifest: AppManifest, override val releaseChannels: List? = emptyList(), @@ -126,7 +126,7 @@ internal constructor( public val manifest: AppManifest get() = version.manifest - public val file: FileV1 + public override val file: FileV1 get() = version.file public val src: FileV2? From 54e69a49faabc925aed6073ec55ec952656ddd89 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 31 Mar 2026 09:34:46 -0300 Subject: [PATCH 05/30] Use more generic PackageVersion in AppInstallManager --- .../org/fdroid/install/AppInstallManager.kt | 100 ++++++------ .../kotlin/org/fdroid/install/InstallState.kt | 4 +- .../fdroid/install/SessionInstallManager.kt | 14 +- .../org/fdroid/install/UnarchiveWorker.kt | 13 +- .../org/fdroid/ui/details/AppDetailsItem.kt | 6 +- .../fdroid/ui/details/AppDetailsViewModel.kt | 9 +- .../org/fdroid/ui/utils/PreviewUtils.kt | 4 + .../org/fdroid/updates/UpdateInstaller.kt | 13 +- .../fdroid/install/AppInstallManagerTest.kt | 26 +-- .../install/SessionInstallManagerTest.kt | 13 +- .../org/fdroid/updates/UpdateInstallerTest.kt | 153 ++++++++++-------- 11 files changed, 190 insertions(+), 165 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/install/AppInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/AppInstallManager.kt index 3f176ddae..96aae5f43 100644 --- a/app/src/main/kotlin/org/fdroid/install/AppInstallManager.kt +++ b/app/src/main/kotlin/org/fdroid/install/AppInstallManager.kt @@ -31,13 +31,13 @@ import mu.KotlinLogging import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.NotificationManager import org.fdroid.database.AppMetadata -import org.fdroid.database.AppVersion import org.fdroid.database.Repository import org.fdroid.download.DownloaderFactory import org.fdroid.download.getUri import org.fdroid.history.HistoryManager import org.fdroid.history.InstallEvent import org.fdroid.history.UninstallEvent +import org.fdroid.index.v2.PackageVersion import org.fdroid.utils.IoDispatcher @Singleton @@ -81,18 +81,17 @@ constructor( else -> null } // track app state for in progress apps - val appState = - appStateCategory?.let { - // all states that get a category above must be InstallStateWithInfo - state as InstallStateWithInfo - AppState( - packageName = packageName, - category = it, - name = state.name, - installVersionName = state.versionName, - currentVersionName = state.currentVersionName, - ) - } + val appState = appStateCategory?.let { + // all states that get a category above must be InstallStateWithInfo + state as InstallStateWithInfo + AppState( + packageName = packageName, + category = it, + name = state.name, + installVersionName = state.versionName, + currentVersionName = state.currentVersionName, + ) + } if (appState != null) appStates.add(appState) } return InstallNotificationState( @@ -116,19 +115,20 @@ constructor( */ @UiThread suspend fun install( + packageName: String, appMetadata: AppMetadata?, - version: AppVersion, + version: PackageVersion, currentVersionName: String?, repo: Repository?, iconModel: Any?, canAskPreApprovalNow: Boolean, ): InstallState { if (appMetadata == null || repo == null) { - log.error { "Can't install app without metadata for ${version.packageName}" } + log.error { "Can't install app without metadata for $packageName" } val error = InstallState.Error( - msg = "App ${version.packageName} no longer in DB.", - name = version.packageName, + msg = "App $packageName no longer in DB.", + name = packageName, versionName = version.versionName, currentVersionName = currentVersionName, lastUpdated = version.added, @@ -136,27 +136,25 @@ constructor( ) // Write the terminal state so any prior Waiting state is cleared and the // service stop logic in onStatesUpdated() has a chance to run. - updateAppState(version.packageName, error) + updateAppState(packageName, error) return error } - val packageName = appMetadata.packageName val currentState = apps.value[packageName] if (currentState?.showProgress == true && currentState !is InstallState.Waiting) { log.warn { "Attempted to install $packageName with install in progress: $currentState" } return currentState } currentCoroutineContext().ensureActive() - val job = - scope.async { - startInstall( - appMetadata = appMetadata, - version = version, - currentVersionName = currentVersionName, - repo = repo, - iconModel = iconModel, - canAskPreApprovalNow = canAskPreApprovalNow, - ) - } + val job = scope.async { + startInstall( + appMetadata = appMetadata, + version = version, + currentVersionName = currentVersionName, + repo = repo, + iconModel = iconModel, + canAskPreApprovalNow = canAskPreApprovalNow, + ) + } // keep track of this job, in case we want to cancel it return trackJob(packageName, job) } @@ -212,7 +210,7 @@ constructor( @WorkerThread private suspend fun startInstall( appMetadata: AppMetadata, - version: AppVersion, + version: PackageVersion, currentVersionName: String?, repo: Repository, iconModel: Any?, @@ -256,7 +254,14 @@ constructor( ) } as InstallState.PreApproved - downloadAndInstall(newState, version, currentVersionName, repo, iconModel) + downloadAndInstall( + state = newState, + packageName = appMetadata.packageName, + version = version, + currentVersionName = currentVersionName, + repo = repo, + iconModel = iconModel, + ) } is PreApprovalResult.UserConfirmationRequired -> { InstallState.PreApprovalConfirmationNeeded( @@ -295,16 +300,16 @@ constructor( updateAppState(packageName, result) return if (result is InstallState.PreApproved) { // move us off the UiThread, so we can download/install this app now - val job = - scope.async { - downloadAndInstall( - state = result, - version = installState.version, - currentVersionName = installState.currentVersionName, - repo = installState.repo, - iconModel = installState.iconModel, - ) - } + val job = scope.async { + downloadAndInstall( + state = result, + packageName = packageName, + version = installState.version, + currentVersionName = installState.currentVersionName, + repo = installState.repo, + iconModel = installState.iconModel, + ) + } // suspend/wait for this job and track it in case we want to cancel it trackJob(packageName, job) } else result @@ -313,7 +318,8 @@ constructor( @WorkerThread private suspend fun downloadAndInstall( state: InstallState.PreApproved, - version: AppVersion, + packageName: String, + version: PackageVersion, currentVersionName: String?, repo: Repository, iconModel: Any?, @@ -328,7 +334,7 @@ constructor( val now = System.currentTimeMillis() downloader.setListener { bytesRead, totalBytes -> coroutineContext.ensureActive() - updateAndGetAppState(version.packageName) { + updateAndGetAppState(packageName) { InstallState.Downloading( name = it.name, versionName = it.versionName, @@ -359,7 +365,7 @@ constructor( } currentCoroutineContext().ensureActive() val newState = - updateAndGetAppState(version.packageName) { + updateAndGetAppState(packageName) { InstallState.Installing( name = it.name, versionName = it.versionName, @@ -368,12 +374,12 @@ constructor( iconModel = it.iconModel, ) } - val result = sessionInstallManager.install(sessionId, version.packageName, newState, file) + val result = sessionInstallManager.install(sessionId, packageName, newState, file) log.debug { "Install result: $result" } return if (result is InstallState.PreApproved && result.result is PreApprovalResult.Error) { // if pre-approval failed (e.g. due to app label mismatch), // then try to install again, this time not using the pre-approved session - sessionInstallManager.install(null, version.packageName, newState, file) + sessionInstallManager.install(null, packageName, newState, file) } else { result } diff --git a/app/src/main/kotlin/org/fdroid/install/InstallState.kt b/app/src/main/kotlin/org/fdroid/install/InstallState.kt index 45fa6aa3a..30bec9a72 100644 --- a/app/src/main/kotlin/org/fdroid/install/InstallState.kt +++ b/app/src/main/kotlin/org/fdroid/install/InstallState.kt @@ -1,8 +1,8 @@ package org.fdroid.install import android.app.PendingIntent -import org.fdroid.database.AppVersion import org.fdroid.database.Repository +import org.fdroid.index.v2.PackageVersion sealed class InstallState(val showProgress: Boolean) { data object Unknown : InstallState(false) @@ -30,7 +30,7 @@ sealed class InstallState(val showProgress: Boolean) { data class PreApprovalConfirmationNeeded( private val state: InstallStateWithInfo, - val version: AppVersion, + val version: PackageVersion, val repo: Repository, override val sessionId: Int, override val creationTimeMillis: Long = System.currentTimeMillis(), diff --git a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt index 3c57d4030..dfe3856ba 100644 --- a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt +++ b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import mu.KotlinLogging import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.database.AppMetadata -import org.fdroid.database.AppVersion +import org.fdroid.index.v2.PackageVersion import org.fdroid.ui.utils.isAppInForeground import org.fdroid.utils.IoDispatcher @@ -93,13 +93,13 @@ constructor( app: AppMetadata, iconGetter: suspend () -> Bitmap?, isUpdate: Boolean, - version: AppVersion, + version: PackageVersion, canRequestUserConfirmationNow: Boolean, ): PreApprovalResult { return if (!context.isAppInForeground()) { log.info { "App not in foreground, pre-approval for ${app.packageName} not supported." } PreApprovalResult.NotSupported - } else if (isUpdate && canDoAutoUpdate(version)) { + } else if (isUpdate && canDoAutoUpdate(app.packageName, version)) { // should not be needed, so we say not supported log.info { "Can do auto-update pre-approval for ${app.packageName} not needed." } PreApprovalResult.NotSupported @@ -368,17 +368,17 @@ constructor( return params } - private fun canDoAutoUpdate(version: AppVersion): Boolean { + private fun canDoAutoUpdate(packageName: String, version: PackageVersion): Boolean { if (SDK_INT < 31) return false - val targetSdkVersion = version.manifest.targetSdkVersion ?: return false + val targetSdkVersion = version.packageManifest.targetSdkVersion ?: return false // docs: // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int) return if (isAutoUpdateSupported(targetSdkVersion)) { val ourPackageName = context.packageName - if (ourPackageName == version.packageName) return true + if (ourPackageName == packageName) return true val sourceInfo = try { - context.packageManager.getInstallSourceInfo(version.packageName) + context.packageManager.getInstallSourceInfo(packageName) } catch (e: Exception) { log.error(e) { "Could not get package info: " } return false diff --git a/app/src/main/kotlin/org/fdroid/install/UnarchiveWorker.kt b/app/src/main/kotlin/org/fdroid/install/UnarchiveWorker.kt index 9cb8a85c5..95ba23dec 100644 --- a/app/src/main/kotlin/org/fdroid/install/UnarchiveWorker.kt +++ b/app/src/main/kotlin/org/fdroid/install/UnarchiveWorker.kt @@ -83,12 +83,13 @@ constructor( // TODO we could do better error handling, e.g. when metadata or repo are null // or install state is an error, also maybe show a Toast to user on error appInstallManager.install( - appMetadata = db.getAppDao().getApp(version.repoId, packageName)?.metadata, - version = version, - currentVersionName = null, - repo = repoManager.getRepository(version.repoId), - iconModel = null, - canAskPreApprovalNow = true, + packageName = packageName, + appMetadata = db.getAppDao().getApp(version.repoId, packageName)?.metadata, + version = version, + currentVersionName = null, + repo = repoManager.getRepository(version.repoId), + iconModel = null, + canAskPreApprovalNow = true, ) Result.success() } diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt index 787109839..e8465073d 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt @@ -53,7 +53,7 @@ data class AppDetailsItem( */ val installedSigner: String? = null, /** The currently suggested version for installation. */ - val suggestedVersion: AppVersion? = null, + val suggestedVersion: PackageVersion? = null, /** * Similar to [suggestedVersion], but doesn't obey [appPrefs] for ignoring versions. This is * useful for (un-)ignoring this version. @@ -225,8 +225,8 @@ data class AppDetailsItem( val bitcoinUri = app.bitcoin?.let { "bitcoin:$it" } } -class AppDetailsActions( - val installAction: (AppMetadata, AppVersion, Any?) -> Unit, +data class AppDetailsActions( + val installAction: (AppMetadata, PackageVersion, Any?) -> Unit, val requestUserConfirmation: (InstallState.UserConfirmationNeeded) -> Unit, /** * A workaround for Android 10, 11, 12 and 13 where tapping outside the confirmation dialog diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index 7c5e37d3a..01ae0b543 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -31,13 +31,13 @@ import kotlinx.coroutines.withContext import mu.KotlinLogging import org.fdroid.UpdateChecker import org.fdroid.database.AppMetadata -import org.fdroid.database.AppVersion import org.fdroid.database.FDroidDatabase import org.fdroid.download.DownloadRequest import org.fdroid.download.NetworkMonitor import org.fdroid.getCacheKey import org.fdroid.index.RELEASE_CHANNEL_BETA import org.fdroid.index.RepoManager +import org.fdroid.index.v2.PackageVersion import org.fdroid.install.AppInstallManager import org.fdroid.install.InstallState import org.fdroid.repo.RepoPreLoader @@ -116,14 +116,15 @@ constructor( } @UiThread - fun install(appMetadata: AppMetadata, version: AppVersion, iconModel: Any?) { + fun install(appMetadata: AppMetadata, version: PackageVersion, iconModel: Any?) { scope.launch(Dispatchers.Main) { val result = appInstallManager.install( + packageName = packageName, appMetadata = appMetadata, version = version, - currentVersionName = packageInfoFlow.value?.packageInfo?.versionName, - repo = repoManager.getRepository(version.repoId) ?: return@launch, // TODO + currentVersionName = packageInfoFlow.value?.packageInfo?.versionName, // TODO + repo = repoManager.getRepository(appMetadata.repoId) ?: return@launch, iconModel = iconModel, canAskPreApprovalNow = true, ) diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 968605bb6..50095e104 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -13,6 +13,7 @@ import org.fdroid.database.Repository import org.fdroid.download.Mirror import org.fdroid.download.NetworkState import org.fdroid.index.IndexFormatVersion +import org.fdroid.index.v2.FileV1 import org.fdroid.index.v2.PackageManifest import org.fdroid.index.v2.PackageVersion import org.fdroid.index.v2.SignerV2 @@ -81,6 +82,7 @@ val testVersion1 = object : PackageVersion { override val versionCode: Long = 42 override val versionName: String = "42.23.0-alpha1337-33d2252b90" + override val file: FileV1 = FileV1("foo/bar", "abcd", 23) override val added: Long = System.currentTimeMillis() - DAYS.toMillis(4) override val size: Long = 1024 * 1024 * 42 override val signer: SignerV2 = @@ -100,6 +102,7 @@ val testVersion2 = object : PackageVersion { override val versionCode: Long = 23 override val versionName: String = "23.42.0" + override val file: FileV1 = FileV1("foo/bar", "abcd", 23) override val added: Long = System.currentTimeMillis() - DAYS.toMillis(4) override val size: Long = 1024 * 1024 * 23 override val signer: SignerV2 = @@ -252,6 +255,7 @@ fun getPreviewVersion(versionName: String, size: Long? = null) = object : PackageVersion { override val versionCode: Long = 23 override val versionName: String = versionName + override val file: FileV1 = FileV1("foo/bar", "abcd", 23) override val added: Long = System.currentTimeMillis() - DAYS.toMillis(3) override val size: Long? = size override val signer: SignerV2? = null diff --git a/app/src/main/kotlin/org/fdroid/updates/UpdateInstaller.kt b/app/src/main/kotlin/org/fdroid/updates/UpdateInstaller.kt index 85e957551..fa9b2d5af 100644 --- a/app/src/main/kotlin/org/fdroid/updates/UpdateInstaller.kt +++ b/app/src/main/kotlin/org/fdroid/updates/UpdateInstaller.kt @@ -114,12 +114,13 @@ constructor( private suspend fun updateApp(update: AppUpdateItem, canAskPreApprovalNow: Boolean) { val app = db.getAppDao().getApp(update.repoId, update.packageName) appInstallManager.install( - appMetadata = app?.metadata, - version = update.update as AppVersion, - currentVersionName = update.installedVersionName, - repo = repoManager.getRepository(update.repoId), - iconModel = update.iconModel, - canAskPreApprovalNow = canAskPreApprovalNow, + packageName = update.packageName, + appMetadata = app?.metadata, + version = update.update as AppVersion, + currentVersionName = update.installedVersionName, + repo = repoManager.getRepository(update.repoId), + iconModel = update.iconModel, + canAskPreApprovalNow = canAskPreApprovalNow, ) } } diff --git a/app/src/test/java/org/fdroid/install/AppInstallManagerTest.kt b/app/src/test/java/org/fdroid/install/AppInstallManagerTest.kt index 59202b07f..8c4461bc7 100644 --- a/app/src/test/java/org/fdroid/install/AppInstallManagerTest.kt +++ b/app/src/test/java/org/fdroid/install/AppInstallManagerTest.kt @@ -624,12 +624,13 @@ internal class AppInstallManagerTest { val installJob = installScope.async { appInstallManager.install( - appMetadata = appMetadata, - version = version, - currentVersionName = installedVersionName, - repo = repo, - iconModel = null, - canAskPreApprovalNow = false, + packageName = packageName, + appMetadata = appMetadata, + version = version, + currentVersionName = installedVersionName, + repo = repo, + iconModel = null, + canAskPreApprovalNow = false, ) } @@ -648,12 +649,13 @@ internal class AppInstallManagerTest { canAskPreApprovalNow: Boolean = false, ): InstallState { return appInstallManager.install( - appMetadata = appMetadata, - version = version, - currentVersionName = currentVersionName, - repo = repo, - iconModel = iconModel, - canAskPreApprovalNow = canAskPreApprovalNow, + packageName = packageName, + appMetadata = appMetadata, + version = version, + currentVersionName = currentVersionName, + repo = repo, + iconModel = iconModel, + canAskPreApprovalNow = canAskPreApprovalNow, ) } } diff --git a/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt b/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt index a173e98eb..ccc838d93 100644 --- a/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt +++ b/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt @@ -34,6 +34,7 @@ import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.runBlocking import org.fdroid.database.AppMetadata import org.fdroid.database.AppVersion +import org.fdroid.index.v2.PackageVersion import org.fdroid.ui.utils.isAppInForeground import org.junit.After import org.junit.Before @@ -73,7 +74,7 @@ internal class SessionInstallManagerTest { name = mapOf("en-US" to "Example App"), isCompatible = true, ) - private val appVersion: AppVersion = mockk(relaxed = true) + private val appVersion: PackageVersion = mockk(relaxed = true) private val installingState = InstallState.Installing( @@ -213,14 +214,13 @@ internal class SessionInstallManagerTest { ) assertIs(notForegroundResult) - // in foreground + update that can auto-update -> NotSupported + // in foreground + update that can auto-update, we say NotSupported, because not needed every { context.isAppInForeground() } returns true - every { appVersion.packageName } returns packageName - every { appVersion.manifest.targetSdkVersion } returns 34 + every { appVersion.packageManifest.targetSdkVersion } returns 42 val sourceInfo: InstallSourceInfo = mockk(relaxed = true) every { sourceInfo.installingPackageName } returns context.packageName if (SDK_INT >= 34) { - every { sourceInfo.updateOwnerPackageName } returns null + every { sourceInfo.updateOwnerPackageName } returns context.packageName } every { packageManager.getInstallSourceInfo(packageName) } returns sourceInfo @@ -237,8 +237,7 @@ internal class SessionInstallManagerTest { // isUpdate = true but not our package, and we are not the update owner -> NotSupported, // because canDoAutoUpdate() returns false when getInstallSourceInfo() throws every { context.isAppInForeground() } returns true - every { appVersion.packageName } returns packageName - every { appVersion.manifest.targetSdkVersion } returns 34 + every { appVersion.packageManifest.targetSdkVersion } returns 34 every { packageManager.getInstallSourceInfo(packageName) } throws SecurityException("nope") val installSourceError = diff --git a/app/src/test/java/org/fdroid/updates/UpdateInstallerTest.kt b/app/src/test/java/org/fdroid/updates/UpdateInstallerTest.kt index 93079c57c..31a3c78a5 100644 --- a/app/src/test/java/org/fdroid/updates/UpdateInstallerTest.kt +++ b/app/src/test/java/org/fdroid/updates/UpdateInstallerTest.kt @@ -62,23 +62,25 @@ internal class UpdateInstallerTest { } @Test - fun `updateAll returns early when updates list is empty`() = - testScope.runTest { - val installer = createUpdateInstaller() + fun `updateAll returns early when updates list is empty`() = testScope.runTest { + val installer = createUpdateInstaller() - installer.updateAll(emptyList(), canAskPreApprovalNow = false) - advanceUntilIdle() + installer.updateAll(emptyList(), canAskPreApprovalNow = false) + advanceUntilIdle() - coVerify(exactly = 0) { appInstallManager.install(any(), any(), any(), any(), any(), any()) } - verify(exactly = 0) { appInstallManager.setWaitingState(any(), any(), any(), any(), any()) } + coVerify(exactly = 0) { + appInstallManager.install(any(), any(), any(), any(), any(), any(), any()) } + verify(exactly = 0) { appInstallManager.setWaitingState(any(), any(), any(), any(), any()) } + } @Test fun `updateAll preApproval is true for single app and forced false for multiple`() = testScope.runTest { every { repoManager.getRepository(1L) } returns makeRepository() - coEvery { appInstallManager.install(any(), any(), any(), any(), any(), any()) } returns - mockk() + coEvery { + appInstallManager.install(any(), any(), any(), any(), any(), any(), any()) + } returns mockk() // single app + canAsk=true -> canAskPreApprovalNow=true val ver = makeAppVersion(versionName = "5.0", versionCode = 50) @@ -98,6 +100,7 @@ internal class UpdateInstallerTest { coVerify(exactly = 1) { appInstallManager.install( + packageName = "com.example.app", appMetadata = match { metadata -> metadata.packageName == "com.example.app" && metadata.repoId == 1L @@ -121,6 +124,7 @@ internal class UpdateInstallerTest { coVerify(exactly = 2) { appInstallManager.install( + packageName = any(), appMetadata = any(), version = any(), currentVersionName = any(), @@ -132,70 +136,72 @@ internal class UpdateInstallerTest { } @Test - fun `updateAll updates own app last and sets waiting state`() = - testScope.runTest { - val otherPkg = "com.example.other" - every { repoManager.getRepository(1L) } returns makeRepository() - coEvery { appInstallManager.install(any(), any(), any(), any(), any(), any()) } returns - mockk() + fun `updateAll updates own app last and sets waiting state`() = testScope.runTest { + val otherPkg = "com.example.other" + every { repoManager.getRepository(1L) } returns makeRepository() + coEvery { + appInstallManager.install(any(), any(), any(), any(), any(), any(), any()) + } returns mockk() - val ownVersion = - makeAppVersion(packageName = OWN_PACKAGE_NAME, versionName = "3.0", added = 9999L) - every { appDao.getApp(1L, OWN_PACKAGE_NAME) } returns makeApp(packageName = OWN_PACKAGE_NAME) - every { appDao.getApp(1L, otherPkg) } returns makeApp(packageName = otherPkg) - every { - appInstallManager.setWaitingState( + val ownVersion = + makeAppVersion(packageName = OWN_PACKAGE_NAME, versionName = "3.0", added = 9999L) + every { appDao.getApp(1L, OWN_PACKAGE_NAME) } returns makeApp(packageName = OWN_PACKAGE_NAME) + every { appDao.getApp(1L, otherPkg) } returns makeApp(packageName = otherPkg) + every { + appInstallManager.setWaitingState( + packageName = OWN_PACKAGE_NAME, + name = any(), + versionName = any(), + currentVersionName = any(), + lastUpdated = any(), + ) + } just runs + + val updates = + listOf( + makeAppUpdateItem( packageName = OWN_PACKAGE_NAME, - name = any(), - versionName = any(), - currentVersionName = any(), - lastUpdated = any(), - ) - } just runs + installedVersionName = "2.0", + update = ownVersion, + ), + makeAppUpdateItem(packageName = otherPkg), + ) - val updates = - listOf( - makeAppUpdateItem( - packageName = OWN_PACKAGE_NAME, - installedVersionName = "2.0", - update = ownVersion, - ), - makeAppUpdateItem(packageName = otherPkg), - ) + createUpdateInstaller().updateAll(updates, canAskPreApprovalNow = false) + advanceUntilIdle() - createUpdateInstaller().updateAll(updates, canAskPreApprovalNow = false) - advanceUntilIdle() - - verify(exactly = 1) { - appInstallManager.setWaitingState( - packageName = OWN_PACKAGE_NAME, - name = any(), - versionName = "3.0", - currentVersionName = "2.0", - lastUpdated = 9999L, - ) - } - - coVerifyOrder { - appInstallManager.install( - appMetadata = match { metadata -> metadata.packageName == otherPkg }, - version = any(), - currentVersionName = any(), - repo = any(), - iconModel = any(), - canAskPreApprovalNow = any(), - ) - appInstallManager.install( - appMetadata = match { metadata -> metadata.packageName == OWN_PACKAGE_NAME }, - version = any(), - currentVersionName = any(), - repo = any(), - iconModel = any(), - canAskPreApprovalNow = any(), - ) - } + verify(exactly = 1) { + appInstallManager.setWaitingState( + packageName = OWN_PACKAGE_NAME, + name = any(), + versionName = "3.0", + currentVersionName = "2.0", + lastUpdated = 9999L, + ) } + coVerifyOrder { + appInstallManager.install( + packageName = otherPkg, + appMetadata = match { metadata -> metadata.packageName == otherPkg }, + version = any(), + currentVersionName = any(), + repo = any(), + iconModel = any(), + canAskPreApprovalNow = any(), + ) + appInstallManager.install( + packageName = OWN_PACKAGE_NAME, + appMetadata = match { metadata -> metadata.packageName == OWN_PACKAGE_NAME }, + version = any(), + currentVersionName = any(), + repo = any(), + iconModel = any(), + canAskPreApprovalNow = any(), + ) + } + } + @Test fun `updateApp continues with null values if app missing in DB or repo missing`() = testScope.runTest { @@ -208,22 +214,27 @@ internal class UpdateInstallerTest { update = ver, ) ) - coEvery { appInstallManager.install(any(), any(), any(), any(), any(), any()) } returns - mockk() + coEvery { + appInstallManager.install(any(), any(), any(), any(), any(), any(), any()) + } returns mockk() // repo is null every { repoManager.getRepository(1L) } returns null every { appDao.getApp(1L, "com.example.app") } returns makeApp() createUpdateInstaller().updateAll(updates, canAskPreApprovalNow = false) advanceUntilIdle() - coVerify(exactly = 1) { appInstallManager.install(any(), any(), any(), null, any(), any()) } + coVerify(exactly = 1) { + appInstallManager.install(any(), any(), any(), any(), null, any(), any()) + } // app is null every { repoManager.getRepository(1L) } returns makeRepository() every { appDao.getApp(1L, "com.example.app") } returns null createUpdateInstaller().updateAll(updates, canAskPreApprovalNow = false) advanceUntilIdle() - coVerify(exactly = 1) { appInstallManager.install(null, any(), any(), any(), any(), any()) } + coVerify(exactly = 1) { + appInstallManager.install(any(), null, any(), any(), any(), any(), any()) + } } private fun makeAppUpdateItem( From 0f73f9ca0ea1d1b031e262fde8dffb8f02107455 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 30 Mar 2026 11:15:53 -0300 Subject: [PATCH 06/30] Port old nearby feature to new full flavor We had worked on a new nearby feature which would also employ BLE to find contacts when they are not on the same Wi-Fi and then auto connect via Wifi Direct. This had a prototype app and a UI prototype. The new nearby was ~80% done, but then scraped. So now unfortuntately, F-Droid 2.0 will need to ship with the old nearby code which is barely held together by duct tape. This commit mostly copies the old files as-is and does minimal changes so they can keep working in the new environment. An exception is the "success view" which was woven so deep into the old job intent services and state handling that it couldn't be re-used. Instead, a quick compose re-implementation with a viewmodel was made which hooks into the modern infrastructure, so app installs and presentation with icons is working. --- app/build.gradle.kts | 8 + .../ui/navigation/ExtraNavigationEntries.kt | 5 + app/src/full/AndroidManifest.xml | 105 +- app/src/full/assets/index.template.html | 138 ++ app/src/full/assets/swap-icon.png | Bin 0 -> 2710 bytes app/src/full/assets/swap-icon.svg | 40 + app/src/full/assets/swap-tick-done.png | Bin 0 -> 621 bytes app/src/full/assets/swap-tick-not-done.png | Bin 0 -> 588 bytes .../cc/mvdan/accesspoint/WifiApControl.java | 417 ++++ .../com/google/zxing/encode/Contents.java | 113 ++ .../google/zxing/encode/QRCodeEncoder.java | 261 +++ .../javax/jmdns/impl/FDroidServiceInfo.java | 122 ++ .../kellinwood/logging/AbstractLogger.java | 94 + .../logging/ConsoleLoggerFactory.java | 24 + .../kellinwood/logging/LoggerFactory.java | 22 + .../kellinwood/logging/LoggerInterface.java | 49 + .../kellinwood/logging/LoggerManager.java | 41 + .../kellinwood/logging/NullLoggerFactory.java | 69 + .../java/kellinwood/logging/StreamLogger.java | 35 + .../security/zipsigner/AutoKeyException.java | 14 + .../zipsigner/DefaultResourceAdapter.java | 34 + .../security/zipsigner/HexDumpEncoder.java | 72 + .../kellinwood/security/zipsigner/KeySet.java | 95 + .../security/zipsigner/ProgressEvent.java | 51 + .../security/zipsigner/ProgressHelper.java | 80 + .../security/zipsigner/ProgressListener.java | 26 + .../security/zipsigner/ResourceAdapter.java | 20 + .../security/zipsigner/ZipSigner.java | 780 ++++++++ .../optional/SignatureBlockGenerator.java | 60 + .../java/kellinwood/zipio/CentralEnd.java | 104 + .../full/java/kellinwood/zipio/ZioEntry.java | 639 ++++++ .../kellinwood/zipio/ZioEntryInputStream.java | 141 ++ .../zipio/ZioEntryOutputStream.java | 85 + .../full/java/kellinwood/zipio/ZipInput.java | 234 +++ .../kellinwood/zipio/ZipListingHelper.java | 52 + .../full/java/kellinwood/zipio/ZipOutput.java | 144 ++ .../java/org/fdroid/fdroid/FDroidApp.java | 62 + .../full/java/org/fdroid/fdroid/Hasher.java | 99 + .../java/org/fdroid/fdroid/Preferences.java | 87 + .../full/java/org/fdroid/fdroid/Utils.java | 437 +++++ .../org/fdroid/fdroid/compat/FileCompat.java | 78 + .../full/java/org/fdroid/fdroid/data/Apk.java | 11 + .../full/java/org/fdroid/fdroid/data/App.java | 24 + .../org/fdroid/fdroid/data/SanitizedFile.java | 69 + .../fdroid/fdroid/nearby/BluetoothClient.java | 35 + .../fdroid/nearby/BluetoothConnection.java | 55 + .../fdroid/nearby/BluetoothConstants.java | 11 + .../fdroid/nearby/BluetoothManager.java | 195 ++ .../fdroid/fdroid/nearby/BluetoothServer.java | 350 ++++ .../fdroid/fdroid/nearby/BonjourManager.java | 322 +++ .../org/fdroid/fdroid/nearby/LocalHTTPD.java | 504 +++++ .../fdroid/nearby/LocalHTTPDManager.java | 127 ++ .../fdroid/nearby/LocalRepoKeyStore.java | 377 ++++ .../fdroid/nearby/LocalRepoManager.java | 301 +++ .../fdroid/nearby/LocalRepoService.java | 130 ++ .../fdroid/fdroid/nearby/NewRepoConfig.java | 233 +++ .../nearby/PublicSourceDirProvider.java | 126 ++ .../fdroid/nearby/SDCardScannerService.java | 148 ++ .../fdroid/fdroid/nearby/SelectAppsView.java | 222 +++ .../fdroid/fdroid/nearby/StartSwapView.java | 255 +++ .../org/fdroid/fdroid/nearby/SwapService.java | 658 +++++++ .../fdroid/fdroid/nearby/SwapSuccessView.java | 40 + .../org/fdroid/fdroid/nearby/SwapView.java | 86 + .../fdroid/nearby/SwapWorkflowActivity.java | 1457 ++++++++++++++ .../nearby/TreeUriScannerIntentService.java | 166 ++ .../nearby/UsbDeviceAttachedReceiver.java | 67 + .../nearby/UsbDeviceDetachedReceiver.java | 60 + .../nearby/UsbDeviceMediaMountedReceiver.java | 24 + .../nearby/WifiStateChangeReceiver.java | 21 + .../fdroid/nearby/WifiStateChangeService.java | 391 ++++ .../nearby/httpish/ContentLengthHeader.java | 13 + .../fdroid/nearby/httpish/ETagHeader.java | 13 + .../fdroid/nearby/httpish/FileDetails.java | 23 + .../fdroid/fdroid/nearby/httpish/Header.java | 25 + .../fdroid/fdroid/nearby/httpish/Request.java | 204 ++ .../fdroid/nearby/httpish/Response.java | 166 ++ .../fdroid/nearby/peers/BluetoothPeer.java | 120 ++ .../fdroid/nearby/peers/BonjourPeer.java | 101 + .../org/fdroid/fdroid/nearby/peers/Peer.java | 29 + .../fdroid/fdroid/nearby/peers/WifiPeer.java | 109 ++ .../fdroid/fdroid/net/TreeUriDownloader.java | 114 ++ .../qr/CameraCharacteristicsChecker.java | 17 + .../CameraCharacteristicsMinApiLevel21.java | 108 + .../fdroid/views/main/NearbyViewBinder.java | 252 +++ .../apache/commons/codec/BinaryDecoder.java | 37 + .../apache/commons/codec/BinaryEncoder.java | 37 + .../apache/commons/codec/CharEncoding.java | 119 ++ .../org/apache/commons/codec/Decoder.java | 46 + .../commons/codec/DecoderException.java | 84 + .../org/apache/commons/codec/Encoder.java | 43 + .../commons/codec/EncoderException.java | 87 + .../codec/binary/CharSequenceUtils.java | 82 + .../org/apache/commons/codec/binary/Hex.java | 567 ++++++ .../commons/codec/binary/StringUtils.java | 419 ++++ .../commons/codec/digest/DigestUtils.java | 1743 +++++++++++++++++ .../codec/digest/MessageDigestAlgorithms.java | 174 ++ app/src/full/kotlin/org/fdroid/LegacyUtils.kt | 24 + .../ui/navigation/ExtraNavigationEntries.kt | 14 +- .../org/fdroid/ui/nearby/NearbyStart.kt | 58 + .../org/fdroid/ui/nearby/SwapSuccess.kt | 117 ++ .../org/fdroid/ui/nearby/SwapSuccessAppRow.kt | 290 +++ .../org/fdroid/ui/nearby/SwapSuccessModel.kt | 26 + .../fdroid/ui/nearby/SwapSuccessViewModel.kt | 241 +++ app/src/full/res/drawable-hdpi/circle.png | Bin 0 -> 213 bytes .../full/res/drawable-hdpi/ic_fdroid_grey.png | Bin 0 -> 246 bytes .../res/drawable-hdpi/swap_start_header.png | Bin 0 -> 12481 bytes app/src/full/res/drawable-ldpi/circle.png | Bin 0 -> 94 bytes .../full/res/drawable-ldpi/ic_fdroid_grey.png | Bin 0 -> 107 bytes .../res/drawable-ldpi/swap_start_header.png | Bin 0 -> 2790 bytes app/src/full/res/drawable-mdpi/circle.png | Bin 0 -> 112 bytes .../full/res/drawable-mdpi/ic_fdroid_grey.png | Bin 0 -> 139 bytes .../res/drawable-mdpi/swap_start_header.png | Bin 0 -> 5106 bytes app/src/full/res/drawable-xhdpi/circle.png | Bin 0 -> 304 bytes .../res/drawable-xhdpi/ic_fdroid_grey.png | Bin 0 -> 252 bytes .../res/drawable-xhdpi/swap_start_header.png | Bin 0 -> 17404 bytes app/src/full/res/drawable-xxhdpi/circle.png | Bin 0 -> 808 bytes .../res/drawable-xxhdpi/ic_fdroid_grey.png | Bin 0 -> 655 bytes .../res/drawable-xxhdpi/swap_start_header.png | Bin 0 -> 51061 bytes app/src/full/res/drawable-xxxhdpi/circle.png | Bin 0 -> 1446 bytes .../res/drawable-xxxhdpi/ic_fdroid_grey.png | Bin 0 -> 1014 bytes .../drawable-xxxhdpi/swap_start_header.png | Bin 0 -> 84307 bytes app/src/full/res/drawable/check.xml | 11 + .../res/drawable/ic_add_circle_outline.xml | 10 + app/src/full/res/drawable/ic_apps.xml | 10 + app/src/full/res/drawable/ic_arrow_back.xml | 11 + .../full/res/drawable/ic_arrow_forward.xml | 10 + app/src/full/res/drawable/ic_bluetooth.xml | 10 + .../res/drawable/ic_bluetooth_searching.xml | 10 + app/src/full/res/drawable/ic_close.xml | 10 + app/src/full/res/drawable/ic_nearby.xml | 33 + app/src/full/res/drawable/ic_qr_code.xml | 40 + app/src/full/res/drawable/ic_search.xml | 10 + app/src/full/res/drawable/ic_wifi.xml | 10 + app/src/full/res/drawable/ic_wifi_off.xml | 10 + .../full/res/drawable/ic_wifi_tethering.xml | 10 + app/src/full/res/drawable/nearby_splash.xml | 12 + .../res/layout-sw480dp/start_swap_header.xml | 33 + app/src/full/res/layout/main_tab_nearby.xml | 141 ++ .../layout/select_local_apps_list_item.xml | 70 + app/src/full/res/layout/start_swap_header.xml | 6 + app/src/full/res/layout/swap_activity.xml | 27 + .../full/res/layout/swap_app_list_item.xml | 97 + .../full/res/layout/swap_confirm_receive.xml | 76 + app/src/full/res/layout/swap_connecting.xml | 44 + app/src/full/res/layout/swap_join_wifi.xml | 59 + .../full/res/layout/swap_peer_list_item.xml | 45 + app/src/full/res/layout/swap_select_apps.xml | 17 + app/src/full/res/layout/swap_send_fdroid.xml | 57 + app/src/full/res/layout/swap_start_swap.xml | 272 +++ app/src/full/res/layout/swap_success.xml | 16 + app/src/full/res/layout/swap_wifi_qr.xml | 65 + app/src/full/res/menu/swap_next.xml | 12 + app/src/full/res/menu/swap_next_search.xml | 15 + app/src/full/res/menu/swap_search.xml | 16 + app/src/full/res/values-night/colors.xml | 56 + app/src/full/res/values-night/themes.xml | 75 + app/src/full/res/values/attrs.xml | 7 + app/src/full/res/values/colors.xml | 77 + app/src/full/res/values/styles.xml | 268 +++ app/src/full/res/values/themes.xml | 99 + app/src/full/res/xml/device_filter.xml | 25 + .../full/res/xml/network_security_config.xml | 43 + .../org/fdroid/ui/navigation/NavigationKey.kt | 18 +- .../fdroid/fdroid/data/SanitizedFileTest.java | 57 + gradle/verification-metadata.xml | 15 + 165 files changed, 18913 insertions(+), 6 deletions(-) create mode 100644 app/src/full/assets/index.template.html create mode 100644 app/src/full/assets/swap-icon.png create mode 100644 app/src/full/assets/swap-icon.svg create mode 100644 app/src/full/assets/swap-tick-done.png create mode 100644 app/src/full/assets/swap-tick-not-done.png create mode 100644 app/src/full/java/cc/mvdan/accesspoint/WifiApControl.java create mode 100755 app/src/full/java/com/google/zxing/encode/Contents.java create mode 100755 app/src/full/java/com/google/zxing/encode/QRCodeEncoder.java create mode 100644 app/src/full/java/javax/jmdns/impl/FDroidServiceInfo.java create mode 100644 app/src/full/java/kellinwood/logging/AbstractLogger.java create mode 100644 app/src/full/java/kellinwood/logging/ConsoleLoggerFactory.java create mode 100644 app/src/full/java/kellinwood/logging/LoggerFactory.java create mode 100644 app/src/full/java/kellinwood/logging/LoggerInterface.java create mode 100644 app/src/full/java/kellinwood/logging/LoggerManager.java create mode 100644 app/src/full/java/kellinwood/logging/NullLoggerFactory.java create mode 100644 app/src/full/java/kellinwood/logging/StreamLogger.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/AutoKeyException.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/DefaultResourceAdapter.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/HexDumpEncoder.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/KeySet.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/ProgressEvent.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/ProgressHelper.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/ProgressListener.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/ResourceAdapter.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/ZipSigner.java create mode 100644 app/src/full/java/kellinwood/security/zipsigner/optional/SignatureBlockGenerator.java create mode 100644 app/src/full/java/kellinwood/zipio/CentralEnd.java create mode 100644 app/src/full/java/kellinwood/zipio/ZioEntry.java create mode 100644 app/src/full/java/kellinwood/zipio/ZioEntryInputStream.java create mode 100644 app/src/full/java/kellinwood/zipio/ZioEntryOutputStream.java create mode 100644 app/src/full/java/kellinwood/zipio/ZipInput.java create mode 100644 app/src/full/java/kellinwood/zipio/ZipListingHelper.java create mode 100644 app/src/full/java/kellinwood/zipio/ZipOutput.java create mode 100644 app/src/full/java/org/fdroid/fdroid/FDroidApp.java create mode 100644 app/src/full/java/org/fdroid/fdroid/Hasher.java create mode 100644 app/src/full/java/org/fdroid/fdroid/Preferences.java create mode 100644 app/src/full/java/org/fdroid/fdroid/Utils.java create mode 100644 app/src/full/java/org/fdroid/fdroid/compat/FileCompat.java create mode 100644 app/src/full/java/org/fdroid/fdroid/data/Apk.java create mode 100644 app/src/full/java/org/fdroid/fdroid/data/App.java create mode 100644 app/src/full/java/org/fdroid/fdroid/data/SanitizedFile.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/BluetoothClient.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/BluetoothConnection.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/BluetoothConstants.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/BluetoothManager.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/BluetoothServer.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/BonjourManager.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/LocalHTTPD.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/LocalHTTPDManager.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoKeyStore.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoService.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/NewRepoConfig.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/PublicSourceDirProvider.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/SDCardScannerService.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/StartSwapView.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/SwapSuccessView.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/SwapView.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/UsbDeviceAttachedReceiver.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/UsbDeviceDetachedReceiver.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/UsbDeviceMediaMountedReceiver.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeReceiver.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/httpish/ContentLengthHeader.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/httpish/ETagHeader.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/httpish/FileDetails.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/httpish/Header.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/httpish/Request.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/httpish/Response.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/peers/BluetoothPeer.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/peers/BonjourPeer.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/peers/Peer.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/peers/WifiPeer.java create mode 100644 app/src/full/java/org/fdroid/fdroid/net/TreeUriDownloader.java create mode 100644 app/src/full/java/org/fdroid/fdroid/qr/CameraCharacteristicsChecker.java create mode 100644 app/src/full/java/org/fdroid/fdroid/qr/CameraCharacteristicsMinApiLevel21.java create mode 100644 app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/BinaryDecoder.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/BinaryEncoder.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/CharEncoding.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/Decoder.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/DecoderException.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/Encoder.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/EncoderException.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/binary/CharSequenceUtils.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/binary/Hex.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/binary/StringUtils.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/digest/DigestUtils.java create mode 100644 app/src/full/java/vendored/org/apache/commons/codec/digest/MessageDigestAlgorithms.java create mode 100644 app/src/full/kotlin/org/fdroid/LegacyUtils.kt create mode 100644 app/src/full/kotlin/org/fdroid/ui/nearby/NearbyStart.kt create mode 100644 app/src/full/kotlin/org/fdroid/ui/nearby/SwapSuccess.kt create mode 100644 app/src/full/kotlin/org/fdroid/ui/nearby/SwapSuccessAppRow.kt create mode 100644 app/src/full/kotlin/org/fdroid/ui/nearby/SwapSuccessModel.kt create mode 100644 app/src/full/kotlin/org/fdroid/ui/nearby/SwapSuccessViewModel.kt create mode 100644 app/src/full/res/drawable-hdpi/circle.png create mode 100644 app/src/full/res/drawable-hdpi/ic_fdroid_grey.png create mode 100644 app/src/full/res/drawable-hdpi/swap_start_header.png create mode 100644 app/src/full/res/drawable-ldpi/circle.png create mode 100644 app/src/full/res/drawable-ldpi/ic_fdroid_grey.png create mode 100644 app/src/full/res/drawable-ldpi/swap_start_header.png create mode 100644 app/src/full/res/drawable-mdpi/circle.png create mode 100644 app/src/full/res/drawable-mdpi/ic_fdroid_grey.png create mode 100644 app/src/full/res/drawable-mdpi/swap_start_header.png create mode 100644 app/src/full/res/drawable-xhdpi/circle.png create mode 100644 app/src/full/res/drawable-xhdpi/ic_fdroid_grey.png create mode 100644 app/src/full/res/drawable-xhdpi/swap_start_header.png create mode 100644 app/src/full/res/drawable-xxhdpi/circle.png create mode 100644 app/src/full/res/drawable-xxhdpi/ic_fdroid_grey.png create mode 100644 app/src/full/res/drawable-xxhdpi/swap_start_header.png create mode 100644 app/src/full/res/drawable-xxxhdpi/circle.png create mode 100644 app/src/full/res/drawable-xxxhdpi/ic_fdroid_grey.png create mode 100644 app/src/full/res/drawable-xxxhdpi/swap_start_header.png create mode 100644 app/src/full/res/drawable/check.xml create mode 100644 app/src/full/res/drawable/ic_add_circle_outline.xml create mode 100644 app/src/full/res/drawable/ic_apps.xml create mode 100644 app/src/full/res/drawable/ic_arrow_back.xml create mode 100644 app/src/full/res/drawable/ic_arrow_forward.xml create mode 100644 app/src/full/res/drawable/ic_bluetooth.xml create mode 100644 app/src/full/res/drawable/ic_bluetooth_searching.xml create mode 100644 app/src/full/res/drawable/ic_close.xml create mode 100644 app/src/full/res/drawable/ic_nearby.xml create mode 100644 app/src/full/res/drawable/ic_qr_code.xml create mode 100644 app/src/full/res/drawable/ic_search.xml create mode 100644 app/src/full/res/drawable/ic_wifi.xml create mode 100644 app/src/full/res/drawable/ic_wifi_off.xml create mode 100644 app/src/full/res/drawable/ic_wifi_tethering.xml create mode 100644 app/src/full/res/drawable/nearby_splash.xml create mode 100644 app/src/full/res/layout-sw480dp/start_swap_header.xml create mode 100644 app/src/full/res/layout/main_tab_nearby.xml create mode 100644 app/src/full/res/layout/select_local_apps_list_item.xml create mode 100644 app/src/full/res/layout/start_swap_header.xml create mode 100644 app/src/full/res/layout/swap_activity.xml create mode 100644 app/src/full/res/layout/swap_app_list_item.xml create mode 100644 app/src/full/res/layout/swap_confirm_receive.xml create mode 100644 app/src/full/res/layout/swap_connecting.xml create mode 100644 app/src/full/res/layout/swap_join_wifi.xml create mode 100644 app/src/full/res/layout/swap_peer_list_item.xml create mode 100644 app/src/full/res/layout/swap_select_apps.xml create mode 100644 app/src/full/res/layout/swap_send_fdroid.xml create mode 100644 app/src/full/res/layout/swap_start_swap.xml create mode 100644 app/src/full/res/layout/swap_success.xml create mode 100644 app/src/full/res/layout/swap_wifi_qr.xml create mode 100644 app/src/full/res/menu/swap_next.xml create mode 100644 app/src/full/res/menu/swap_next_search.xml create mode 100644 app/src/full/res/menu/swap_search.xml create mode 100644 app/src/full/res/values-night/colors.xml create mode 100644 app/src/full/res/values-night/themes.xml create mode 100644 app/src/full/res/values/attrs.xml create mode 100644 app/src/full/res/values/colors.xml create mode 100644 app/src/full/res/values/styles.xml create mode 100644 app/src/full/res/values/themes.xml create mode 100644 app/src/full/res/xml/device_filter.xml create mode 100644 app/src/full/res/xml/network_security_config.xml create mode 100644 app/src/testFull/java/org/fdroid/fdroid/data/SanitizedFileTest.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3672628d9..909b03de1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,7 +137,15 @@ dependencies { debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + "fullImplementation"(libs.material) + "fullImplementation"(libs.androidx.documentfile) + "fullImplementation"(libs.androidx.localbroadcastmanager) "fullImplementation"(libs.guardianproject.panic) + "fullImplementation"(libs.bcpkix.jdk15to18) + "fullImplementation"(libs.jmdns) + "fullImplementation"(libs.nanohttpd) + "fullImplementation"(libs.commons.io) + "fullImplementation"(libs.commons.net) testImplementation(libs.junit) testImplementation(kotlin("test")) diff --git a/app/src/basic/kotlin/org/fdroid/ui/navigation/ExtraNavigationEntries.kt b/app/src/basic/kotlin/org/fdroid/ui/navigation/ExtraNavigationEntries.kt index eb281857b..1337b4249 100644 --- a/app/src/basic/kotlin/org/fdroid/ui/navigation/ExtraNavigationEntries.kt +++ b/app/src/basic/kotlin/org/fdroid/ui/navigation/ExtraNavigationEntries.kt @@ -1,6 +1,11 @@ package org.fdroid.ui.navigation +import android.content.Context import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey +import org.fdroid.R + +fun getMoreMenuItems(context: Context): List = + listOf(NavDestinations.AllApps(context.getString(R.string.app_list_all)), NavDestinations.About) fun EntryProviderScope.extraNavigationEntries(navigator: Navigator) {} diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml index 4f8cd7354..a6b88dcc3 100644 --- a/app/src/full/AndroidManifest.xml +++ b/app/src/full/AndroidManifest.xml @@ -2,7 +2,58 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/full/assets/index.template.html b/app/src/full/assets/index.template.html new file mode 100644 index 000000000..5d8d08e11 --- /dev/null +++ b/app/src/full/assets/index.template.html @@ -0,0 +1,138 @@ + + + + + F-Droid swap + + + + + + + + + + +

You're minutes away from having swap success!

+
    +
  1. + Find a swap + Done +
  2. +
  3. + Download F-Droid + Not done +
  4. +
  5. + Install F-Droid + Not done +
  6. +
  7. + Add the swap to F-Droid + Not done +
  8. +
  9. + Install the apps you want + Not done +
  10. +
+



+
+ Available Apps +
    + {{APP_LIST}} +
+
+ + diff --git a/app/src/full/assets/swap-icon.png b/app/src/full/assets/swap-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a970a8a6eece543e65776f7c640ad3589df93a GIT binary patch literal 2710 zcmV;H3TgF;P)Ql<#`AIB?1WXLnml%a?WGTP%HSaT>S9y|=wB=Hcr<%CS;0RjKigCbRBOCX{_>u8k#{puf zVlVFD9g6Y8LWXdL?2$R3cPYhhi_KvvACNh$K%p@+DJ7^D(}fJF$f&`+{7&%Y^BBiT zMZ`f2W)tC&U=B^BBHN0JHE=*Sk5(KL>gaLy=OVA)9d4uRO|4943wY1bmDMNGWA#S26;H=2M!8uS>hGP4(LnTbExNk|Eqb^vBO%!xe`1a z%8xiEp#I@u+5xT@fr5~#^L_=azB9`g$xgVLkP|I4Lb&ipQy}>SQq1Y z4h~I=^u4Ubn%8`NK2e3Yux=(NNvKepe&nO;9jf}<<2@6rme3%yg_-AwqwYIui+`xa zdsq`^3^gUhsKei=S-{@@9@1x6GoK42^r)>AIIZR|e*=e^SP^At@-$A-j8*JTme22j zJdPC$IVU?(5BRrk;wMfd3tc~PxFkKQ@F-=*iJv%t<>-2w8d5><5K;dVLzVa#UC&TS zDi9j-235sR^hVcc@s`2G#L&}Qyk(HdN^~S>BVIF@hK@LQiq{MtK~HP(n!#xlp<}3c z&7eB-(ea`L230wbQ>nw*oWLPuCh;ac`k-STRm3Ok&$V=ABrh8x! zx)RP7kFXD2S&GvVd`@c)kZ_?U%TU)!Ji+^D)!W;k4 zmJ|s-j-?n?i%At%aSUV0!dqJz$$=7Vyo;(LclS4v&O~DPqk#U@mY|{@>K+sqaU1LL z*Bb5#=(CCsQ1y|xhLib}VE8qFt<+qIs$#bmYm&k!q69~RN2w)2!aJzyC$8XnRuepn z1JqW{xu{x1byvExrw~*zh6W^o%PdqibY&X;7lQ434hq5^R#mUKl4Dp$@U7vTu$0qt zoL@@DEff(Bh1@9q;RF)?jQkQ|FyS(gi1>q1sM$)o!$WOT37@BYIVh^I9yNW$LF~uR zI47HLnait;r!TFziOad0Zj9x1KI9KJ6302u`EXENiJC2>I(n17#|bNWgGV@>N_(7i zJdyUiOA$_Z(ep$SGf~ss(ecpP*b`+MH*=_k!bl6ovk<#pp|ZCvA5m0nbTVhm)7Y_u zZX95{-=h<&vExbc4qZ^wO&mintSaI)8W3@8nf-$-#kvmS73QO&#JR>4?!cM^FL9s? z0n{BVz?wLh2P-T7hB$<#lw!q~oa5pQ*A4uN6?vR4?&C?+)E7r^2D#{4;r`n{5!z9V zzE$imZet}X7C2T-T8^$bgQ?+J-~3)6p~3-&|fHH64lYqNq!7xA=@|%t1#9$BG+x3Kg>)`E33azVT41^By{8iyK&h ziu=Vjx}l?MfW{s^Mn`jT8YiJ5!GRtRwG9Z^Hp(whH=iovEbc?a_hK85qHdf745@sF zGRFfS9`&YPOD6fK`jW~LI2=Ycs@7-)=Je#c-PactFY{Sq6Wvjj!;un5+=;q-^?sZ! zpZVNMN_ly_2hxhuc%o?rUV*Sq3Ubh0^ByMY4`7J0{u~Oi`YVA{Wp%_;w4m_ zEjpOOzpbZB*#IyzP;tyo?r0r)GE6O=VJ>Pukq{uA)vml&a3}?ox0&9gizqD>$ST)W z*N6dpLJ=^ZeH3476R4>pp+I}otkZn4pa(n3W&|qU7a@(74C8A`DYKM=MG@yQSz*li z9SI3i*ovB_x_REkShav3iBaAf-9jBl@hAoC&N&hq3`NaLqKD=uh1?)|4Hn4Lu zBt$rXVpQZ(!}n?E=NAnm3l(4si4lPg-BVB(?Whr(%CoW)b zu9Fa=7Nst=`p^pNekmWd7<}ncMwFUt!kTQ3mI{eOIoa0E!O!+^C+o?M=!-R1Nd-lO zcld>lJ9dN62o)Q4Iseg^m&l^-&YuJY=o=*!82hjqi18k`lTz-xsRRW(7H2q>k*wbB zh%6J(^*L3fLZc2*03e^&Xi=6@%xdbmlk8%plw#c z%JsT?>975E=EHUg9vlk-h3vbVk|9rodU{>aPk4?xzO@yafb;chTHpw&0?E;4iG~Fy-|}z zn%Kdc)>md2q><&x!Uk@o%z9G9&_o~mcS!+z69)hu5f)r8XG9n zPK=;p2mnxEUTXu4!-xa$oERb4WOSAof!}2Gtr)>Zgul5%jG*IgYKWHq0t4(JFu?@{ QN&o-=07*qoM6N<$f+{fvr~m)} literal 0 HcmV?d00001 diff --git a/app/src/full/assets/swap-icon.svg b/app/src/full/assets/swap-icon.svg new file mode 100644 index 000000000..43db30a87 --- /dev/null +++ b/app/src/full/assets/swap-icon.svg @@ -0,0 +1,40 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/app/src/full/assets/swap-tick-done.png b/app/src/full/assets/swap-tick-done.png new file mode 100644 index 0000000000000000000000000000000000000000..81a2cb7d8767ee2f1980def34868c623a12fc656 GIT binary patch literal 621 zcmV-z0+RiSP)YN+qP}nwr$(CZQDj|<8)MHvK!~$p0RcLMe$7LW}8LvZ0v8m7R54^ zy8$c~#j&*xM!GeMVLA^h^;Q(Y&N?ggwn-y3GkHoWUN%vrW^dh<;%VbYS|;(Zat|9P z(z2lMmAlJWk(TZCR_+F4MoPwUg)En)@0fRFIVaMxh7Piv7->1qU|EijluYU|*=~uH zOyg78-Y`z2V^%-N_KT?^9rO89w(b^)bS$Z(Y=f;I=~z_{c@B+qY^1L|*GD>bG)SIT zj2)>szz}(UG)1K1MESa!H&Stwe1ohNsn|;;$44r*F+{$1>$=ix{`7&XZEBLJ%1ZjEZr1zuJ@*DHi_n$!AqKTYqW+jU8@1R-DH=7@8m-Sk22)wG zKiH=Fs@D(3k9M%4F6y&xw2KA&sdkUHJCgWWZTgx$+RMDUsKsf~epWL;HNP4^Qn0IP zHjh-Csgma-9b>pxo&n~Kl#J(X*)E9GOzl_Y{xNZ+XJtcBYKJI>3zd2!iefxp8ELRZ zqBxc?5WtmDET;fEnmmeSY#)HVqIl-h*9XRk(SbuO6D>9jxR99@>oUnW00000NkvXX Hu0mjfzWgb_ literal 0 HcmV?d00001 diff --git a/app/src/full/assets/swap-tick-not-done.png b/app/src/full/assets/swap-tick-not-done.png new file mode 100644 index 0000000000000000000000000000000000000000..9046ea31f978e1a4dd35b19e0c6a7f5d6e9dd95c GIT binary patch literal 588 zcmV-S0<-;zP)Rdu3roa={}GxwMyc+3zqvOk)l1h?LkABi z)kj**54w3mslL-~e$dA&N;PK0{9urGl;MK;!6*^RaK|?wicyB=j^~ECoF93h#k^n{ zW%@>!dBGyeG-lW{NK}r?p4m){GQFaqypJt2!w9|QU&@T|fijKC+9mY=^RbvGuGwac z=1*N{5z2Ai%7Kf_?RLou{n-;tFK;Nvv-0DLLzLqqb7^@BUA&+i-)Oh;VWwcBUs$H4 zBw~F+xlUPS(8UeP^_^uJeg{VPDAyya92(g|1>P}WMswkBztCF!%le86+~E=xI98jw z$2qFfz1rn+JE#huYFa5_1y$@&?Vq?973-1*Ge(5U^r22g8wPkoWjW%O(G+-G?~=qL z`jio!R$u7)