diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 82893a609..c3ca44408 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -19,6 +19,7 @@ workflow:
- saas-linux-medium-amd64
variables:
JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64
+ GIT_SUBMODULE_STRATEGY: none
before_script:
- test -e /etc/apt/sources.list.d/trixie-backports.list
|| echo "deb http://deb.debian.org/debian trixie-backports main" >> /etc/apt/sources.list
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..8a1137a46
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "fastlane"]
+ path = fastlane
+ url = https://gitlab.com/fdroid/fdroidclient-fastlane.git
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 39adaf66f..cb4be776f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,7 +22,7 @@ android {
versionCode = 2000010
versionName = "2.0-alpha10"
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner = "org.fdroid.HiltTestRunner"
}
// filter out incomplete translations from stable releases (which end in 50+)
@@ -177,6 +177,9 @@ dependencies {
androidTestImplementation(libs.androidx.test.uiautomator)
androidTestImplementation(libs.coil.test)
androidTestImplementation(libs.coil.network.okhttp)
+ androidTestImplementation(libs.turbine)
+ androidTestImplementation(libs.hilt.android.testing)
+ kspAndroidTest(libs.hilt.android.compiler)
screenshotTestImplementation(libs.screenshot.validation.api)
}
diff --git a/app/lint.xml b/app/lint.xml
index cdd395479..dedbe3114 100644
--- a/app/lint.xml
+++ b/app/lint.xml
@@ -19,4 +19,7 @@
+
+
+
diff --git a/app/src/androidTest/java/org/fdroid/HiltTestRunner.kt b/app/src/androidTest/java/org/fdroid/HiltTestRunner.kt
new file mode 100644
index 000000000..753c9085f
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/HiltTestRunner.kt
@@ -0,0 +1,12 @@
+package org.fdroid
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+class HiltTestRunner : AndroidJUnitRunner() {
+ override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
+ return super.newApplication(cl, HiltTestApplication::class.java.name, context)
+ }
+}
diff --git a/app/src/androidTest/java/org/fdroid/repo/RepoManagerAddAllIntegrationTest.kt b/app/src/androidTest/java/org/fdroid/repo/RepoManagerAddAllIntegrationTest.kt
new file mode 100644
index 000000000..f77f40df3
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/repo/RepoManagerAddAllIntegrationTest.kt
@@ -0,0 +1,170 @@
+package org.fdroid.repo
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import app.cash.turbine.TurbineTestContext
+import app.cash.turbine.test
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlin.test.assertIs
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.runBlocking
+import org.fdroid.index.RepoManager
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.slf4j.LoggerFactory
+
+@HiltAndroidTest
+@RunWith(AndroidJUnit4::class)
+internal class RepoManagerAddAllIntegrationTest {
+
+ @get:Rule var hiltRule = HiltAndroidRule(this)
+
+ @get:Rule var folder: TemporaryFolder = TemporaryFolder()
+
+ private val repos =
+ listOf(
+ "https://raw.githubusercontent.com/2br-2b/Fdroid-repo/master/fdroid/repo",
+ "https://anonymousmessenger.ly/fdroid/repo",
+ "https://fdroid.beocode.eu/fdroid/repo",
+ "https://mobileapp.bitwarden.com/fdroid/repo",
+ "https://briarproject.org/fdroid/repo",
+ "https://fdroid.bromite.org/fdroid/repo",
+ "https://fdroid.gitlab.io/ccc/fdroid/repo",
+ "https://www.collaboraoffice.com/downloads/fdroid/repo",
+ "https://bubu1.eu/cctg/fdroid/repo",
+ "https://static.cryptomator.org/android/fdroid/repo",
+ "https://lucaapp.gitlab.io/fdroid-repository/fdroid/repo",
+ "https://divestos.org/apks/official/fdroid/repo",
+ "https://divestos.org/apks/unofficial/fdroid/repo",
+ "https://raw.githubusercontent.com/efreak/auto-daily-fdroid/main/fdroid/repo",
+ "https://bubu1.eu/fdroidclassic/fdroid/repo",
+ "https://f5a.typed.icu/fdroid/repo",
+ "https://fdroid.fedilab.app/repo",
+ "https://raw.githubusercontent.com/Tobi823/ffupdaterrepo/master/fdroid/repo",
+ "https://rfc2822.gitlab.io/fdroid-firefox/fdroid/repo",
+ "https://raw.githubusercontent.com/Five-Prayers/fdroid-repo-stable/main/fdroid/repo",
+ "https://codeberg.org/florian-obernberger/fdroid-repo/raw/branch/main/repo",
+ "https://fdroid.frostnerd.com/fdroid/repo",
+ "https://pili.qi0.de/fdroid/repo",
+ "https://gitjournal.io/fdroid/repo",
+ "https://guardianproject.info/fdroid/repo",
+ "https://s3.amazonaws.com/guardianproject/fdroid/repo",
+ "https://guardianproject.info/fdroid/repo",
+ "https://f-droid.i2p.io/repo",
+ "https://iitc.app/fdroid/repo",
+ "https://jhass.github.io/insporation/fdroid/repo",
+ "https://raw.githubusercontent.com/iodeOS/fdroid/master/fdroid/repo",
+ "https://apt.izzysoft.de/fdroid/repo",
+ "https://android.izzysoft.de/repo",
+ "https://jak-linux.org/fdroid/repo",
+ "https://julianfairfax.gitlab.io/fdroid-repo/fdroid/repo",
+ "https://kaffeemitkoffein.de/fdroid/repo",
+ "https://store.nethunter.com/repo",
+ "https://cdn.kde.org/android/stable-releases/fdroid/repo",
+ "https://repo.kuschku.de/fdroid/repo",
+ "https://fdroid.libretro.com/repo",
+ "https://fdroid.ltheinrich.de/fdroid/repo",
+ "https://ltt.rs/fdroid/repo",
+ "https://pili.qi0.de/fdroid/repo",
+ "https://fdroid.metatransapps.com/fdroid/repo",
+ "https://microg.org/fdroid/repo",
+ "https://fdroid.mm20.de/repo",
+ "https://repo.mobilsicher.de/fdroid/repo",
+ "https://molly.im/fdroid/repo",
+ "https://molly.im/fdroid/foss/fdroid/repo",
+ "https://f-droid.monerujo.io/fdroid/repo",
+ "https://releases.nailyk.fr/repo",
+ "https://nanolx.org/fdroid/repo",
+ "https://www.nanolx.org/fdroid/repo",
+ "https://repo.netsyms.com/fdroid/repo",
+ "https://archive.newpipe.net/fdroid/repo",
+ "https://repo.nononsenseapps.com/fdroid/repo",
+ "https://fdroid.novy.software/repo",
+ "https://raw.githubusercontent.com/nucleus-ffm/Nucleus-F-Droid-Repo/master/fdroid/repo",
+ "https://obfusk.ch/fdroid/repo",
+ "https://ouchadam.github.io/fdroid-repository/repo",
+ "https://fdroid.partidopirata.com.ar/fdroid/repo",
+ "https://thecapslock.gitlab.io/fdroid-patched-apps/fdroid/repo",
+ "https://fdroid.i2pd.xyz/fdroid/repo",
+ "https://fdroid.rami.io/fdroid/repo",
+ "https://thedoc.eu.org/fdroid/repo",
+ "https://repo.samourai.io/fdroid/repo",
+ "https://fdroid.a3.pm/seabear/repo",
+ "https://raw.githubusercontent.com/jackbonadies/seekerandroid/fdroid/fdroid/repo",
+ "https://fdroid.getsession.org/fdroid/repo",
+ "https://raw.githubusercontent.com/simlar/simlar-fdroid-repo/master/fdroid/repo",
+ "https://s2.spiritcroc.de/fdroid/repo",
+ "https://haagch.frickel.club/files/fdroid/repo",
+ "https://submarine.strangled.net/fdroid/repo",
+ "https://service.tagesschau.de/app/repo",
+ "https://fdroid-repo.calyxinstitute.org/fdroid/repo",
+ "https://releases.threema.ch/fdroid/repo",
+ "https://raw.githubusercontent.com/chrisgch/tca/master/fdroid/repo",
+ "https://fdroid.twinhelix.com/fdroid/repo",
+ "https://secfirst.org/fdroid/repo",
+ "https://fdroid.videlibri.de/repo",
+ "https://guardianproject-wind.s3.amazonaws.com/fdroid/repo",
+ "https://raw.githubusercontent.com/xarantolus/fdroid/main/fdroid/repo",
+ "https://zimbelstern.eu/fdroid/repo",
+ )
+
+ private val log = LoggerFactory.getLogger(this::class.java.simpleName)
+
+ @Inject lateinit var repoManager: RepoManager
+
+ @Before
+ fun optIn() {
+ // Careful! This will add lots of repos to your live DB
+ assumeTrue(false) // don't run integration tests with real repos all the time
+
+ // inject repo manager before running test
+ hiltRule.inject()
+ }
+
+ @Test
+ fun addAllTheThings() = runBlocking {
+ repos.forEach { addRepo(it) }
+ }
+
+ private suspend fun addRepo(url: String) {
+ log.info("Fetching $url")
+ repoManager.fetchRepositoryPreview(url = url, proxy = null)
+ repoManager.addRepoState.test(timeout = 15.seconds) {
+ val fetchState = awaitFinalFetchState()
+ if (fetchState is Fetching && fetchState.fetchResult != null) {
+ repoManager.addFetchedRepository()
+ val item = awaitItem()
+ if (item is Adding) {
+ // await final state
+ assertIs(awaitItem())
+ } else {
+ // was already final state
+ assertIs(item)
+ }
+ log.info(" Added")
+ } else if (fetchState is AddRepoError) {
+ log.error(" $fetchState $url")
+ }
+ repoManager.abortAddingRepository()
+ assertIs(awaitItem())
+ cancelAndIgnoreRemainingEvents()
+ }
+ log.info("End $url")
+ }
+
+ private suspend fun TurbineTestContext.awaitFinalFetchState(): AddRepoState {
+ var item = awaitItem()
+ log.info(" $item")
+ while (item is None || (item is Fetching && !item.done)) {
+ item = awaitItem()
+ log.info(" $item")
+ }
+ log.info(" final: $item")
+ return item
+ }
+}
diff --git a/app/src/androidTest/java/org/fdroid/ui/LocalizationTest.kt b/app/src/androidTest/java/org/fdroid/ui/LocalizationTest.kt
new file mode 100644
index 000000000..dd2e78de4
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/ui/LocalizationTest.kt
@@ -0,0 +1,231 @@
+package org.fdroid.ui
+
+import android.content.res.Resources
+import android.text.TextUtils
+import android.util.DisplayMetrics
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import java.util.IllegalFormatException
+import java.util.Locale
+import java.util.regex.Pattern
+import org.fdroid.R
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Runs through all the translated strings and tests them with the same format values that the
+ * source strings expect. This is to ensure that the formats in the translations are correct in
+ * number and in type (e.g. `s` or `s`). It reads the source formats and then builds `formats` to
+ * represent the position and type of the formats. Then it runs through all the translations with
+ * formats of the correct number and type.
+ */
+@Suppress("DEPRECATION")
+@RunWith(AndroidJUnit4::class)
+class LocalizationTest {
+ private val androidFormat: Pattern = Pattern.compile("(%[a-z0-9]\\$?[a-z]?)")
+ private val locales: Array = Locale.getAvailableLocales()
+ private val localeNames = HashSet(locales.size)
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ private var assets = context.assets
+ private var config =
+ context.resources.configuration.apply {
+ locale = Locale.ENGLISH
+ }
+ private var resources = Resources(assets, DisplayMetrics(), config)
+
+ @Before
+ fun setUp() {
+ for (locale in LOCALES_TO_TEST) {
+ localeNames.add(locale.toString())
+ }
+ for (locale in locales) {
+ localeNames.add(locale.toString())
+ }
+ }
+
+ @Test
+ @Throws(IllegalAccessException::class)
+ fun testLoadAllPlural() {
+ val fields = R.plurals::class.java.declaredFields
+
+ val haveFormats = HashMap()
+ for (field in fields) {
+ // Log.i(TAG, field.getName());
+ val resId = field.getInt(Int::class.javaPrimitiveType)
+ val string = resources.getQuantityText(resId, 4)
+ // Log.i(TAG, field.getName() + ": '" + string + "'");
+ val matcher = androidFormat.matcher(string)
+ var matches = 0
+ val formats = CharArray(5)
+ while (matcher.find()) {
+ val match = matcher.group(0)
+ val formatType = match!!.get(match.length - 1)
+ when (match.length) {
+ 2 -> {
+ formats[matches] = formatType
+ matches++
+ }
+ 4 -> formats[match.substring(1, 2).toInt() - 1] = formatType
+ 5 -> formats[match.substring(1, 3).toInt() - 1] = formatType
+ else -> throw IllegalStateException(field.getName() + " has bad format: " + match)
+ }
+ }
+ haveFormats[field.getName()] = String(formats).trim { it <= ' ' }
+ }
+
+ for (locale in locales) {
+ config.locale = locale
+ // Resources() requires DisplayMetrics, but they are only needed for drawables
+ resources = Resources(assets, DisplayMetrics(), config)
+ for (field in fields) {
+ var formats: String? = null
+ try {
+ val resId = field.getInt(Int::class.javaPrimitiveType)
+ for (quantity in 0..566) {
+ resources.getQuantityString(resId, quantity)
+ }
+
+ formats = haveFormats[field.getName()]
+ when (formats) {
+ "d" -> resources.getQuantityString(resId, 1, 1)
+ "s" -> resources.getQuantityString(resId, 1, "ONE")
+ "ds" -> resources.getQuantityString(resId, 2, 1, "TWO")
+ else ->
+ check(TextUtils.isEmpty(formats)) { "Pattern not included in tests: " + formats }
+ }
+ } catch (e: IllegalFormatException) {
+ Log.i(TAG, locale.toString() + " " + field.getName())
+ throw IllegalArgumentException(
+ ("Bad '" + formats + "' format in " + locale + " " + field.getName() + ": " + e.message)
+ )
+ } catch (e: Resources.NotFoundException) {
+ Log.i(TAG, locale.toString() + " " + field.getName())
+ throw IllegalArgumentException(
+ ("Bad '" + formats + "' format in " + locale + " " + field.getName() + ": " + e.message)
+ )
+ }
+ }
+ }
+ }
+
+ @Test
+ @Throws(IllegalAccessException::class)
+ fun testLoadAllStrings() {
+ val fields = R.string::class.java.declaredFields
+
+ val haveFormats = HashMap()
+ for (field in fields) {
+ val string = resources.getString(field.getInt(Int::class.javaPrimitiveType))
+ val matcher = androidFormat.matcher(string)
+ var matches = 0
+ val formats = CharArray(5)
+ while (matcher.find()) {
+ val match = matcher.group(0)
+ val formatType = match!!.get(match.length - 1)
+ when (match.length) {
+ 2 -> {
+ formats[matches] = formatType
+ matches++
+ }
+ 4 -> formats[match.substring(1, 2).toInt() - 1] = formatType
+ 5 -> formats[match.substring(1, 3).toInt() - 1] = formatType
+ else -> throw IllegalStateException(field.getName() + " has bad format: " + match)
+ }
+ }
+ haveFormats[field.getName()] = String(formats).trim { it <= ' ' }
+ }
+
+ for (locale in locales) {
+ config!!.locale = locale
+ // Resources() requires DisplayMetrics, but they are only needed for drawables
+ resources = Resources(assets, DisplayMetrics(), config)
+ for (field in fields) {
+ val resId = field.getInt(Int::class.javaPrimitiveType)
+ resources.getString(resId)
+
+ val formats = haveFormats.get(field.getName())
+ try {
+ when (formats) {
+ "d" -> resources.getString(resId, 1)
+ "dd" -> resources.getString(resId, 1, 2)
+ "ds" -> resources.getString(resId, 1, "TWO")
+ "dds" -> resources.getString(resId, 1, 2, "THREE")
+ "sds" -> resources.getString(resId, "ONE", 2, "THREE")
+ "s" -> resources.getString(resId, "ONE")
+ "ss" -> resources.getString(resId, "ONE", "TWO")
+ "sss" -> resources.getString(resId, "ONE", "TWO", "THREE")
+ "ssss" -> resources.getString(resId, "ONE", "TWO", "THREE", "FOUR")
+ "ssd" -> resources.getString(resId, "ONE", "TWO", 3)
+ "sssd" -> resources.getString(resId, "ONE", "TWO", "THREE", 4)
+ else ->
+ check(TextUtils.isEmpty(formats)) { "Pattern not included in tests: " + formats }
+ }
+ } catch (e: Exception) {
+ Log.i(TAG, locale.toString() + " " + field.getName())
+ throw IllegalArgumentException(
+ ("Bad format in '" + locale + "' '" + field.getName() + "': " + e.message)
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val TAG: String = "LocalizationTest"
+
+ val LOCALES_TO_TEST: Array =
+ arrayOf(
+ Locale.ENGLISH,
+ Locale.FRENCH,
+ Locale.GERMAN,
+ Locale.ITALIAN,
+ Locale.JAPANESE,
+ Locale.KOREAN,
+ Locale.SIMPLIFIED_CHINESE,
+ Locale.TRADITIONAL_CHINESE,
+ Locale("zh", "HK"),
+ Locale("bo"),
+ Locale("af"),
+ Locale("ar"),
+ Locale("be"),
+ Locale("bg"),
+ Locale("ca"),
+ Locale("cs"),
+ Locale("da"),
+ Locale("el"),
+ Locale("es"),
+ Locale("eo"),
+ Locale("et"),
+ Locale("eu"),
+ Locale("fa"),
+ Locale("fi"),
+ Locale("he"),
+ Locale("hi"),
+ Locale("hu"),
+ Locale("hy"),
+ Locale("id"),
+ Locale("is"),
+ Locale("it"),
+ Locale("ml"),
+ Locale("my"),
+ Locale("nb"),
+ Locale("nl"),
+ Locale("pl"),
+ Locale("pt"),
+ Locale("ro"),
+ Locale("ru"),
+ Locale("sc"),
+ Locale("sk"),
+ Locale("sn"),
+ Locale("sr"),
+ Locale("sv"),
+ Locale("th"),
+ Locale("tr"),
+ Locale("uk"),
+ Locale("vi"),
+ )
+ }
+}
diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/CategoryItems.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/CategoryItems.kt
index 1f2383b88..5975aa7b1 100644
--- a/app/src/androidTest/java/org/fdroid/ui/screenshots/CategoryItems.kt
+++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/CategoryItems.kt
@@ -6,11 +6,39 @@ import org.fdroid.ui.categories.CategoryItem
fun getCategoryItems(localeList: LocaleListCompat) =
listOf(
+ CategoryItem(
+ id = "Action Game",
+ name =
+ mapOf(
+ "ar" to "العاب الآكشن",
+ "cs" to "Akční hra",
+ "de" to "Action-Spiel",
+ "en-GB" to "Action Game",
+ "en-US" to "Action Game",
+ "et" to "Märulimängud",
+ "fa" to "بازی کنشی",
+ "fi" to "Toimintapeli",
+ "fr" to "Jeu d'action",
+ "ga" to "Cluiche Gníomhaíochta",
+ "id" to "Game Aksi",
+ "ja" to "アクション",
+ "nl" to "Actie Spel",
+ "pl" to "Gra akcji",
+ "pt-BR" to "Jogo de ação",
+ "ro" to "Joc de acțiune",
+ "ru" to "Экшен-игра",
+ "vi" to "Game hành động",
+ "zh-CN" to "动作游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "AI Chat",
name =
mapOf(
+ "af" to "AI Chat",
"ar" to "محادثة ذكاء اصناعي",
+ "be" to "ШІ Размова",
"bg" to "Разговори с ИИ",
"ca" to "Xat d'IA",
"cs" to "AI chat",
@@ -30,6 +58,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"hu" to "MI csevegés",
"id" to "Chat AI",
"is" to "AI-spjall",
+ "it" to "Chat IA",
"ja" to "AI チャット",
"kn" to "AI ಚಾಟ್",
"ko" to "AI 채팅",
@@ -39,9 +68,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl" to "AI-chat",
"nl-BE" to "AI Chat",
"pl" to "Czat AI",
+ "pt" to "Conversar com IA",
"pt-BR" to "Conversar com IA",
+ "pt-PT" to "Conversar com IA",
"ro" to "Chat AI",
"ru" to "Чаты с ИИ",
+ "sk" to "AI Chat",
"sl" to "Klepet z UI",
"sq" to "Fjalosje IA",
"sr" to "Ћаскање са вештачким паметњаковићем",
@@ -60,6 +92,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "مدير تطبيق",
+ "be" to "Кіраўнік Праграм",
"bg" to "Управление на проложения",
"ca" to "Gestor d'aplicacions",
"cs" to "Správce aplikací",
@@ -72,6 +105,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Gestor de aplicaciones",
"et" to "Rakendustehaldur",
"fa" to "مدیر کاره",
+ "fi" to "Sovellusten hallinta",
"fr" to "Gestionnaire d'applications",
"ga" to "Bainisteoir Aipeanna",
"he" to "מנהל יישומונים",
@@ -79,6 +113,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"hu" to "Alkalmazáskezelő",
"id" to "Manajer Aplikasi",
"is" to "Forritastýring",
+ "it" to "Gestore di app",
"ja" to "アプリマネージャー",
"kn" to "ಅಪ್ಲಿಕೇಶನ್ ಮ್ಯಾನೇಜರ್",
"ko" to "앱 관리자",
@@ -88,9 +123,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl" to "Appbeheerder",
"nl-BE" to "App Manager",
"pl" to "Menedżer aplikacji",
+ "pt" to "Gestor de apps",
"pt-BR" to "Gerenciador de aplicativos",
+ "pt-PT" to "Gestor de apps",
"ro" to "Manager aplicații",
"ru" to "Менеджер приложений",
+ "sk" to "Správca aplikácií",
"sl" to "Upravitelj orodij",
"sq" to "Përgjegjës Aplikacionesh",
"sr" to "Управник програма",
@@ -109,6 +147,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "متجر التطبيقات والمُحدِّث",
+ "be" to "App Store і абнаўленні",
"bg" to "Магазин за приложения и обновяване",
"ca" to "Botiga d'aplicacions i actualitzador",
"ckb" to "ئاپستۆر و نوێکەرەوەکان",
@@ -129,7 +168,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ga" to "App Store & Nuashonraitheoir",
"gl" to "Tenda de aplicacións e actualizador",
"he" to "חנות יישומונים ומעדכן",
- "hr" to "Trgovina aplikacija i ažuriranje",
+ "hr" to "Trgovina aplikacija i alat za aktualiziranje",
"hu" to "Alkalmazásbolt és -frissítő",
"id" to "Toko Aplikasi & Aplikasi Pembaruan",
"is" to "Forritasafn og uppfærslur",
@@ -148,12 +187,13 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"pt-BR" to "Loja de apps e atualizador",
"pt-PT" to "Loja de apps e atualizador",
"ro" to "Magazin aplicații & Actualizări",
- "ru" to "Приложения и обновления",
+ "ru" to "Магазин приложений и обновления",
"sc" to "Butega de aplicatziones e atualizadore",
+ "sk" to "Obchod s aplikáciami a aktualizácie",
"sl" to "App trgovina in posodobitve",
"sq" to "Shitore & Përditësues Aplikacionesh",
"sr" to "Складиште и надграђивач програма",
- "sv" to "App Store och uppdateringsprogram",
+ "sv" to "Appbutik & uppdateringsprogram",
"sw" to "Ghala la Programu",
"ta" to "ஆப் கடை & அப்டேட்டர்",
"tr" to "Uygulama Deposu ve Güncelleyici",
@@ -165,11 +205,83 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Battery",
+ name =
+ mapOf(
+ "ar" to "البطارية",
+ "be" to "Батарэя",
+ "bg" to "Батерия",
+ "ca" to "Bateria",
+ "cs" to "Baterie",
+ "de" to "Akku",
+ "el" to "Μπαταρία",
+ "en-GB" to "Battery",
+ "en-US" to "Battery",
+ "eo" to "Baterio",
+ "es" to "Batería",
+ "et" to "Akuhaldus",
+ "fa" to "باتری",
+ "fi" to "Akku",
+ "fr" to "Batterie",
+ "ga" to "Ceallraí",
+ "hr" to "Baterija",
+ "hu" to "Akkumulátor",
+ "id" to "Baterai",
+ "is" to "Rafhlaða",
+ "it" to "Batteria",
+ "ja" to "バッテリー",
+ "nb" to "Batteri",
+ "nl" to "Batterij",
+ "pl" to "Bateria",
+ "pt" to "Pilha",
+ "pt-BR" to "Bateria",
+ "pt-PT" to "Pilha",
+ "ro" to "Baterie",
+ "ru" to "Батарея",
+ "sk" to "Batéria",
+ "sl" to "Baterija",
+ "sq" to "Bateri",
+ "sr" to "Батерија",
+ "tr" to "Batarya",
+ "uk" to "Батарея",
+ "vi" to "Pin",
+ "zh-CN" to "电池",
+ "zh-TW" to "電池",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Board Game",
+ name =
+ mapOf(
+ "ar" to "العاب الطاولة",
+ "cs" to "Desková hra",
+ "de" to "Brettspiel",
+ "en-GB" to "Board Game",
+ "en-US" to "Board Game",
+ "et" to "Lauamängud",
+ "fa" to "بازی صفحهای",
+ "fr" to "Jeu de société",
+ "ga" to "Cluiche Boird",
+ "id" to "Game Papan",
+ "ja" to "ボードゲーム",
+ "nl" to "Bordspel",
+ "pl" to "Gra planszowa",
+ "pt-BR" to "Jogo de tabuleiro",
+ "ro" to "Joc de societate",
+ "ru" to "Настольная игра",
+ "vi" to "Game cờ",
+ "zh-CN" to "桌游",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Bookmark",
name =
mapOf(
"ar" to "علامة",
+ "be" to "Закладка",
"bg" to "Отметки",
"ca" to "Adreça d'interès",
"cs" to "Záložka",
@@ -209,8 +321,9 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"pt-BR" to "Marcador",
"pt-PT" to "Marcador",
"ro" to "Semn de carte",
- "ru" to "Закладки",
+ "ru" to "Закладка",
"sc" to "Sinnalibru",
+ "sk" to "Záložky",
"sl" to "Zaznamek",
"sq" to "Faqerojtës",
"sr" to "Забелешка",
@@ -231,6 +344,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "المتصفح",
+ "be" to "Аглядальнік",
"bg" to "Мрежов четец",
"br" to "Merdeer",
"ca" to "Navegador",
@@ -257,6 +371,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Vafri",
"it" to "Browser",
"ja" to "ブラウザ",
+ "kab" to "Iminig",
"kn" to "ಬ್ರೌಸರ್",
"ko" to "브라우저",
"lb" to "Browser",
@@ -271,8 +386,9 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"pt-BR" to "Navegador",
"pt-PT" to "Navegador",
"ro" to "Navigator web",
- "ru" to "Браузеры",
+ "ru" to "Браузер",
"sc" to "Navigadore",
+ "sk" to "Prehliadač",
"sl" to "Brskalnik",
"sq" to "Shfletues",
"sr" to "Прегледник",
@@ -293,6 +409,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "حاسبة",
+ "be" to "Калькулятар",
"bg" to "Калкулатор",
"ca" to "Calculadora",
"cs" to "Kalkulačka",
@@ -318,6 +435,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Reiknivél",
"it" to "Calcolatrice",
"ja" to "計算機",
+ "kab" to "Tamsiḍent",
"kn" to "ಕ್ಯಾಲ್ಕುಲೇಟರ್",
"ko" to "계산기",
"lt" to "Skaičiuotuvas",
@@ -331,10 +449,11 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"pt-BR" to "Calculadora",
"pt-PT" to "Calculador",
"ro" to "Calculator",
- "ru" to "Калькуляторы",
+ "ru" to "Калькулятор",
"sc" to "Carculadora",
+ "sk" to "Kalkulačka",
"sl" to "Kalkulator",
- "sq" to "Llogaritës",
+ "sq" to "Makinë Llogaritëse",
"sr" to "Рачунар",
"sv" to "Kalkylator",
"sw" to "Kikokotoo",
@@ -353,6 +472,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "التقويم والأجندة",
+ "be" to "Каляндар і парадак дня",
"bg" to "Календар и график",
"ca" to "Calendaris i Agendes",
"cs" to "Kalendář a agenda",
@@ -367,7 +487,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es-MX" to "Calendario y Agenda",
"et" to "Kalender ja päevakava",
"fa" to "تقویم و دستور کار",
- "fi" to "Kalenteri & esityslista",
+ "fi" to "Kalenteri ja päivyri",
"fr" to "Calendrier & Agenda",
"ga" to "Féilire agus Clár Oibre",
"gl" to "Calendario e axenda",
@@ -384,15 +504,16 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Kalendārs un dienaskārtība",
"my" to "ပြက္ခဒိန်နှင့် လုပ်ငန်းစဉ်များ",
"nb" to "Kalender & Agenda",
- "nl" to "Kalender & agenda",
+ "nl" to "Kalender & Agenda",
"nl-BE" to "Kalender & Agenda",
"pl" to "Kalendarz i harmonogram",
"pt" to "Calendário e agenda",
"pt-BR" to "Calendário e agenda",
"pt-PT" to "Calendário e agenda",
"ro" to "Calendar & Agenda",
- "ru" to "Ежедневники",
+ "ru" to "Календари и ежедневники",
"sc" to "Calendàriu e agenda",
+ "sk" to "Kalendár a diár",
"sl" to "Koledar in urnik",
"sq" to "Kalendar & Plane",
"sr" to "Календар и роковник",
@@ -408,11 +529,61 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Card Game",
+ name =
+ mapOf(
+ "ar" to "العاب البطاقات",
+ "cs" to "Karetní hra",
+ "de" to "Kartenspiel",
+ "en-GB" to "Card Game",
+ "en-US" to "Card Game",
+ "et" to "Kaardimängud",
+ "fa" to "بازی کارتی",
+ "fr" to "Jeu de cartes",
+ "ga" to "Cluiche Cártaí",
+ "id" to "Game Kartu",
+ "ja" to "カードゲーム",
+ "nl" to "Kaartspel",
+ "pl" to "Gra karciana",
+ "pt-BR" to "Jogo de cartas",
+ "ro" to "Joc de cărți",
+ "ru" to "Карточная игра",
+ "vi" to "Game bài",
+ "zh-CN" to "纸牌游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Casual Game",
+ name =
+ mapOf(
+ "cs" to "Ležérní hra",
+ "de" to "Casual Game",
+ "en-GB" to "Casual Game",
+ "en-US" to "Casual Game",
+ "et" to "Vabaaja mäng",
+ "fr" to "Jeu occasionnel",
+ "ga" to "Cluiche Ócáideach",
+ "id" to "Game Kasual",
+ "ja" to "カジュアルゲーム",
+ "nl" to "Casual spel",
+ "pl" to "Gra rekreacyjna",
+ "pt-BR" to "Jogo casual",
+ "ro" to "Joc ocazional",
+ "ru" to "Казуальная игра",
+ "vi" to "Game casual",
+ "zh-CN" to "休闲游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Clock",
name =
mapOf(
"ar" to "ساعة",
+ "be" to "Гадзіннік",
+ "bg" to "Часовник",
"br" to "Horolaj",
"ca" to "Rellotge",
"cs" to "Hodiny",
@@ -425,6 +596,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Reloj",
"et" to "Kellad",
"fa" to "ساعت",
+ "fi" to "Kello",
"fr" to "Horloge",
"ga" to "Clog",
"he" to "שעון",
@@ -432,6 +604,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"hu" to "Óra",
"id" to "Jam",
"is" to "Klukka",
+ "it" to "Orologio",
"ja" to "時計",
"kn" to "ಗಡಿಯಾರ",
"ko" to "시계",
@@ -441,9 +614,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl" to "Klok",
"nl-BE" to "Klok",
"pl" to "Zegar",
+ "pt" to "Relógio",
"pt-BR" to "Relógio",
+ "pt-PT" to "Relógio",
"ro" to "Ceas",
"ru" to "Часы",
+ "sk" to "Hodiny",
"sl" to "Ura",
"sq" to "Sahat",
"sr" to "Часовник",
@@ -462,12 +638,13 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "التخزين السحابي ومزامنة الملفات",
+ "be" to "Воблачнае Сховішча і сінхранізацыя файлаў",
"bg" to "Облачно хранилище и синхронизиране",
"ca" to "Emmagatzematge al núvol i sincronització d'arxius",
"cs" to "Cloudové úložiště a synchronizace souborů",
"da" to "Cloudlager og filsynkronisering",
- "de" to "Cloud-Speicher / Dateisynchronisierung",
- "el" to "Cloud Αποθηκευτικός Χώρος & Συγχρονισμός Αρχείων",
+ "de" to "Cloudspeicher / Dateisynchronisierung",
+ "el" to "Μεταφόρτωση και συγχρονισμός αρχείων",
"en-GB" to "Cloud Storage & File Sync",
"en-US" to "Cloud Storage & File Sync",
"eo" to "Nuba konservado kaj dosiera samtempigo",
@@ -491,7 +668,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Mākoņkrātuve un datņu sinhronizēšana",
"my" to "Cloud သိုလှောင်မှုနှင့် ဖိုင်ချိတ်ဆက်ခြင်း",
"nb" to "Skylagring & Filsynkronisering",
- "nl" to "Cloudopslag & bestandssynchronisatie",
+ "nl" to "Cloudopslag & Bestanden Synchroniseren",
"nl-BE" to "Cloud opslag & Bestand Synchronisatie",
"pl" to "Przechowywanie w chmurze i synchronizacja plików",
"pt" to "Armazenamento na nuvem e sincronização de ficheiros",
@@ -500,6 +677,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Stocare în cloud & Sincronizare fișiere",
"ru" to "Хранение и синхронизация",
"sc" to "Archiviatzione in sa nue e sincronizada de documentos",
+ "sk" to "Úložisko v cloude a synchronizácia súborov",
"sl" to "Shranjevanje v oblaku in sinhronizacija datotek",
"sq" to "Depozitim Në Re & Njëkohësim Kartelash",
"sr" to "Складиште у облаку и усклађивање датотека",
@@ -585,7 +763,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Conectivitate",
"ru" to "Связь",
"sc" to "Connetividade",
- "sk" to "Pripojiteľnosť",
+ "sk" to "Pripojenie",
"sl" to "Povezljivost",
"sn" to "Mahakiro",
"so" to "Ku",
@@ -614,6 +792,8 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "التواصل",
+ "be" to "Кантакты",
+ "bg" to "Контакт",
"ca" to "Contacte",
"cs" to "Kontakt",
"da" to "Kontakt",
@@ -625,6 +805,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Contactos",
"et" to "Kontaktihaldus",
"fa" to "آشنا",
+ "fi" to "Yhteystiedot",
"fr" to "Contact",
"ga" to "Teagmháil",
"he" to "פרטי קשר",
@@ -632,6 +813,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"hu" to "Névjegy",
"id" to "Kontak",
"is" to "Tengiliðir",
+ "it" to "Contatto",
"ja" to "連絡",
"kn" to "ಸಂಪರ್ಕಿಸಿ",
"ko" to "연락처",
@@ -640,9 +822,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl" to "Contact",
"nl-BE" to "Contact",
"pl" to "Kontakt",
+ "pt" to "Contacto",
"pt-BR" to "Contato",
+ "pt-PT" to "Contacto",
"ro" to "Contact",
"ru" to "Контакты",
+ "sk" to "Kontakty",
"sl" to "Stiki",
"sq" to "Kontakt",
"sr" to "Додир",
@@ -651,7 +836,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"tr" to "İletişim",
"uk" to "Контакти",
"vi" to "Danh bạ",
- "zh-CN" to "联络",
+ "zh-CN" to "联系人",
"zh-TW" to "聯絡",
)
.getBestLocale(localeList) ?: "Unknown Category",
@@ -720,7 +905,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl-BE" to "Ontwikkeling",
"nn" to "Utvikling",
"pa" to "ਡਿਵੈਲਪਮੈਂਟ",
- "pl" to "Rozwój",
+ "pl" to "Rozwój oprogramowania",
"ps" to "پرمختيا",
"pt" to "Desenvolvimento",
"pt-BR" to "Desenvolvimento",
@@ -752,12 +937,75 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Dice",
+ name =
+ mapOf(
+ "ar" to "النرد",
+ "cs" to "Kostky",
+ "de" to "Würfel",
+ "en-GB" to "Dice",
+ "en-US" to "Dice",
+ "et" to "Täringumängud",
+ "fa" to "تاس",
+ "fr" to "Dés",
+ "ga" to "Dísle",
+ "id" to "Dadu",
+ "ja" to "賽子",
+ "nl" to "Dobbelsteen",
+ "pl" to "Kości",
+ "pt-BR" to "Dados",
+ "ro" to "Zar",
+ "ru" to "Игральные кости",
+ "vi" to "Xúc xắc",
+ "zh-CN" to "骰子",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Diet",
+ name =
+ mapOf(
+ "ar" to "حمية",
+ "cs" to "Dieta",
+ "de" to "Diät",
+ "el" to "Δίαιτα",
+ "en-GB" to "Diet",
+ "en-US" to "Diet",
+ "eo" to "Dieto",
+ "es" to "Dieta",
+ "et" to "Toiduvalik",
+ "fa" to "رژیم",
+ "fr" to "Régime alimentaire",
+ "ga" to "Aiste bia",
+ "hr" to "Dijeta",
+ "hu" to "Étrend",
+ "id" to "Diet",
+ "is" to "Mataræði",
+ "ja" to "ダイエット",
+ "nl" to "Diëet",
+ "pl" to "Dieta",
+ "pt" to "Dieta",
+ "pt-BR" to "Dieta",
+ "pt-PT" to "Dieta",
+ "ro" to "Dietă",
+ "ru" to "Диета",
+ "sk" to "Strava",
+ "sl" to "Dieta",
+ "sr" to "Исхрана",
+ "tr" to "Diyet",
+ "vi" to "Ăn kiêng",
+ "zh-CN" to "饮食",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "DNS & Hosts",
name =
mapOf(
"ar" to "DNS والمضيفون",
"ba" to "DNS һәм хост",
+ "be" to "DNS і хасты",
"bg" to "DNS и хостове",
"ca" to "DNS i Allotjament",
"cs" to "DNS a hostitelé",
@@ -767,7 +1015,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"en-GB" to "DNS & Hosts",
"en-US" to "DNS & Hosts",
"eo" to "DNS kaj retgastigo",
- "es" to "DNS y bloqueos de red",
+ "es" to "DNS y Hospedajes",
"et" to "Nimelahendus ja hostid",
"fa" to "ساناد و میزبانها",
"fr" to "DNS & Hôtes",
@@ -794,6 +1042,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "DNS & Gazde",
"ru" to "DNS и хосты",
"sc" to "DNS e retzidores (hosts)",
+ "sk" to "DNS a hostitelia",
"sl" to "DNS in gostitelji",
"sq" to "DNS & Strehë",
"sr" to "ДНС и домаћини",
@@ -809,11 +1058,28 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Download",
+ name =
+ mapOf(
+ "cs" to "Stahování",
+ "de" to "Download",
+ "en-US" to "Download",
+ "et" to "Allalaadimised",
+ "fa" to "بارگیری",
+ "pl" to "Pobieranie",
+ "pt-BR" to "Download",
+ "ro" to "Descărcare",
+ "zh-CN" to "下载",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Draw",
name =
mapOf(
"ar" to "رسم",
+ "be" to "Маляванне",
"bg" to "Рисуване",
"ca" to "Dibuix",
"cs" to "Kreslení",
@@ -852,6 +1118,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Desen",
"ru" to "Рисование",
"sc" to "Disinnu",
+ "sk" to "Kreslenie",
"sl" to "Riši",
"sq" to "Vizatim",
"sr" to "Цртање",
@@ -872,16 +1139,17 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "قارئ الكتب الإلكترونية",
+ "be" to "Чытальнікі электронных кніг",
"bg" to "Четене на електронни книги",
"ca" to "Lector de llibres electrònics",
"cs" to "Čtečka e-knih",
"da" to "E-bogslæser",
"de" to "E-Book-Reader",
- "el" to "Αναγνώστης ηλεκτρονικών βιβλίων",
+ "el" to "Ανάγνωση ηλεκτρονικών βιβλίων",
"en-GB" to "Ebook Reader",
"en-US" to "Ebook Reader",
"eo" to "Legiloj de bitlibroj",
- "es" to "Lector de libros electrónicos",
+ "es" to "Lector de libro-e",
"et" to "E-raamatute lugemine",
"fa" to "کتابخوان",
"fi" to "E-kirjan lukija",
@@ -910,6 +1178,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Carte electronică",
"ru" to "Читалки",
"sc" to "Leghidore de libros eletrònicos",
+ "sk" to "Čítačka e-kníh",
"sl" to "Bralnik e-knjig",
"sq" to "Lexues Elibrash",
"sr" to "Читач е-књига",
@@ -925,23 +1194,49 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Educational Game",
+ name =
+ mapOf(
+ "ar" to "الالعاب التعليمية",
+ "cs" to "Vzdělávací hra",
+ "de" to "Lernspiel",
+ "en-GB" to "Educational Game",
+ "en-US" to "Educational Game",
+ "et" to "Harivad mängud",
+ "fa" to "بازی آموزشی",
+ "fr" to "Jeu éducatif",
+ "ga" to "Cluiche Oideachasúil",
+ "id" to "Game Edukasi",
+ "ja" to "教育向けゲーム",
+ "nl" to "Educatief spel",
+ "pl" to "Gra edukacyjna",
+ "pt-BR" to "Jogo educacional",
+ "ro" to "Joc educațional",
+ "ru" to "Образовательная игра",
+ "vi" to "Game giáo dục",
+ "zh-CN" to "益智游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Email",
name =
mapOf(
"ar" to "البريد الإلكتروني",
"ba" to "Email",
+ "be" to "Электронная пошта",
"bg" to "Електронна поща",
"br" to "Postel",
"ca" to "Correu electrònic",
"cs" to "E-mail",
"da" to "E-mail",
"de" to "E-Mail",
- "el" to "Email",
+ "el" to "Ηλεκτρονικό ταχυδρομείο",
"en-GB" to "Email",
"en-US" to "Email",
"eo" to "Retpoŝto",
- "es" to "Correo electrónico",
+ "es" to "Correo-e",
"es-MX" to "Email",
"et" to "E-post",
"fa" to "رایانامه",
@@ -956,6 +1251,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Tölvupóstur",
"it" to "Email",
"ja" to "Eメール",
+ "kab" to "Imayl",
"kn" to "ಇಮೇಲ್",
"ko" to "이메일",
"lb" to "E-Mail",
@@ -972,6 +1268,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "E-mail",
"ru" to "Электронная почта",
"sc" to "Posta eletrònica",
+ "sk" to "E-mail",
"sl" to "E-pošta",
"sq" to "Email",
"sr" to "Е-пошта",
@@ -987,25 +1284,51 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Emulator",
+ name =
+ mapOf(
+ "ar" to "مُحاكي",
+ "cs" to "Emulátor",
+ "de" to "Emulator",
+ "en-GB" to "Emulator",
+ "en-US" to "Emulator",
+ "et" to "Emulaatorid",
+ "fa" to "شبیهساز",
+ "fr" to "Émulateur",
+ "ga" to "Aithriseoir",
+ "id" to "Emulator",
+ "ja" to "エミュレータ",
+ "nl" to "Emulator",
+ "pl" to "Emulator",
+ "pt-BR" to "Emulador",
+ "ro" to "Emulator",
+ "ru" to "Эмулятор",
+ "vi" to "Giả lập",
+ "zh-CN" to "模拟器",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "File Encryption & Vault",
name =
mapOf(
"ar" to "تعمية الملفات والخزنة",
"ba" to "Файлдарҙы шифрлау һәм Һаҡлағыс",
+ "be" to "Шыфраванне файлаў і сховішча",
"bg" to "Шифроване на файлове и хранилище",
"ca" to "Xifratge de fitxers i caixa forta",
"cs" to "Šifrování souborů a trezor",
"da" to "Filkryptering og boks",
"de" to "Dateiverschlüsselung / Tresor",
- "el" to "Κρυπτογράφηση Αρχείων & Ασφαλής Φάκελος",
+ "el" to "Κρυπτογράφηση Αρχείων & Φακέλων",
"en-GB" to "File Encryption & Vault",
"en-US" to "File Encryption & Vault",
"eo" to "Dosiera ĉifrado",
"es" to "Cifrado de archivos y bóvedas",
"et" to "Krüptimine ja andmehoidlad",
"fa" to "رمزنگاری پرونده",
- "fi" to "Tiedoston salaus & holvi",
+ "fi" to "Tiedostojen salaus",
"fr" to "Chiffrement de fichiers & Coffre-fort",
"ga" to "Criptiú Comhad & Cruinneachán",
"he" to "הצפנת קבצים וכספת",
@@ -1020,7 +1343,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Datņu šifrēšana un glabātava",
"my" to "ဖိုင်လုံခြုံရေး နှင့် သိုလှောင်ခန်း",
"nb" to "Filkryptering & Hvelv",
- "nl" to "Bestandsversleuteling & kluis",
+ "nl" to "Bestandsversleuteling & Kluis",
"nl-BE" to "Bestanden versleuteling en kluis",
"pl" to "Szyfrowanie plików i sejf",
"pt" to "Criptografia de ficheiros e cofre",
@@ -1029,6 +1352,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Criptare fișiere și seif",
"ru" to "Шифрование файлов",
"sc" to "Tzifradura de documentos e cassaforte",
+ "sk" to "Šifrovanie súborov a trezor",
"sl" to "Šifriranje datotek in trezor",
"sq" to "Fshehtëzim & Kasafortë Kartelash",
"sr" to "Шифровање и складиштење датотека",
@@ -1044,12 +1368,29 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "File Manager",
+ name =
+ mapOf(
+ "cs" to "Správce souborů",
+ "de" to "Dateimanager",
+ "en-US" to "File Manager",
+ "et" to "Failihaldurid",
+ "fa" to "مدیر پرونده",
+ "pl" to "Menedżer plików",
+ "pt-BR" to "Gerenciador de arquivos",
+ "ro" to "Manager fișier",
+ "zh-CN" to "文件管理器",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "File Transfer",
name =
mapOf(
"ar" to "نقل الملفات",
"ba" to "Файлдарҙы тапшырыу",
+ "be" to "Перадача файлаў",
"bg" to "Прехвърляне на файлове",
"ca" to "Transferències de fitxers",
"cs" to "Přenos souborů",
@@ -1059,7 +1400,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"en-GB" to "File Transfer",
"en-US" to "File Transfer",
"eo" to "Dosiera kunhavigo",
- "es" to "Transferencia de archivos",
+ "es" to "Transferencia de archivo",
"et" to "Failide edastamine",
"fa" to "انتقال پرونده",
"fi" to "Tiedostonsiirto",
@@ -1087,6 +1428,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Transfer fișiere",
"ru" to "Передача файлов",
"sc" to "Tràmuda de documentos",
+ "sk" to "Prenos súborov",
"sl" to "Posredovanje datotek",
"sq" to "Shpërngulje Kartelash",
"sr" to "Пренос датотека",
@@ -1107,18 +1449,20 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "مدير مالي",
+ "be" to "Кіраўнік Фінансаў",
"bg" to "Управление на файлове",
"ca" to "Gestor de finances",
"cs" to "Správce financí",
"da" to "Økonomihåndtering",
"de" to "Finanzmanager",
- "el" to "Διαχειριστής Οικονομικών",
+ "el" to "Διαχείριση Οικονομικών",
"en-GB" to "Finance Manager",
"en-US" to "Finance Manager",
"eo" to "Financ-administriloj",
"es" to "Contabilidad y finanzas",
"et" to "Rahahaldus",
"fa" to "مدیر مالی",
+ "fi" to "Raha-asiat",
"fr" to "Gestionnaire de finances",
"ga" to "Bainisteoir Airgeadais",
"he" to "ניהול כספים",
@@ -1143,6 +1487,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Manager financiar",
"ru" to "Финансовый менеджер",
"sc" to "Gestore de finàntzias",
+ "sk" to "Správca financií",
"sl" to "Upravitelj financ",
"sq" to "Përgjegjës Financash",
"sr" to "Управник новцем",
@@ -1162,17 +1507,20 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "جدار حماية",
+ "be" to "Заслона",
+ "bg" to "Защитна стена",
"ca" to "Tallafoc",
"cs" to "Firewall",
"da" to "Firewall",
"de" to "Firewall",
- "el" to "Τοίχος Προστασίας",
+ "el" to "Τείχος Προστασίας",
"en-GB" to "Firewall",
"en-US" to "Firewall",
"eo" to "Farjoŝirmiloj",
"es" to "Cortafuegos",
"et" to "Tulemüürid",
"fa" to "دیوار آتش",
+ "fi" to "Palomuuri",
"fr" to "Pare-feu",
"ga" to "Balla dóiteáin",
"he" to "חומת אש",
@@ -1180,6 +1528,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"hu" to "Tűzfal",
"id" to "Firewall",
"is" to "Eldveggur",
+ "it" to "Firewall",
"ja" to "ファイアウォール",
"kn" to "ಫೈರ್ವಾಲ್",
"ko" to "방화벽",
@@ -1189,11 +1538,15 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl" to "Firewall",
"nl-BE" to "Firewall",
"pl" to "Zapora",
+ "pt" to "Firewall",
"pt-BR" to "Firewall",
+ "pt-PT" to "Firewall",
"ro" to "Firewall",
"ru" to "Брандмауэр",
+ "sk" to "Firewall",
"sl" to "Požarni zid",
"sq" to "Firewall",
+ "sr" to "Ватрени зид",
"sv" to "Brandvägg",
"sw" to "Firewall",
"tr" to "Güvenlik Duvarı",
@@ -1204,11 +1557,57 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Flashlight",
+ name =
+ mapOf(
+ "ar" to "مصباح",
+ "be" to "Ліхтарык",
+ "bg" to "Фенерче",
+ "ca" to "Llanterna",
+ "cs" to "Svítilna",
+ "de" to "Taschenlampe",
+ "el" to "Φακός",
+ "en-GB" to "Torch",
+ "en-US" to "Flashlight",
+ "eo" to "Poŝlampo",
+ "es" to "Linterna",
+ "et" to "Välklambid",
+ "fa" to "چراغقوه",
+ "fi" to "Taskulamppu",
+ "fr" to "Lampe de poche",
+ "ga" to "Tóirse",
+ "hr" to "Svjetiljka",
+ "hu" to "Zseblámpa",
+ "id" to "Senter",
+ "is" to "Vasaljós",
+ "it" to "Torcia",
+ "ja" to "懐中電灯",
+ "nb" to "Lommelykt",
+ "nl" to "Zaklamp",
+ "pl" to "Latarka",
+ "pt" to "Lanterna",
+ "pt-BR" to "Lanterna",
+ "pt-PT" to "Lanterna",
+ "ro" to "Lanternă",
+ "ru" to "Фонарик",
+ "sk" to "Baterka",
+ "sl" to "Svetilka",
+ "sr" to "Лампе",
+ "tr" to "Fener",
+ "uk" to "Ліхтарик",
+ "vi" to "Đèn pin",
+ "zh-CN" to "手电筒",
+ "zh-TW" to "手電筒",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Forum",
name =
mapOf(
"ar" to "المنتدى",
+ "be" to "Форум",
"bg" to "Форум",
"br" to "Forom",
"ca" to "Fòrum",
@@ -1222,6 +1621,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Foros",
"et" to "Foorumid",
"fa" to "انجمن",
+ "fi" to "Foorumi",
"fr" to "Forum",
"ga" to "Fóram",
"he" to "פורום",
@@ -1246,8 +1646,10 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"pt-PT" to "Fórum",
"ro" to "Forum",
"ru" to "Форум",
+ "sk" to "Fórum",
"sl" to "Klepetalnica",
"sq" to "Forum",
+ "sr" to "Збор",
"sv" to "Forum",
"sw" to "Jukwaa la majadiliano",
"ta" to "மன்றம்",
@@ -1264,6 +1666,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "معرض",
+ "be" to "Галерэя",
"bg" to "Галерия",
"ca" to "Galeria",
"cs" to "Galerie",
@@ -1302,6 +1705,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Galerie",
"ru" to "Галерея",
"sc" to "Galleria",
+ "sk" to "Galéria",
"sl" to "Galerija",
"sq" to "Galeri",
"sr" to "Излог слика",
@@ -1318,100 +1722,27 @@ fun getCategoryItems(localeList: LocaleListCompat) =
.getBestLocale(localeList) ?: "Unknown Category",
),
CategoryItem(
- id = "Games",
+ id = "Game Helper",
name =
mapOf(
- "af" to "Speletjies",
- "ar" to "الألعاب",
- "ast" to "Xuegos",
- "ba" to "Уйындар",
- "be" to "Гульні",
- "ber" to "ⵓⵔⴰⵔⵏ",
- "bg" to "Игри",
- "bn" to "গেমস",
- "bn-BD" to "গেমস",
- "bo" to "རྩེད་རིགས།",
- "br" to "C'hoarioù",
- "ca" to "Jocs",
- "ckb" to "یاری",
- "cs" to "Hry",
- "cy" to "Gemau",
- "da" to "Spil",
- "de" to "Spiele",
- "el" to "Παιχνίδια",
- "en-GB" to "Games",
- "en-US" to "Games",
- "eo" to "Ludoj",
- "es" to "Juegos",
- "es-AR" to "Juegos",
- "es-MX" to "Juegos",
- "et" to "Mängud",
- "eu" to "Jolasak",
- "fa" to "بازی",
- "fi" to "Pelit",
- "fil" to "Laro",
- "fr" to "Jeux",
- "fy" to "Spultsjes",
- "ga" to "Cluichí",
- "gd" to "Geamannan",
- "gl" to "Xogos",
- "he" to "משחקים",
- "hi" to "गेम्स",
- "hr" to "Igre",
- "hu" to "Játékok",
- "hy" to "Խաղեր",
- "id" to "Permainan",
- "is" to "Leikir",
- "it" to "Giochi",
- "ja" to "ゲーム",
- "kab" to "Uraren",
- "kmr" to "Lîsk",
- "kn" to "ಆಟಗಳು",
- "ko" to "게임",
- "lb" to "Spiller",
- "lt" to "Žaidimai",
- "lv" to "Spēles",
- "mk" to "Игри",
- "ml" to "കളികൾ",
- "mn" to "Тоглоомууд",
- "mr" to "खेळ",
- "my" to "ဂိမ်းများ",
- "nb" to "Spill",
- "ne" to "खेलहरू",
- "nl" to "Spellen",
- "nl-BE" to "Spellen",
- "nn" to "Spel",
- "pa" to "ਖੇਡਾਂ",
- "pl" to "Gry",
- "ps" to "لوبې",
- "pt" to "Jogos",
- "pt-BR" to "Jogos",
- "pt-PT" to "Jogos",
- "ro" to "Jocuri",
- "ru" to "Игры",
- "sc" to "Giogos",
- "si" to "ක්රීඩා",
- "sk" to "Hry",
- "sl" to "Igre",
- "sn" to "Mitambo",
- "so" to "Geemamka",
- "sq" to "Lojëra",
- "sr" to "Игре",
- "sv" to "Spel",
- "sw" to "Michezo",
- "ta" to "விளையாட்டுகள்",
- "te" to "ఆట",
- "th" to "เกม",
- "tr" to "Oyunlar",
- "tzm" to "uraren",
- "ug" to "ئويۇنلار",
- "uk" to "Ігри",
- "ur" to "گیمس",
- "vi" to "Trò chơi",
- "yue" to "遊戲",
- "zh-CN" to "游戏",
- "zh-HK" to "遊戲",
- "zh-TW" to "遊戲",
+ "ar" to "مُساعد الالعاب",
+ "cs" to "Herní pomocník",
+ "de" to "Spielhilfe",
+ "en-GB" to "Game Helper",
+ "en-US" to "Game Helper",
+ "et" to "Mängude abitarvikud",
+ "fa" to "یاریگر بازی",
+ "fr" to "Aide au jeu",
+ "ga" to "Cúntóir Cluiche",
+ "id" to "Bantuan Game",
+ "ja" to "ゲームヘルパー",
+ "nl" to "Spellenhelper",
+ "pl" to "Pomocnik do gry",
+ "pt-BR" to "Ajudante de jogo",
+ "ro" to "Instrument de ajutor pentru joc",
+ "ru" to "Игровой помощник",
+ "vi" to "Trợ giúp game",
+ "zh-CN" to "游戏助手",
)
.getBestLocale(localeList) ?: "Unknown Category",
),
@@ -1444,7 +1775,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es-MX" to "Gráficos",
"et" to "Graafika",
"eu" to "Grafikoak",
- "fa" to "گرافیک",
+ "fa" to "نگاشتار",
"fi" to "Grafiikka",
"fil" to "Grapiks",
"fr" to "Graphisme",
@@ -1520,13 +1851,14 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ca" to "Registre d'hàbits",
"cs" to "Sledování návyků",
"de" to "Gewohnheitstracker",
- "el" to "Ιχνηλάτης Συνηθειών",
+ "el" to "Καταγραφή Συνηθειών",
"en-GB" to "Habit Tracker",
"en-US" to "Habit Tracker",
"eo" to "Spuriloj de kutimoj",
"es" to "Seguimiento de hábitos",
"et" to "Elustiil",
"fa" to "ردیاب عادت",
+ "fi" to "Rutiinien seuranta",
"fr" to "Suivi des habitudes",
"ga" to "Rianaitheoir Nósanna",
"he" to "מעקב הרגלים",
@@ -1550,8 +1882,10 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"pt-PT" to "Rastejador de hábitos",
"ro" to "Jurnal de obiceiuri",
"ru" to "Трекер привычек",
+ "sk" to "Sledovanie návykov",
"sl" to "Sledilnik navad",
"sq" to "Ndjekës Zakonesh",
+ "sr" to "Праћење навика",
"sv" to "Vanespårare",
"sw" to "Kifuatilia Tabia",
"ta" to "பழக்கமான டிராக்கர்",
@@ -1563,17 +1897,52 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Health Manager",
+ name =
+ mapOf(
+ "ar" to "مدير الصحة",
+ "cs" to "Správce zdraví",
+ "de" to "Gesundheitsmanager",
+ "el" to "Διαχείριση Υγείας",
+ "en-GB" to "Health Manager",
+ "en-US" to "Health Manager",
+ "eo" to "Administriloj de sano",
+ "et" to "Tervisehaldus",
+ "fa" to "مدیر سلامتی",
+ "fr" to "Responsable santé",
+ "ga" to "Bainisteoir Sláinte",
+ "hr" to "Upravljač zdravljem",
+ "hu" to "Egészségkezelő",
+ "id" to "Manajer Kesehatan",
+ "is" to "Heilsustjórnun",
+ "ja" to "健康管理",
+ "nl" to "Gezondheidsbeheer",
+ "pl" to "Menedżer zdrowia",
+ "pt" to "Gestão de saúde",
+ "pt-BR" to "Gerenciamento de saúde",
+ "pt-PT" to "Gestão de saúde",
+ "ro" to "Manager de sănătate",
+ "sk" to "Správca zdravia",
+ "sl" to "Upravljalnik zdravja",
+ "tr" to "Sağlık Yönetimi",
+ "vi" to "Quản lý sức khỏe",
+ "zh-CN" to "健康管理工具",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Icon Pack",
name =
mapOf(
"ar" to "حزمة الأيقونات",
+ "be" to "Пачкі значкоў",
"bg" to "Пакет с пиктограми",
"ca" to "Pack d'icones",
"cs" to "Balíček ikon",
"da" to "Ikonpakke",
- "de" to "Symbol-Paket",
- "el" to "Πακέτο Εικονιδίων",
+ "de" to "Symbolpaket",
+ "el" to "Πακέτα Εικονιδίων",
"en-GB" to "Icon Pack",
"en-US" to "Icon Pack",
"eo" to "Bildsimbol-pakaĵoj",
@@ -1605,6 +1974,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Pachet iconițe",
"ru" to "Наборы значков",
"sc" to "Pachete de iconas",
+ "sk" to "Balík ikon",
"sl" to "Zbirka ikon",
"sq" to "Paketë Ikonash",
"sr" to "Омот сличица",
@@ -1723,17 +2093,19 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "جرد",
+ "be" to "Інвэнтар",
"ca" to "Inventari",
"cs" to "Inventář",
"da" to "Inventar",
"de" to "Inventar",
- "el" to "Απόθεμα",
+ "el" to "Απογραφή",
"en-GB" to "Inventory",
"en-US" to "Inventory",
"eo" to "Administriloj de posedaĵoj",
"es" to "Inventario",
"et" to "Varahaldus",
"fa" to "موجودی",
+ "fi" to "Varasto",
"fr" to "Inventaire",
"ga" to "Fardal",
"he" to "מלאי",
@@ -1750,8 +2122,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl" to "Inventarisatie",
"nl-BE" to "Voorraad",
"pl" to "Zasoby",
+ "pt" to "Inventário",
"pt-BR" to "Inventário",
+ "pt-PT" to "Inventário",
"ro" to "Inventar",
+ "ru" to "Инвентарь",
+ "sk" to "Inventár",
"sl" to "Popis imetja",
"sq" to "Inventar",
"sv" to "Inventarie",
@@ -1769,6 +2145,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "لوحة المفاتيح وIME",
+ "be" to "Клавіятура і ўвод",
"bg" to "Клавиатура и въвеждане",
"ca" to "Teclat i IME",
"cs" to "Klávesnice a IME",
@@ -1806,6 +2183,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Tastatură & IME",
"ru" to "Клавиатуры и ввод",
"sc" to "Tecladu e mètodos de intrada (IME)",
+ "sk" to "Klávesnica a IME",
"sl" to "Tipkovnica in urejevalec vnosa IME",
"sq" to "Tastierë & IME",
"sr" to "Тастатура и начин уноса",
@@ -1826,12 +2204,13 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "مُطلِق",
+ "be" to "Запускальнік",
"bg" to "Начален екран",
"ca" to "Llançador",
"cs" to "Launcher",
"da" to "Launcher",
"de" to "Launcher",
- "el" to "Εκκινητής",
+ "el" to "Εκκίνηση Εφαρμογών",
"en-GB" to "Launcher",
"en-US" to "Launcher",
"eo" to "Aplikaĵlanĉiloj",
@@ -1863,6 +2242,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Lansator",
"ru" to "Лаунчер",
"sc" to "Aviadores de aplicatziones",
+ "sk" to "Spúšťač",
"sl" to "Zaganjalnik",
"sq" to "Nisës",
"sr" to "Покретач",
@@ -1883,6 +2263,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "مشغل الوسائط المحلي",
+ "be" to "Лакальны Медыя Прайгравальнік",
"bg" to "Местно изпълняване на медия",
"ca" to "Reproductor local de mitjans",
"cs" to "Přehrávač lokálních médií",
@@ -1920,6 +2301,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Vizualizator media local",
"ru" to "Медиаплееры",
"sc" to "Leghidore multimediale locale",
+ "sk" to "Prehrávač miestnych médií",
"sl" to "Lokalni predvajalnik glasbe",
"sq" to "Lojtës Mediash Vendore",
"sr" to "Местни извођач садржаја",
@@ -1940,17 +2322,19 @@ fun getCategoryItems(localeList: LocaleListCompat) =
name =
mapOf(
"ar" to "متتبع ومشارك الموقع",
+ "be" to "Адсочванне і абагульванне месцазнаходжання",
"bg" to "Публикуване и споделяне на местоположение",
"ca" to "Seguiment i compartició d'ubicació",
"cs" to "Sledování a sdílení polohy",
"de" to "Standort-Tracker / -Teiler",
- "el" to "Ιχνηλάτης & Διαμοιραστής Τοποθεσίας",
+ "el" to "Εντοπισμός & Κοινοποίηση Τοποθεσίας",
"en-GB" to "Location Tracker & Sharer",
"en-US" to "Location Tracker & Sharer",
"eo" to "Spuriloj (kaj kunhavigiloj) de pozicio",
"es" to "Seguir y compartir mi ubicación",
"et" to "Asukoha logimine ja jagamine",
"fa" to "ردیابی و همرسانی مکان",
+ "fi" to "Sijainnin seuranta ja jakaminen",
"fr" to "Suivi & partage de localisation",
"ga" to "Rianaitheoir Suímh & Roinnteoir",
"he" to "מעקב ושיתוף מיקום",
@@ -1968,9 +2352,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl" to "Locatie bijhouden en delen",
"nl-BE" to "Locatietracker en -deler",
"pl" to "Śledzenie i udostępnianie lokalizacji",
+ "pt" to "Rastreador e compartilhador de localização",
"pt-BR" to "Rastreador e compartilhador de localização",
+ "pt-PT" to "Rastreador e compartilhador de localização",
"ro" to "Localizare și partajare locație",
"ru" to "Отслеживание и публикация местоположения",
+ "sk" to "Sledovanie a zdieľanie polohy",
"sl" to "Sledilnik in delilnik položaja",
"sq" to "Gjurmues & Dhënës Vendndodhjesh",
"sv" to "Platsspårare och delare",
@@ -1983,6 +2370,29 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Meditation",
+ name =
+ mapOf(
+ "ar" to "الاسترخاء",
+ "cs" to "Meditace",
+ "de" to "Meditation",
+ "en-GB" to "Meditation",
+ "en-US" to "Meditation",
+ "et" to "Mediteerimine",
+ "fa" to "مراقبه",
+ "fr" to "Méditation",
+ "ga" to "Machnamh",
+ "nl" to "Meditatie",
+ "pl" to "Medytacja",
+ "pt-BR" to "Meditação",
+ "ro" to "Meditație",
+ "ru" to "Медитация",
+ "vi" to "Thiền",
+ "zh-CN" to "冥想",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Messaging",
name =
@@ -2026,6 +2436,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Mesagerie",
"ru" to "Сообщения",
"sc" to "Messagìstica",
+ "sk" to "Správy",
"sl" to "Sporočila",
"sq" to "Shkëmbim Mesazhesh",
"sr" to "Поруке",
@@ -2112,7 +2523,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Finanțe",
"ru" to "Финансы",
"sc" to "Dinare",
- "sk" to "Financie",
+ "sk" to "Peniaze",
"sl" to "Denar",
"sn" to "Mari",
"so" to "Lacag",
@@ -2234,17 +2645,19 @@ fun getCategoryItems(localeList: LocaleListCompat) =
id = "Music Practice Tool",
name =
mapOf(
+ "ar" to "أداة تدريب موسيقية",
"bg" to "Инструмент за музикална практика",
"ca" to "Eina de pràctica musical",
"cs" to "Nástroj pro cvičení hudby",
"de" to "Musikübungstool",
- "el" to "Εργαλείο Εξάσκησης Μουσικής",
+ "el" to "Μουσική εξάσκηση",
"en-GB" to "Music Practice Tool",
"en-US" to "Music Practice Tool",
"eo" to "Muzik-ekzerciloj",
"es" to "Práctica musical",
"et" to "Muusikatarvikud",
"fa" to "ابزار تمرین آهنگ",
+ "fi" to "Soitonharjoittelu",
"fr" to "Outil de pratique musicale",
"ga" to "Uirlis Cleachtaidh Ceoil",
"he" to "כלי תרגול מוזיקה",
@@ -2260,7 +2673,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Mūzikas vingrināšanās rīks",
"my" to "ဂီတလေ့ကျင့်ရေး ကိရိယာ",
"nb" to "Musikkøvingsverktøy",
- "nl" to "Hulpmiddelen voor muzikanten",
+ "nl" to "Hulpmiddel om muziek te oefenen",
"nl-BE" to "Muziek oefentool",
"pl" to "Narzędzie do ćwiczeń muzycznych",
"pt" to "Ferramenta de prática musical",
@@ -2268,9 +2681,10 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"pt-PT" to "Ferramenta de prática musical",
"ro" to "Instrumentul de practică muzicală",
"ru" to "Инструмент для музыкальной практики",
+ "sk" to "Nástroje na cvičenie hudby",
"sl" to "Orodje za vadbo glasbe",
"sq" to "Mjet Praktimi Muzike",
- "sv" to "Muskikövningsapp",
+ "sv" to "Musikövningsapp",
"sw" to "Zana ya Mazoezi ya Muziki",
"ta" to "இசை பயிற்சி கருவி",
"tr" to "Müzik Çalışma Aracı",
@@ -2383,13 +2797,14 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ca" to "Analitzador de xarxa",
"cs" to "Analýza sítě",
"de" to "Netzwerkanalyse",
- "el" to "Αναλυτής Δικτύου",
+ "el" to "Διάγνωση Δικτύου",
"en-GB" to "Network Analyser",
"en-US" to "Network Analyzer",
"eo" to "Analiziloj de retkonektoj",
"es" to "Diagnósticos de red",
"et" to "Võrguanalüüs",
"fa" to "تحلیلگر شبکه",
+ "fi" to "Verkkoanalysaattori",
"fr" to "Analyseur de réseau",
"ga" to "Anailíseoir Líonra",
"he" to "מאבחן רשת",
@@ -2397,6 +2812,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"hu" to "Hálózatelemző",
"id" to "Penganalisis Jaringan",
"is" to "Greining netkerfa",
+ "it" to "Analizzatore di rete",
"ja" to "ネットワークアナライザ",
"kn" to "ನೆಟ್ವರ್ಕ್ ವಿಶ್ಲೇಷಕ",
"ko" to "네트워크 분석기",
@@ -2406,9 +2822,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nl" to "Netwerkanalyse",
"nl-BE" to "Netwerk analysetool",
"pl" to "Analizator sieci",
+ "pt" to "Analisador de rede",
"pt-BR" to "Analisador de rede",
+ "pt-PT" to "Analisador de rede",
"ro" to "Analiză rețea",
"ru" to "Сетевой анализатор",
+ "sk" to "Analyzátor siete",
"sl" to "Pregledovalnik omrežja",
"sq" to "Analizues Rrjeti",
"sw" to "Kichanganuzi cha Mtandao",
@@ -2435,7 +2854,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"en-GB" to "News",
"en-US" to "News",
"eo" to "Legiloj de novaĵoj",
- "es" to "Noticias",
+ "es" to "Novedades",
"et" to "Uudised",
"fa" to "اخبار",
"fi" to "Uutiset",
@@ -2448,6 +2867,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Fréttir",
"it" to "Notizie",
"ja" to "ニュース",
+ "kab" to "Isallen",
"kn" to "ಸುದ್ದಿ",
"ko" to "뉴스",
"lb" to "Noriichten",
@@ -2464,6 +2884,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Știri",
"ru" to "Новости",
"sc" to "Novas",
+ "sk" to "Správy",
"sl" to "Novice",
"sq" to "Lajme",
"sr" to "Вести",
@@ -2496,6 +2917,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Notas",
"et" to "Märkmikud",
"fa" to "یادداشت",
+ "fi" to "Muistiinpano",
"fr" to "Note",
"ga" to "Nóta",
"he" to "הערה",
@@ -2505,6 +2927,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Minnispunktur",
"it" to "Appunti",
"ja" to "ノート",
+ "kab" to "Tazmilt",
"kn" to "ಗಮನಿಸಿ",
"ko" to "메모",
"lb" to "Notiz",
@@ -2521,6 +2944,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Notițe",
"ru" to "Заметки",
"sc" to "Nota",
+ "sk" to "Poznámky",
"sl" to "Beležka",
"sq" to "Shënim",
"sr" to "Белешка",
@@ -2546,7 +2970,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"cs" to "Přehrávač online médií",
"da" to "Online medieafspiller",
"de" to "Online-Mediaplayer",
- "el" to "Διαδικτυακός Αναπαραγωγέας Πολυμέσων",
+ "el" to "Διαδικτυακή Αναπαραγωγή Πολυμέσων",
"en-GB" to "Online Media Player",
"en-US" to "Online Media Player",
"eo" to "Ludiloj de foraj aŭdvidaĵoj",
@@ -2577,6 +3001,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Vizualizator media pe Internet",
"ru" to "Онлайн-медиаплееры",
"sc" to "Leghidore multimediale in lìnia",
+ "sk" to "Prehrávač online médií",
"sl" to "Predvajalnik spletnih vsebin",
"sq" to "Lojtës Mediash Në Internet",
"sr" to "Мрежни извођач садржаја",
@@ -2592,6 +3017,30 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Party Game",
+ name =
+ mapOf(
+ "ar" to "العاب الحفلة",
+ "cs" to "Párty hra",
+ "de" to "Partyspiel",
+ "en-GB" to "Party Game",
+ "en-US" to "Party Game",
+ "et" to "Peomängud",
+ "fa" to "بازی مهمانی",
+ "fr" to "Jeu de société",
+ "ga" to "Cluiche Cóisire",
+ "id" to "Game Pesta",
+ "ja" to "パーティゲーム",
+ "nl" to "Party spel",
+ "pl" to "Gra towarzyska",
+ "pt-BR" to "Jogo de festa",
+ "ro" to "Joc de petrecere",
+ "vi" to "Game party",
+ "zh-CN" to "派对游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Pass Wallet",
name =
@@ -2601,7 +3050,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ca" to "Cartera de paraules de pas",
"cs" to "Peněženka na lístky",
"de" to "Passcode-Wallet",
- "el" to "Πορτοφόλι Πάσου",
+ "el" to "Πορτοφόλι Εισιτηρίων",
"en-GB" to "Pass Wallet",
"en-US" to "Pass Wallet",
"eo" to "Biletujoj (por lojalec-kartoj)",
@@ -2632,10 +3081,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Portofel tichete",
"ru" to "Кошелек",
"sc" to "Portafòlliu de cartas e billetes",
+ "sk" to "Peňaženka na preukazy",
"sl" to "Denarnica kartic",
"sq" to "Kuletë Fjalëkalimesh",
"sr" to "Новчаник лозинки",
"sv" to "Passplånbok",
+ "sw" to "Pass Wallet",
"ta" to "பணப்பையை கடந்து செல்லுங்கள்",
"tr" to "Şifre Cüzdanı",
"ug" to "ئىم ھەميانى",
@@ -2674,13 +3125,14 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Lykilorð og 2FA",
"it" to "Password & Autenticazione a 2 Fattori",
"ja" to "パスワード&2要素認証",
+ "kab" to "Awal n uɛeddi & A2F",
"kn" to "ಪಾಸ್ವರ್ಡ್",
"ko" to "비밀번호 및 2단계 인증",
"lt" to "Slaptažodžiai ir 2 fakt. autentif.",
"lv" to "Paroles un divpakāpju autentificēšana",
"my" to "စကားဝှက်နှင့် အဆင့်ဆင့်လုံခြုံရေး",
"nb" to "Passord & 2FA",
- "nl" to "Wachtwoorden & 2FA",
+ "nl" to "Wachtwoorden & Tweestaps-Authenticatie (2FA)",
"nl-BE" to "Wachtwoord en 2FA",
"pl" to "Hasło i uwierzytelnianie dwuskładnikowe (2FA)",
"pt" to "Palavras-passe e dois fatores",
@@ -2689,10 +3141,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Parole & 2FA",
"ru" to "Пароли и авторизация",
"sc" to "Crae de intrada e autenticatzione a duos fatores",
+ "sk" to "Heslá a 2FA",
"sl" to "Gesla in dvostopenjsko preverjanje pristnosti",
"sq" to "Fjalëkalime & 2FA",
"sr" to "Лозинке и двострука пријава",
"sv" to "Lösenord och 2FA",
+ "sw" to "Nenosiri na 2FA",
"ta" to "கடவுச்சொல் & 2FA",
"tr" to "Şifre & 2FA",
"ug" to "ئىم ۋە ئىككى ئامىللىق دەلىللەش",
@@ -2730,7 +3184,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Llamadas de teléfono y SMS",
"es-AR" to "Teléfono y SMS",
"es-MX" to "Teléfono & SMS",
- "et" to "Telefon & SMS",
+ "et" to "Telefon ja SMS",
"eu" to "Telefonoa eta SMS",
"fa" to "تلفن و پیامک",
"fi" to "Puhelin ja tekstiviestit",
@@ -2798,6 +3252,29 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Platformer Game",
+ name =
+ mapOf(
+ "cs" to "Plošinová hra",
+ "de" to "Plattformspiel",
+ "en-GB" to "Platformer Game",
+ "en-US" to "Platformer Game",
+ "et" to "Platvormimängud",
+ "fa" to "بازی سکویی",
+ "fr" to "Jeu de plateforme",
+ "ga" to "Cluiche Ardáin",
+ "id" to "Game Platformer",
+ "ja" to "プラットフォーマーゲーム",
+ "nl" to "Platform spel",
+ "pl" to "Gra platformowa",
+ "pt-BR" to "Jogo de plataforma",
+ "ro" to "Joc platformer",
+ "vi" to "Game platformer",
+ "zh-CN" to "平台游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Podcast",
name =
@@ -2842,10 +3319,11 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Podcast",
"ru" to "Подкасты",
"sc" to "Podcast",
+ "sk" to "Podcast",
"sl" to "Podkast",
"sq" to "Podkast",
"sr" to "Довод",
- "sv" to "Pod",
+ "sv" to "Podd",
"sw" to "Podikasti",
"ta" to "போட்காச்ட்",
"tr" to "Podcast",
@@ -2874,7 +3352,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Transporte público",
"et" to "Ühistransport ja sõiduplaanid",
"fa" to "حملونقل عمومی",
- "fi" to "Joukkoliikenteen kartta ja aikataulu",
+ "fi" to "Joukkoliikenne",
"fr" to "Transports en commun",
"ga" to "Léarscáil & Amchlár Iompair Phoiblí",
"he" to "מפה ולו״ז תחבורה ציבורית",
@@ -2893,13 +3371,14 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"nb" to "Offentlig transport",
"nl" to "Openbaar vervoer",
"nl-BE" to "Openbaar vervoer",
- "pl" to "Mapa transportu publicznego i rozkład jazdy",
+ "pl" to "Transport publiczny",
"pt" to "Mapa de transporte público e horários",
"pt-BR" to "Mapa de transporte público e horários",
"pt-PT" to "Mapa de transporte público e horários",
"ro" to "Transport public & Orar",
"ru" to "Транспорт и расписания",
"sc" to "Oràrios e mapas de sos trasportos pùblicos",
+ "sk" to "Verejná doprava",
"sl" to "Javni prevoz",
"sq" to "Transport Publik",
"sr" to "Карта и ред вожње јавног превоза",
@@ -2910,11 +3389,74 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ug" to "ئاممىۋى قاتناش خەرىتىسى ۋە ۋاقىت جەدۋىلى",
"uk" to "Карта та розклад руху громадського транспорту",
"vi" to "Phương tiện giao thông",
- "zh-CN" to "公共交通地图和时间表",
+ "zh-CN" to "公共交通",
"zh-TW" to "大眾運輸",
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Puzzle Game",
+ name =
+ mapOf(
+ "ar" to "العاب الالغاز",
+ "cs" to "Logická hra",
+ "de" to "Puzzlespiel",
+ "en-GB" to "Puzzle Game",
+ "en-US" to "Puzzle Game",
+ "et" to "Mõistatusmängud",
+ "fa" to "بازی معمّایی",
+ "fr" to "Jeu de réflexion",
+ "ga" to "Cluiche Puzail",
+ "id" to "Game Puzzle",
+ "ja" to "パズルゲーム",
+ "nl" to "Puzzelspel",
+ "pl" to "Gra logiczna",
+ "pt-BR" to "Jogo de quebra-cabeça",
+ "ro" to "Jocuri de puzzle",
+ "vi" to "Game xếp hình",
+ "zh-CN" to "解谜游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Radio",
+ name =
+ mapOf(
+ "ar" to "مذياع",
+ "cs" to "Rádio",
+ "de" to "Radio",
+ "el" to "Ραδιόφωνο",
+ "en-GB" to "Radio",
+ "en-US" to "Radio",
+ "eo" to "Radio",
+ "es" to "Radio",
+ "et" to "Raadio",
+ "fa" to "رادیو",
+ "fi" to "Radio",
+ "fr" to "Radio",
+ "ga" to "Raidió",
+ "hr" to "Radio",
+ "hu" to "Rádió",
+ "id" to "Radio",
+ "is" to "Útvarp",
+ "ja" to "ラジオ",
+ "nb" to "Radio",
+ "nl" to "Radio",
+ "pl" to "Radio",
+ "pt" to "Rádio",
+ "pt-BR" to "Rádio",
+ "pt-PT" to "Rádio",
+ "ro" to "Radio",
+ "ru" to "Радио",
+ "sk" to "Rádio",
+ "sq" to "Radio",
+ "tr" to "Radyo",
+ "vi" to "Radio",
+ "zh-CN" to "收音机",
+ "zh-TW" to "收音機",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Reading",
name =
@@ -3021,7 +3563,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ca" to "Gestor de receptes",
"cs" to "Správce receptů",
"de" to "Rezeptmanager",
- "el" to "Διαχείριση συνταγών",
+ "el" to "Διαχείριση Συνταγών",
"en-GB" to "Recipe Manager",
"en-US" to "Recipe Manager",
"eo" to "Administriloj de kuir-receptoj",
@@ -3053,10 +3595,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Manager rețete",
"ru" to "Рецепты",
"sc" to "Manigiadore de retzetas",
+ "sk" to "Správca receptov",
"sl" to "Upravitelj receptov",
"sq" to "Përgjegjës Recetash Gatimi",
"sr" to "Управник упутства",
"sv" to "Recepthanterare",
+ "sw" to "Kidhibiti cha Mapishi",
"ta" to "செய்முறை மேலாளர்",
"tr" to "Tarif Yöneticisi",
"ug" to "رىسالە باشقۇرغۇچ",
@@ -3083,6 +3627,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Religión",
"et" to "Usk ja religioon",
"fa" to "مذهبی",
+ "fi" to "Uskonto",
"fr" to "Religion",
"ga" to "Reiligiún",
"he" to "דת",
@@ -3090,18 +3635,22 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"hu" to "Vallás",
"id" to "Agama",
"is" to "Trúarbrögð",
+ "it" to "Religione",
"ja" to "宗教",
"kn" to "ಧರ್ಮ",
"ko" to "종교",
"lv" to "Ticība",
"my" to "ဘာသာရေး",
"nb" to "Religion",
- "nl" to "Godsdienst",
+ "nl" to "Religie",
"nl-BE" to "Religie",
"pl" to "Religia",
+ "pt" to "Religião",
"pt-BR" to "Religião",
+ "pt-PT" to "Religião",
"ro" to "Religie",
"ru" to "Религия",
+ "sk" to "Náboženstvo",
"sl" to "Vera",
"sq" to "Fe",
"sv" to "Religion",
@@ -3114,6 +3663,137 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Role-Playing Game",
+ name =
+ mapOf(
+ "cs" to "Hra na hrdiny",
+ "de" to "Rollenspiel",
+ "en-GB" to "Role-playing Game",
+ "en-US" to "Role-playing Game",
+ "et" to "Rollimängud",
+ "fa" to "بازی نقشآفرینی",
+ "fr" to "Jeu de rôle",
+ "ga" to "Cluiche Ról-imirt",
+ "id" to "Game Bermain Peran",
+ "ja" to "ロールプレイングゲーム",
+ "nl" to "Rollenspel",
+ "pl" to "Gra fabularna",
+ "pt-BR" to "Jogo de RPG",
+ "ro" to "Joc de rol",
+ "vi" to "Game nhập vai",
+ "zh-CN" to "角色扮演游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Remote Access",
+ name =
+ mapOf(
+ "ar" to "الوصول عن بُعد",
+ "cs" to "Vzdálený přístup",
+ "de" to "Fernzugriff",
+ "el" to "Αποομακρυσμένη Πρόσβαση",
+ "en-GB" to "Remote Access",
+ "en-US" to "Remote Access",
+ "eo" to "Fora aliro",
+ "es" to "Acceso Remoto",
+ "et" to "Kaugligipääs",
+ "fa" to "دسترسی از راه دور",
+ "fi" to "Etäkäyttö",
+ "fr" to "Accès à distance",
+ "ga" to "Rochtain Chianda",
+ "hr" to "Udaljeni pristup",
+ "hu" to "Távoli elérés",
+ "id" to "Akses Jarak Jauh",
+ "is" to "Fjaraðgangur",
+ "ja" to "リモートアクセス",
+ "nb" to "Fjernkontroll",
+ "nl" to "Toegang op Afstand",
+ "pl" to "Zdalny dostęp",
+ "pt" to "Acesso remoto",
+ "pt-BR" to "Acesso remoto",
+ "pt-PT" to "Acesso remoto",
+ "ro" to "Acces la distanță",
+ "ru" to "Удаленный доступ",
+ "sk" to "Vzdialený prístup",
+ "tr" to "Uzaktan Erişim",
+ "vi" to "Truy cập từ xa",
+ "zh-CN" to "远程访问",
+ "zh-TW" to "遠端存取",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Remote Controller",
+ name =
+ mapOf(
+ "ar" to "متحكم بعيد",
+ "cs" to "Vzdálené ovládání",
+ "de" to "Fernbedienung",
+ "el" to "Τηλεχειριστήριο",
+ "en-GB" to "Remote Controller",
+ "en-US" to "Remote Controller",
+ "eo" to "Teleregiloj",
+ "es" to "Mando a Distancia",
+ "et" to "Kaugjuhtimispuldid",
+ "fa" to "واپایش از راه دور",
+ "fi" to "Kaukosäädin",
+ "fr" to "Télécommande",
+ "ga" to "Rialaitheoir Cianrialtáin",
+ "hr" to "Daljinski upravljač",
+ "hu" to "Távirányító",
+ "id" to "Alat Kontrol Jarak Jauh",
+ "is" to "Fjarstýring",
+ "ja" to "リモコン",
+ "nb" to "Fjernkontroll",
+ "nl" to "Afstandsbediening",
+ "pl" to "Pilot",
+ "pt" to "Controlo remoto",
+ "pt-BR" to "Controle remoto",
+ "pt-PT" to "Controlo remoto",
+ "ro" to "Control la distanță",
+ "ru" to "Пульт дистанционного управления",
+ "sk" to "Diaľkové ovládanie",
+ "tr" to "Uzaktan Kumanda",
+ "vi" to "Điều khiển từ xa",
+ "zh-CN" to "遥控器",
+ "zh-TW" to "遙控器",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Schedule",
+ name =
+ mapOf(
+ "ar" to "جدولة",
+ "cs" to "Plán",
+ "de" to "Terminplan",
+ "en-GB" to "Schedule",
+ "en-US" to "Schedule",
+ "eo" to "Horplanoj",
+ "et" to "Ürituste päevakavad",
+ "fa" to "زمانبندی",
+ "fr" to "Calendrier",
+ "ga" to "Sceideal",
+ "hr" to "Raspored termina",
+ "hu" to "Ütemezés",
+ "id" to "Jadwal",
+ "is" to "Vinnuáætlun",
+ "ja" to "スケジュール",
+ "nl" to "Rooster",
+ "pl" to "Harmonogram",
+ "pt" to "Agenda",
+ "pt-BR" to "Agenda",
+ "pt-PT" to "Agenda",
+ "ro" to "Planificare",
+ "sk" to "Plánovanie",
+ "tr" to "Planlama",
+ "vi" to "Lịch trình",
+ "zh-CN" to "计划",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Science & Education",
name =
@@ -3142,7 +3822,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Ciencia y Educación",
"es-AR" to "Ciencias y Educación",
"es-MX" to "Ciencia y Educación",
- "et" to "Teadus & haridus",
+ "et" to "Teadus ja haridus",
"eu" to "Zientzia eta hezkuntza",
"fa" to "علم و آموزش",
"fi" to "Tiede ja koulutus",
@@ -3187,7 +3867,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Știință & educație",
"ru" to "Наука и образование",
"sc" to "Iscièntzia e educatzione",
- "sk" to "Veda a výuka",
+ "sk" to "Veda a vzdelávanie",
"sl" to "Znanost & Izobrazba",
"sn" to "zveScience neFundo",
"so" to "Sayniska & Waxbarashada",
@@ -3307,6 +3987,53 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Shooter Game",
+ name =
+ mapOf(
+ "cs" to "Střílečka",
+ "de" to "Shooter",
+ "en-GB" to "Shooter Game",
+ "en-US" to "Shooter Game",
+ "et" to "Laskmismängud",
+ "fa" to "بازی شلیکی",
+ "fr" to "Jeu de tir",
+ "ga" to "Cluiche Lámhachóra",
+ "id" to "Game Tembak-menembak",
+ "ja" to "シューティングゲーム",
+ "nl" to "Schietspel",
+ "pl" to "Gra strzelanka",
+ "pt-BR" to "Jogo de tiro",
+ "ro" to "Joc cu împușcături",
+ "vi" to "Game bắn súng",
+ "zh-CN" to "射击游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Strategy Game",
+ name =
+ mapOf(
+ "ar" to "العاب ستراتيجية",
+ "cs" to "Strategická hra",
+ "de" to "Strategiespiel",
+ "en-GB" to "Strategy Game",
+ "en-US" to "Strategy Game",
+ "et" to "Strateegiamängud",
+ "fa" to "بازی استراتژی",
+ "fr" to "Jeu de stratégie",
+ "ga" to "Cluiche Straitéise",
+ "id" to "Game Strategi",
+ "ja" to "戦略ゲーム",
+ "nl" to "Strategiespel",
+ "pl" to "Gra strategiczna",
+ "pt-BR" to "Jogo de estratégia",
+ "ro" to "Joc de strategie",
+ "vi" to "Game chiến thuật",
+ "zh-CN" to "策略游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Shopping List",
name =
@@ -3349,6 +4076,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Listă cumpărături",
"ru" to "Список покупок",
"sc" to "Lista de còmporas",
+ "sk" to "Nákupný zoznam",
"sl" to "Nakupovalni seznam",
"sq" to "Listë Blerjesh",
"sr" to "Списак за куповину",
@@ -3397,7 +4125,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Sabiedriskais tīkls",
"my" to "လှုမှုကွန်ယက်",
"nb" to "Sosialt nettverk",
- "nl" to "Sociaal netwerk",
+ "nl" to "Sociaal Netwerk",
"nl-BE" to "Sociaalnetwerk",
"pl" to "Sieć społecznościowa",
"pt" to "Rede social",
@@ -3406,10 +4134,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Rețea socială",
"ru" to "Соцсети",
"sc" to "Retes sotziales",
+ "sk" to "Sociálne siete",
"sl" to "Družabna omrežja",
"sq" to "Rrjet Shoqëror",
"sr" to "Друштвена мрежа",
"sv" to "Socialt nätverk",
+ "sw" to "Mtandao wa Kijamii",
"ta" to "சமூக வலைப்பின்னல்",
"tr" to "Sosyal Ağ",
"ug" to "ئىجتىمائىي تور",
@@ -3420,6 +4150,30 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Sport Game",
+ name =
+ mapOf(
+ "ar" to "لعبة رياضة",
+ "cs" to "Sportovní hra",
+ "de" to "Sportspiel",
+ "en-GB" to "Sport Game",
+ "en-US" to "Sport Game",
+ "et" to "Spordimängud",
+ "fa" to "بازی ورزشی",
+ "fr" to "Jeu de sport",
+ "ga" to "Cluiche Spóirt",
+ "id" to "Game Olahraga",
+ "ja" to "スポーツゲーム",
+ "nl" to "Sportspel",
+ "pl" to "Gra sportowa",
+ "pt-BR" to "Jogo de esporte",
+ "ro" to "Joc sportiv",
+ "vi" to "Game thể thao",
+ "zh-CN" to "体育游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Sports & Health",
name =
@@ -3447,7 +4201,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Deportes y Salud",
"es-AR" to "Deportes y Salud",
"es-MX" to "Deportes & Salud",
- "et" to "Sport & tervis",
+ "et" to "Sport ja tervis",
"eu" to "Kirola eta osasuna",
"fa" to "ورزش و سلامت",
"fi" to "Urheilu ja terveys",
@@ -3491,7 +4245,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Sport & sănătate",
"ru" to "Спорт и здоровье",
"sc" to "Isport e Salude",
- "sk" to "Šport & Zdravie",
+ "sk" to "Šport a zdravie",
"sl" to "Šport & zdravje",
"sn" to "Nhabvu neUtano",
"so" to "Ciyaaraha & Caafimaadka",
@@ -3651,6 +4405,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Activitate",
"ru" to "Задачи",
"sc" to "Faina",
+ "sk" to "Úlohy",
"sl" to "Opravila",
"sq" to "Punë",
"sr" to "Задатак",
@@ -3693,6 +4448,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Textaritill",
"it" to "Editor di Testo",
"ja" to "テキストエディタ",
+ "kab" to "Amaẓrag n weḍris",
"kn" to "ಪಠ್ಯ ಸಂಪಾದಕ",
"ko" to "텍스트 편집기",
"lt" to "Tekstų redaktorius",
@@ -3708,10 +4464,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Editor text",
"ru" to "Текстовые редакторы",
"sc" to "Editore de testu",
+ "sk" to "Textový editor",
"sl" to "Urejevalnik besedila",
"sq" to "Përpunues Tekstesh",
"sr" to "Управник писања",
"sv" to "Textredigerare",
+ "sw" to "Mhariri wa maandishi",
"ta" to "உரை ஆசிரியர்",
"tr" to "Metin Düzenleyici",
"ug" to "تېكىست تەھرىرلىگۈچ",
@@ -3722,6 +4480,38 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Text to Speech",
+ name =
+ mapOf(
+ "ar" to "تحويل النص إلى كلام",
+ "cs" to "Text na řeč",
+ "de" to "Text-zu-Sprache",
+ "en-GB" to "Text to Speech",
+ "en-US" to "Text to Speech",
+ "eo" to "Parolsinteziloj",
+ "et" to "Kõnesüntees",
+ "fa" to "متن به گفتار",
+ "fr" to "Synthèse vocale",
+ "ga" to "Téacs go Caint",
+ "hr" to "Tekst u govor",
+ "hu" to "Szöveg beszéddé alakítása",
+ "id" to "Text to Speech",
+ "is" to "Texti-í-tal",
+ "ja" to "テキスト読み上げ",
+ "nl" to "Text naar Spraak",
+ "pl" to "Synteza mowy",
+ "pt" to "Texto para fala",
+ "pt-BR" to "Texto para fala",
+ "pt-PT" to "Texto para fala",
+ "ro" to "Conversie text în vorbire",
+ "sk" to "Prevod textu na reč",
+ "tr" to "Metinden Sese",
+ "vi" to "Chuyển văn bản thành giọng nói",
+ "zh-CN" to "文本转语音",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Theming",
name =
@@ -3760,7 +4550,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"gl" to "Aparencia",
"he" to "עיצוב",
"hi" to "विषय",
- "hr" to "Teme",
+ "hr" to "Izgled",
"hu" to "Stílus",
"hy" to "Ոճ",
"id" to "Personalisasi",
@@ -3907,6 +4697,60 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Time Tracker",
+ name =
+ mapOf(
+ "ar" to "متتبع الوقت",
+ "cs" to "Sledování času",
+ "de" to "Zeiterfassung",
+ "en-GB" to "Time Tracker",
+ "en-US" to "Time Tracker",
+ "eo" to "Temp-mezuriloj",
+ "et" to "Ajakasutuse jälgimine",
+ "fa" to "ردیاب زمان",
+ "fr" to "Compteur de temps",
+ "ga" to "Rianaitheoir Ama",
+ "hr" to "Praćenje vremena",
+ "hu" to "Időkövető",
+ "id" to "Pelacak Waktu",
+ "is" to "Tímastjórnun",
+ "ja" to "タイムトラッカー",
+ "nl" to "Tijd Bijhouden",
+ "pl" to "Miernik czasu",
+ "pt" to "Rastreador de tempo",
+ "pt-BR" to "Rastreador de tempo",
+ "pt-PT" to "Rastreador de tempo",
+ "ro" to "Urmărirea timpului",
+ "sk" to "Sledovanie času",
+ "tr" to "Zaman Takipçisi",
+ "vi" to "Theo dõi thời gian",
+ "zh-CN" to "时间追踪工具",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
+ CategoryItem(
+ id = "Timer",
+ name =
+ mapOf(
+ "ar" to "مُئَقِّت",
+ "cs" to "Časovač",
+ "de" to "Timer",
+ "en-GB" to "Timer",
+ "en-US" to "Timer",
+ "et" to "Taimerid",
+ "fa" to "زمانسنج",
+ "fr" to "Minuteur",
+ "ga" to "Amaireadóir",
+ "nl" to "Timer",
+ "pl" to "Minutnik",
+ "pt-BR" to "Cronômetro",
+ "ro" to "Temporizator",
+ "vi" to "Đếm thời gian",
+ "zh-CN" to "计时器",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Translation & Dictionary",
name =
@@ -3940,7 +4784,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Tulkošana un vārdnīca",
"my" to "ဘာသာပြန်နှင့် အဘိဓာန်",
"nb" to "Oversettelse & Ordbok",
- "nl" to "Vertalen & woordenboek",
+ "nl" to "Vertalen & Woordenboek",
"pl" to "Tłumaczenie i słownik",
"pt" to "Tradução e dicionário",
"pt-BR" to "Tradução e dicionário",
@@ -3948,6 +4792,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Traducere & dicționar",
"ru" to "Перевод и словари",
"sc" to "Tradutzione e ditzionàriu",
+ "sk" to "Preklad a slovník",
"sl" to "Prevajalec in slovar",
"sq" to "Përkthim & Fjalor",
"sr" to "Превод и речник",
@@ -3995,7 +4840,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Vienību pārveidotājs",
"my" to "ယူနစ် ပြောင်းလဲမှု",
"nb" to "Enhetskonvertering",
- "nl" to "Eenheden omrekenen",
+ "nl" to "Eenheden Omrekenen",
"pl" to "Konwerter jednostek",
"pt" to "Conversor de unidades",
"pt-BR" to "Conversor de unidades",
@@ -4003,6 +4848,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Convertor unități",
"ru" to "Перевод единиц",
"sc" to "Cunvertidore de unidades",
+ "sk" to "Prevodník jednotiek",
"sl" to "Pretvornik enot",
"sq" to "Shndërrues Njësish",
"sr" to "Претварач јединца",
@@ -4018,6 +4864,30 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Visual Novel",
+ name =
+ mapOf(
+ "ar" to "رواية صورية",
+ "cs" to "Vizuální román",
+ "de" to "Visual Novel",
+ "en-GB" to "Visual Novel",
+ "en-US" to "Visual Novel",
+ "et" to "Visuaalromaanid",
+ "fa" to "رمان تصویری",
+ "fr" to "Roman visuel",
+ "ga" to "Úrscéal Amhairc",
+ "id" to "Novel Visual",
+ "ja" to "ヴィジュアルノベル",
+ "nl" to "Visueel verhaal",
+ "pl" to "Powieść wizualna",
+ "pt-BR" to "Romance visual",
+ "ro" to "Romanul vizual",
+ "vi" to "Visual novel",
+ "zh-CN" to "视觉小说",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Voice & Video Chat",
name =
@@ -4027,7 +4897,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ca" to "Xat de veu i vídeo",
"cs" to "Hlasový a video chat",
"da" to "Stemme- og videochat",
- "de" to "Sprach-/Video-Chat",
+ "de" to "Sprach- / Video-Chat",
"el" to "Φωνή & Βιντεοκλήσεις",
"en-GB" to "Voice & Video Chat",
"en-US" to "Voice & Video Chat",
@@ -4050,7 +4920,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Balss un video tērzēšana",
"my" to "အသံနှင့် ဗီဒီယိုဖြင့် ပြောဆိုခြင်း",
"nb" to "Stemme- & Video-chat",
- "nl" to "Bellen & videobellen",
+ "nl" to "Bellen & Videobellen",
"pl" to "Czat głosowy i wideo",
"pt" to "Chat de voz e vídeo",
"pt-BR" to "Ligação de voz e vídeo",
@@ -4058,6 +4928,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Apeluri audio & Video",
"ru" to "Голосовые и видеочаты",
"sc" to "Tzarrada de boghe e vìdeu",
+ "sk" to "Hlasový a video chat",
"sl" to "Glasovni in slikovni klepet",
"sq" to "Fjalosje Me Zë & Video",
"sr" to "Ћаскање гласом и видеом",
@@ -4067,7 +4938,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"tr" to "Sesli ve Görüntülü Görüşme",
"ug" to "ئۈن ۋە سىن سۆھبەت",
"uk" to "Голосовий та відеочат",
- "vi" to "Trò chuyện thoại và video",
+ "vi" to "Gọi thoại và video",
"zh-CN" to "音视频聊天",
"zh-TW" to "語音與視像聊天",
)
@@ -4090,7 +4961,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"eo" to "VPN kaj retperiloj",
"es" to "Redes privadas (VPN) y proxies",
"es-MX" to "VPN & Proxy",
- "et" to "VPN ja proksiteenused",
+ "et" to "VPN-id ja proksiteenused",
"fa" to "پیشکار و ویپیان",
"fi" to "VPN ja välityspalvelin",
"fr" to "VPN & proxy",
@@ -4102,13 +4973,14 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "VPN og milliþjónn",
"it" to "VPN & Proxy",
"ja" to "VPN & プロキシ",
+ "kab" to "VPN & Upṛuksi",
"kn" to "VPN",
"ko" to "VPN 및 프록시",
"lt" to "VPN ir įgaliotieji serveriai",
"lv" to "VPN un starpniekserveris",
"my" to "VPN နှင့် Proxy",
"nb" to "VPN & Mellomserver",
- "nl" to "VPN & proxy",
+ "nl" to "VPN & Proxy",
"pl" to "VPN i proxy",
"pt" to "VPN e proxy",
"pt-BR" to "VPN e proxy",
@@ -4116,6 +4988,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "VPN & Proxy",
"ru" to "VPN и прокси",
"sc" to "VPN e serbidores intermediàrios",
+ "sk" to "VPN a proxy",
"sl" to "VPN in posredniški strežniki (proxy)",
"sq" to "VPN & Ndërmjetës",
"sr" to "Нестварна лична мрежа и посредник",
@@ -4157,6 +5030,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Veski",
"it" to "Portafoglio",
"ja" to "ウォレット",
+ "kab" to "Ṭṭezḍam",
"kn" to "ವಾಲೆಟ್",
"ko" to "지갑",
"lt" to "Piniginė",
@@ -4171,6 +5045,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Portofel",
"ru" to "Кошельки",
"sc" to "Taschinu",
+ "sk" to "Peňaženka",
"sl" to "Denarnica",
"sq" to "Kuletë",
"sr" to "Новчаник",
@@ -4227,6 +5102,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Fundal",
"ru" to "Обои",
"sc" to "Isfundu",
+ "sk" to "Tapety",
"sl" to "Ozadja",
"sq" to "Sfond",
"sr" to "Позадинска слика",
@@ -4270,6 +5146,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"is" to "Veður",
"it" to "Meteo",
"ja" to "天気",
+ "kab" to "Tignewt",
"kn" to "ಹವಾಮಾನ",
"ko" to "날씨",
"lt" to "Orai",
@@ -4284,10 +5161,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Meteo",
"ru" to "Погода",
"sc" to "Mèteu",
+ "sk" to "Počasie",
"sl" to "Vreme",
"sq" to "Mot",
"sr" to "Временске прилике",
"sv" to "Väder",
+ "sw" to "Hali ya hewa",
"ta" to "வானிலை",
"tr" to "Hava Durumu",
"ug" to "ھاۋا رايى",
@@ -4298,6 +5177,30 @@ fun getCategoryItems(localeList: LocaleListCompat) =
)
.getBestLocale(localeList) ?: "Unknown Category",
),
+ CategoryItem(
+ id = "Word Game",
+ name =
+ mapOf(
+ "ar" to "لعبة الغاز كتابية",
+ "cs" to "Slovní hra",
+ "de" to "Wortspiel",
+ "en-GB" to "Word Game",
+ "en-US" to "Word Game",
+ "et" to "Sõnamängud",
+ "fa" to "بازی واژهای",
+ "fr" to "Jeu de lettres",
+ "ga" to "Cluiche Focal",
+ "id" to "Game Kata",
+ "ja" to "言葉遊び",
+ "nl" to "Woordspel",
+ "pl" to "Gra słowna",
+ "pt-BR" to "Jogo de palavras",
+ "ro" to "Joc de cuvinte",
+ "vi" to "Game chữ",
+ "zh-CN" to "单词游戏",
+ )
+ .getBestLocale(localeList) ?: "Unknown Category",
+ ),
CategoryItem(
id = "Workout",
name =
@@ -4315,7 +5218,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"es" to "Ejercicio físico",
"et" to "Treening",
"fa" to "ورزش",
- "fi" to "Harjoitus",
+ "fi" to "Kuntoilu",
"fr" to "Entraînement",
"ga" to "Babhta traenála",
"he" to "כושר",
@@ -4331,7 +5234,7 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"lv" to "Trenēšanās",
"my" to "လေ့ကျင့်ခန်း",
"nb" to "Trening",
- "nl" to "Trainen & work-out",
+ "nl" to "Work-Out",
"pl" to "Trening",
"pt" to "Exercício físico",
"pt-BR" to "Exercício físico",
@@ -4339,10 +5242,12 @@ fun getCategoryItems(localeList: LocaleListCompat) =
"ro" to "Antrenament",
"ru" to "Тренировки",
"sc" to "Allenamentu",
+ "sk" to "Cvičenie",
"sl" to "Telovadba",
"sq" to "Stërvitje",
"sr" to "Вежбање",
"sv" to "Träningspass",
+ "sw" to "Mazoezi",
"ta" to "பயிற்சி",
"tr" to "Antrenman",
"ug" to "چېنىقىش",
diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/DetailsItem.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/DetailsItem.kt
index 2a0434836..076e2e141 100644
--- a/app/src/androidTest/java/org/fdroid/ui/screenshots/DetailsItem.kt
+++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/DetailsItem.kt
@@ -7,35 +7,42 @@ val appMetadata =
repoId = 1L,
packageName = "org.fdroid.basic",
added = 1680776466000L,
- lastUpdated = 1772364577000L,
+ lastUpdated = 1777704496684L,
name =
mapOf(
"en-GB" to "F-Droid Basic",
"es" to "F-Droid Basic",
- "pl" to "F-Droid Podstawowy",
+ "nb" to "F-Droid Basic",
+ "pl" to "F-Droid Basic",
"pt" to "F-Droid Básico",
"pt-PT" to "F-Droid Básico",
"ru" to "F-Droid Basic",
+ "sv" to "F-Droid Basic",
"ar" to "F-Droid Basic",
+ "be" to "F-Droid Basic",
"ca" to "F-Droid bàsic",
"cs" to "F-Droid Basic",
"de" to "F-Droid Basic",
"en-US" to "F-Droid Basic",
"eo" to "F-Droid Baza",
"et" to "F-Droid Basic",
- "fa-IR" to "افدروید پایهای",
+ "fa" to "افدروید پایهای",
"fr" to "F-Droid Basic",
"ga" to "F-Droid Bunúsach",
"he" to "F-Droid בסיסי",
- "hu-HU" to "Alap F-Droid",
- "is-IS" to "Grunnútgáfa F-Droid",
+ "hr" to "F-Droid Basic",
+ "hu" to "Alap F-Droid",
+ "id" to "F-Droid Basic",
+ "is" to "Grunnútgáfa F-Droid",
+ "it" to "F-Droid Basic",
"ja" to "F-Droid Basic",
+ "nl" to "F-Droid Basis",
"pt-BR" to "F-Droid Básico",
"ro" to "F-Droid Basic",
"sr" to "Основни F-Droid",
- "sv-SE" to "F-Droid Basic",
- "ta-IN" to "எஃப்-டிராய்டு அடிப்படை",
- "tr" to "F-Droid Temel",
+ "sw" to "F-Droid Basic",
+ "ta" to "எஃப்-டிராய்டு அடிப்படை",
+ "tr" to "F-Droid Basic",
"uk" to "F-Droid Basic",
"zh-CN" to "F-Droid Basic",
"zh-TW" to "F-Droid Basic",
@@ -44,38 +51,42 @@ val appMetadata =
mapOf(
"en-GB" to "The minimal client app for the app store that respects freedom and privacy",
"es" to "Aplicación para la tienda de aplicaciones; respeta la libertad y la privacidad",
+ "nb" to "Den minimale klient-appen for app-butikken som respekterer frihet og personvern",
"pl" to "Minimalistyczna apka kliencka dla sklepu z apkami szanująca wolność i prywatność",
"pt" to "App cliente mínima para a loja de apps que respeita a liberdade e a privacidade",
"pt-PT" to
"App cliente mínima para a loja de apps que respeita a liberdade e a privacidade",
"ru" to "Минималистичный магазин приложений, уважающий свободу и приватность",
- "ar" to "تطبيق عميل خفيف لمتجر التطبيقات والذي يحترم الحرية والخصوصية",
+ "sv" to "Den minimala klient-appen för app-butiken som respekterar frihet och privatliv",
+ "ar" to "تطبيق عميل بسيط لمتجر التطبيقات الذي يحترم الحرية والخصوصية",
+ "be" to "Мінімальная кліентская праграма што паважае вашыя свабоду і прыватнасць",
"ca" to "El client mínim del centre d'aplicacions que respecta la llibertat i privadesa",
"cs" to "Minimální klient pro obchod s aplikacemi, který respektuje svobodu a soukromí",
"de" to "Der minimale Client für den App-Store, der Freiheit und Privatsphäre respektiert",
"en-US" to "The minimal client app for the app store that respects freedom and privacy",
"eo" to "Minimuma kliento por la aplikaĵa vendejo respektanta liberecon kaj privatecon",
"et" to "Lihtsustatud variant F-Droidi rakendustepoe kliendist",
- "fa-IR" to "کارخواه کمینه برای فروشگاه کارهای که به آزادی و محرمانگی احترام میگذارد",
+ "fa" to "کارخواه کمینه برای فروشگاه کارهای که به آزادی و محرمانگی احترام میگذارد",
"fr" to "L’application minimale pour le magasin d’applis respectueux de la vie privée",
"ga" to "Aip íosta cliant don siopa a léiríonn meas ar shaoirse agus ar phríobháideachas",
"he" to "יישומון הלקוח המזערי לחנות יישומונים שמכבד חופש ופרטיות",
- "hu-HU" to "Az alap app a tárolóban, amely tiszteli a szabadságot és a magánéletet",
- "is-IS" to
- "Lágmarksviðmót forritsins fyrir hugbúnaðarsafnið sem virðir frelsi og gagnaleynd",
+ "hr" to "Minimalalna aplikacija za trgovinu aplikacija koja poštuje slobodu i privatnost",
+ "hu" to "Az alap app a tárolóban, amely tiszteli a szabadságot és a magánéletet",
+ "id" to "Aplikasi klien minimal untuk toko aplikasi yang menghormati kebebasan dan privas",
+ "is" to "Lágmarksviðmót forritsins fyrir hugbúnaðarsafnið sem virðir frelsi og gagnaleynd",
+ "it" to "Il client minimale per l'app store che rispetta la libertà e la privacy",
"ja" to "自由とプライバシーを尊重するアプリストアの簡略版クライアント",
+ "nl" to "De minimale clientapp van de appwinkel die vrijheid en privacy respecteert",
"pt-BR" to
- "App cliente mínimo para a loja de apps que respeita a liberdade e a privacidade",
+ "O app minimalista para a loja de aplicativos que respeita a liberdade e a privac",
"ro" to "Client minim pentru magazinul de aplicații ce respectă libertatea și intimitatea",
"sr" to "Клијент апликација за продавницу апликација која поштује слободу и приватност",
- "sv-SE" to "Den minimala klient-appen för app-butiken som respekterar frihet och privatliv",
"sw" to "An app cliant íosta don siopa app a urramaíonn saoirse agus príobháideachta",
- "ta-IN" to
- "விடுதலை மற்றும் தனியுரிமையை மதிக்கும் பயன்பாட்டு கடைக்கான குறைந்தபட்ச வாங்கி பயன",
- "tr" to "Özgürlük ve gizliliğe saygılı uygulama mağazası için sade bir istemci uygulaması",
+ "ta" to "விடுதலை மற்றும் தனியுரிமையை மதிக்கும் பயன்பாட்டு கடைக்கான குறைந்தபட்ச வாங்கி பயன",
+ "tr" to "Özgürlüğe ve gizliliğe saygılı uygulama mağazası için en sade istemci uygulaması",
"uk" to "Мінімалістичний клієнтський застосунок, який поважає свободу й приватність",
"zh-CN" to "尊重自由和隐私的应用商店的极简客户端应用",
- "zh-TW" to "尊重自由和隱私的應用商店最小客戶端應用程序",
+ "zh-TW" to "尊重自由和隱私的應用商店最小用戶端應用程序",
),
description =
mapOf(
@@ -91,8 +102,12 @@ val appMetadata =
"AVISO: A versão básica do cliente F-Droid tem menos funções (por exemplo, não tem o \"Por perto\" ou a função de pânico). Está preparado para o Android 13 e pode fazer atualizações sem intervenção do utilizador, sem precisar da extensão privilegiada ou de ",
"ru" to
"ПРИМЕЧАНИЕ: Basic-версия клиента F-Droid имеет урезанный набор функций (например, нет общего доступа к близлежащим ресурсам и функции паники). Она нацелена на Android 13 и может выполнять обновления без участия пользователя и без особого расширения или roo",
+ "sv" to
+ "OBS: Basic-versionen av F-Droid Client har en reducerad funktionsuppsättning (t.ex. ingen när-delning och ingen panikfunktion). Den riktar sig till Android 13 och kan göra obevakade uppdateringar utan privilegierad förlängning eller root.\n\nF-Droid är en ",
"ar" to
- "ملاحظة: الإصدار الأساسي من عميل F-Droid يحتوي على مجموعة ميزات مخفضة (على سبيل المثال، لا توجد مشاركة قريبة ولا توجد ميزة الذعر). وهو يستهدف Android 13 ويمكنه إجراء تحديثات غير مراقبة بدون امتداد أو جذر مميز.\n\nF-Droid عبارة عن كتالوج قابل للتثبيت لتطبيقا",
+ "ملاحظة: النسخة الأساسية من عميل F-Droid تأتي بميزات محدودة (على سبيل المثال، لا تتوفر ميزة المشاركة القريبة أو ميزة الطوارئ). تستهدف هذه النسخة نظام أندرويد 13، ويمكنها إجراء تحديثات تلقائية دون الحاجة إلى صلاحيات إضافية أو الجذر.\n\nF-Droid هو دليل قابل ل",
+ "be" to
+ "Увага: Базавая версія Кліента F-Droid мае зменшаны набор функцый (напр. няма функцый абагульвання і панікі). Ён нацэлены на Android 13 і можа выконваць аўтаматычныя абнаўленні без пашыраных прывіляў альбо root.\n\nF-Droid гэта каталог для ўсталёўкі Вольнаг",
"ca" to
"NOTA: La versió bàsica del client de l'F-Droid té un conjunt de funcionalitats reduït (p. ex. no hi ha compartició propera i funció del pànic). Està orientada a l'Android 13 i pot fer actualitzacions desateses sense extensions de privilegis ni permisos d'a",
"cs" to
@@ -105,7 +120,7 @@ val appMetadata =
"RIMARKO: la baza versio de kliento F-Droid estas senigita de kelkaj ebloj (kiel proksima interŝanĝo kaj panika butono). Ĝi celas Android 13 kaj povas ĝisdatigi aplikaĵojn fone sen uzi ĉefuzantan permeson aŭ la privilegian aldonaĵon.\n\nF-Droid estas instal",
"et" to
"NB! F-Droid Basic on piiratud funktsionaalsusega klient (nt. puuduvad naabritega jagamine ja paanikarežiimi keelamine). Ta on mõeldud Android 13-le ja saab paigaldada uuendusi iseseisvalt, priviligeeritud laienduse või juurkasutajata.\n\nF-Droid on paigald",
- "fa-IR" to
+ "fa" to
"نکته: نگارش پایهای کارخواه اف دروید مجموعه ویز؛یهای کمتری دارد (برای نمونه همرسانی نزدیک و دکمهٔ هراس). اندروید ۱۳ را هدف قرار داده و میتواند بهروز رسانیهای پسزمینه را بدون ریشه یا افزونهٔ ممتاز انجام دهد.\n\nافدروید کالانمایی قابل نصب از کارههای",
"fr" to
"REMARQUE : la version Basic du client F-Droid a un jeu de fonctionnalités réduit (par ex. pas de partage de proximité et aucune fonction \"no panic\"). Elle est destinée à la plateforme Android 13 et peut faire des mises à jour sans extension privilégiée o",
@@ -113,30 +128,32 @@ val appMetadata =
"NÓTA: Tá sraith gnéithe laghdaithe ag an leagan Bunúsach de Chliant F-Droid (e.g. gan aon chomhroinnt in aice láimhe agus gan aon ghné scaoill). Díríonn sé ar Android 13 agus is féidir leis nuashonruithe gan duine a dhéanamh gan síneadh pribhléideach ná fr",
"he" to
"הערה: לגרסה הבסיסית של לקוח F-Droid יש מערכת תכונה מופחתת (לדוגמה, אין שיתוף סמוך ואין תכונה פאניקה). הוא מכוון לאנדרואיד 13 ויכול לעשות עדכונים ללא הרחבה מועדפת או root.\n\nF-Droid הוא קטלוג של יישומי תוכנה חופשית עבור אנדרואיד. יישומון לקוח F-Droid מקל ל",
- "hu-HU" to
+ "hu" to
"MEGJEGYZÉS: Az alap F-Droid verzió csökkentett funkciókészlettel rendelkezik (pl. nincs közeli megosztás és nincs pánik funkció). Az android 13-as rendszert célozza meg, és képes felügyelet nélküli frissítéseket végezni jogosultsági kiterjesztés vagy root ",
- "is-IS" to
+ "id" to
+ "CATATAN: Versi Basic dari Klien F-Droid memiliki fitur yang lebih terbatas (misalnya, tidak ada berbagi terdekat dan tidak ada fitur panik). Versi ini menargetkan Android 13 dan dapat melakukan pembaruan otomatis tanpa ekstensi istimewa atau akses root.\n",
+ "is" to
"ATHUGAÐU: Grunnútgáfa (Basic) F-Droid forritsins er með færri eiginleika (t.d. ekki deilingu til tækja í nágrenninu og enga neyðaraðgerð). Hún miðast við Android 13 og getur séð um uppfærslur án afskipta notandans án forgangsviðbótarinnar eða rótaraðgangs.",
"ja" to
"注意:F-DroidクライアントのBasic版は、機能が制限されています(例えば、近くへ共有やパニック機能がありません)。Android 13がターゲットで、特権の拡張やroot無しで更新出来ます。\n\nF-DroidはAndroidのための自由かつインストール可能なソフトウェアアプリの目録です。F-Droidクライアントアプリは、あなたのデバイス上での閲覧、インストール、更新の追跡を簡単にします。\n\nF-Droidは互換性のあるリポジトリへ接続します。標準のリポジトリはf-droid.orgでホスト",
+ "nl" to
+ "NOTITIE: De basisversie van de F-Droid-client heeft een gereduceerd aantal functies (bvb geen dichtbij delen en geen paniekfunctie). Het is gericht op Android 13 en kan zelfstandig updates uitvoeren zonder bevoorrechtte extensie of root.\n\nF-Droid is een ",
"pt-BR" to
"AVISO: A versão básica do cliente F-Droid tem um número reduzido de funções (por exemplo, não tem o \"Por perto\" ou a função de Pánico). Ela \"atinge\" o Android 13 e pode fazer atualizações sem intervenção do usuário, sem precisar da extensão privilegiad",
"ro" to
"NOTĂ: Versiunea de client F-Droid Basic are un set redus de funcții (de exemplu, fără partajare în apropiere și fără funcție de panică). Aceasta vizează Android 13 și poate face actualizări nesupravegheate fără extensia privilegiată sau root.\n\nF-Droid es",
"sr" to
"Напомена: Основна верзија клијента F-Droid има ограничен сет функција (нпр. нема опцију за дељење у близини и нема функцију за хитне случајеве). Она је намењена за Android 13 и може да обавља ажурирања без повишених привилегија или рута.\n\nF-Droid је ката",
- "sv-SE" to
- "OBS: Basic-versionen av F-Droid Client har en reducerad funktionsuppsättning (t.ex. ingen när-delning och ingen panikfunktion). Den riktar sig till Android 13 och kan göra obevakade uppdateringar utan privilegierad förlängning eller root.\n\nF-Droid är en ",
- "ta-IN" to
+ "ta" to
"குறிப்பு: எஃப்-டிராய்டு கிளையண்டின் அடிப்படை பதிப்பு குறைக்கப்பட்ட அம்சத் தொகுப்பைக் கொண்டுள்ளது (எ.கா. அருகிலுள்ள பங்கு இல்லை மற்றும் அச்சம் நற்பொருத்தம் இல்லை). இது ஆண்ட்ராய்டு 13 ஐ குறிவைக்கிறது மற்றும் சலுகை பெற்ற நீட்டிப்பு அல்லது வேர் இல்லாமல் கவனிக்",
"tr" to
- "NOT: F-Droid İstemcisinin Temel sürümünün düşürülmüş özellikleri mevcuttur (mesela nearby share ve panik özelliği yok). Android 13'ü hedef alır ve özel eklenti veya root gerektirmeden gözetimsiz güncellemeler yapabilir.\nF-Droid, Android için özgür yazılım",
+ "NOT: F-Droid İstemcisinin Basic (Temel) sürümü daraltılmış bir özellik setine sahiptir (örneğin yakındakilerle paylaşım ve panik özelliği yoktur). Android 13'ü hedefler ve ayrıcalıklı bir eklentiye veya root erişimine ihtiyaç duymadan katılımsız (arka plan",
"uk" to
"ПРИМІТКА. Базова версія F-Droid Client має скорочений набір функцій (наприклад, немає функції спільного доступу поблизу та функції паніки). Він націлений на Android 13 і може виконувати автоматичні оновлення без привілейованого розширення або root-права. ",
"zh-CN" to
"注意:F-Droid 客户端基础版本的功能不全 (如没有“附近”分享和应急功能)。它针对 Android 13 ,可以在没有特权扩展或 root 的情况下执行无人值守更新。.\n\nF-Droid 是 Android 系统自由软件应用的可安装目录。 F-Droid 客户端应用使得浏览、安装和跟踪设备上的应用更新变得轻而易举。\n\n它可以连接到任何兼容 F-Droid 的存储库。默认存储库托管在 f-droid.org 上,其中只包含真正的自由软件。\n\nAndroid 本身是开放的,意思是你可以从任何地",
"zh-TW" to
- "注意:F-Droid 用戶端基本版的功能集較少(例如,沒有附近共享和恐慌功能)。它以 Android 13 為目標,無需特權擴充或 root 即可進行無人值守更新。\n\nF-Droid 是適用於 Android 的自由軟體應用程式的可安裝目錄。 F-Droid 用戶端應用程式可以輕鬆瀏覽、安裝和追蹤裝置上的更新。\n\n它可以連接到任何與 F-Droid 相容的儲存庫。預設 repo 託管在 f-droid.org,其中僅包含真正的自由軟體。\n\nAndroid 本身是開放的,這意味著您可以自由地從任何",
+ "注意:F-Droid 用戶端基本版的功能集較少(例如,沒有附近共享和恐慌功能)。它以 Android 13 為目標,無需特權擴充或 root 即可進行無人值守更新。\n\nF-Droid 是適用於 Android 的自由軟體應用程式的可安裝目錄。 F-Droid 用戶端應用程式可以輕鬆瀏覽、安裝和追蹤裝置上的更新。\n\n它可以連線到任何與 F-Droid 相容的軟體庫。預設 repo 託管在 f-droid.org,其中僅包含真正的自由軟體。\n\nAndroid 本身是開放的,這意味著您可以自由地從任何",
),
webSite = "https://f-droid.org",
changelog = "https://gitlab.com/fdroid/fdroidclient/-/blob/HEAD/CHANGELOG.md",
diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverItems.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverItems.kt
index ac9c91d11..82f44fc90 100644
--- a/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverItems.kt
+++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverItems.kt
@@ -6,6 +6,59 @@ import org.fdroid.ui.discover.AppDiscoverItem
fun getNewApps(localeList: LocaleListCompat) =
listOf(
+ AppDiscoverItem(
+ packageName = "org.fairscan.app",
+ name =
+ mapOf("en-US" to "FairScan – PDF Scanner", "ru" to "FairScan – Сканер в PDF")
+ .getBestLocale(localeList) ?: "Unknown App",
+ isInstalled = true,
+ imageModel =
+ "https://f-droid.org/repo/org.fairscan.app/en-US/icon_jXreg-23j73Q35sj5mioXnOD8m6Y9V6kLjX037gxq00=.png",
+ ),
+ AppDiscoverItem(
+ packageName = "com.github.catfriend1.syncthingfork",
+ name =
+ mapOf(
+ "en-US" to "Syncthing-Fork",
+ "vi" to "Syncthing-Fork",
+ "he" to "Syncthing-Fork",
+ "pt-PT" to "Syncthing-Fork",
+ "zh-CN" to "Syncthing-Fork",
+ "ru-RU" to "Syncthing-Fork",
+ "es-419" to "Syncthing-Fork",
+ "fr-FR" to "Syncthing-Fork",
+ "es-ES" to "Syncthing-Fork",
+ "be" to "Syncthing-Fork",
+ "tr-TR" to "Syncthing-Fork",
+ "ro" to "Syncthing-Fork",
+ "ca" to "Syncthing-Fork",
+ "it-IT" to "Syncthing-Fork",
+ "uk" to "Syncthing-Fork",
+ "zh-TW" to "Syncthing-Fork",
+ "bg" to "Syncthing-Fork",
+ "cs-CZ" to "Syncthing-Fork",
+ "nl-NL" to "Syncthing-Fork",
+ "de-DE" to "Syncthing-Fork",
+ "ja-JP" to "Syncthing-Fork",
+ "da-DK" to "Syncthing-Fork",
+ "ko-KR" to "Syncthing-Fork",
+ "id" to "Syncthing-Fork",
+ "sr" to "Syncthing-Fork",
+ "hu-HU" to "Syncthing-Fork",
+ "sv-SE" to "Syncthing-Fork",
+ "et" to "Syncthing-Fork",
+ "pl-PL" to "Syncthing-Fork",
+ "pt-BR" to "Syncthing-Fork",
+ "ar" to "Syncthing-Fork",
+ "sk" to "Syncthing-Fork",
+ "es-US" to "Syncthing-Fork",
+ "fi-FI" to "Syncthing-Fork",
+ )
+ .getBestLocale(localeList) ?: "Unknown App",
+ isInstalled = false,
+ imageModel =
+ "https://f-droid.org/repo/com.github.catfriend1.syncthingfork/en-US/icon_egWOwTWZpQ_1CxmdrwP5wRta5EVmCCuzQWxjI5JTs0w=.png",
+ ),
AppDiscoverItem(
packageName = "jp.nonbili.noutube",
name = mapOf("en-US" to "NouTube").getBestLocale(localeList) ?: "Unknown App",
@@ -13,6 +66,64 @@ fun getNewApps(localeList: LocaleListCompat) =
imageModel =
"https://f-droid.org/repo/jp.nonbili.noutube/en-US/icon_A1nC0BkAzsvNhws7y1thjfhF8LJJReXVUORApHsstDE=.png",
),
+ AppDiscoverItem(
+ packageName = "app.comaps.fdroid",
+ name =
+ mapOf(
+ "pt-PT" to "CoMaps - Mapas e Navegação - Offline e Privada",
+ "zh-CN" to "CoMaps — 隐私保护下的离线远足、骑行、驾驶",
+ "lv" to "CoMaps - dodies ceļā bezsaistē ar privātumu",
+ "hr" to "CoMaps - Pješačite, biciklirajte, vozite Offline i",
+ "sl" to "CoMaps – potujte brez povezave, zasebno",
+ "ru-RU" to "CoMaps - Карты и путешествия с приватностью",
+ "fr-FR" to "CoMaps - Randonnée, Vélo, Conduite hors ligne",
+ "es-ES" to "CoMaps - Senderismo, ciclismo y conducción offline",
+ "lt" to "CoMaps – keliaukite atsijungę ir privačiai",
+ "en-US" to "CoMaps - Hike, Bike, Drive Offline with Privacy",
+ "tr-TR" to "CoMaps - Gizlilikle Yürüyün, Sürün, Gezin",
+ "ro" to "CoMaps - călătorește offline cu confidențialitate",
+ "bn-BD" to "কোম্যাপস - অফলাইনে হাইকিং, সাইকেলিং ও ড্রাইভিং",
+ "ca" to "CoMaps - Camina, pedala o condueix privadament",
+ "it-IT" to "CoMaps - Navigazione Offline con Privacy",
+ "uk" to "CoMaps — ходьба та поїздки офлайн із приватність",
+ "zh-TW" to "CoMaps — 隱私保護下的離線遠足、騎行、駕駛",
+ "gl-ES" to "CoMaps - Aplicación de mapas privada, sen conexión",
+ "bg" to "CoMaps - Хайкинг, Велосипед, Пътуване без Интернет",
+ "cs-CZ" to "CoMaps – pěšky, na kole a autem offline a soukromě",
+ "nl-NL" to "CoMaps - Wandel, fiets, rijd offline met privacy",
+ "de-DE" to "CoMaps – Offline navigieren mit Datenschutz",
+ "da-DK" to "CoMaps - vandr, cykl og kør offline med privatliv",
+ "id" to "CoMaps- Navigasi Offline dengan Privasi",
+ "sr" to "CoMaps - пешачење, бицикл и вожња, са приватношћу",
+ "el-GR" to "CoMaps - Πλοήγηση εκτός σύνδεσης με ιδιωτικότητα",
+ "eu-ES" to "CoMaps - mapak offline eta dena pribatuan",
+ "hu-HU" to "CoMaps – túrázás, kerékpározás, autózás offline",
+ "no-NO" to "CoMaps - Gå, Sykle, Kjør Uten Internett",
+ "sv-SE" to "Comaps- Vandra, Cykla, Kör Offline, Privat",
+ "et" to "CoMaps - sinu privaatne kaart",
+ "pl-PL" to "CoMaps - Nawiguj offline z zachowaniem prywatności",
+ "kn-IN" to "ಸಹ ನಕ್ಷೆಗಳು - ಆಫ್ಲೈನ್ ಮತ್ತು ಗೌಪ್ಯತೆಯೊಂದಿಗೆ",
+ "pt-BR" to "CoMaps - Mapas e Navegação Offline com Privacidade",
+ "ar" to "CoMaps - تنزه وتنقل وقُد دون إنترنت مع الخصوصية",
+ "sk" to "CoMaps - kráčaj, bicykluj a jazdi v súkromí",
+ "fi-FI" to "CoMaps - Navigoi yksityisesti ilman verkkoyhteyttä",
+ "kw" to "CoMaps - Viajya gans Privetter",
+ "fa-IR" to "CoMaps - کوهنوردی، دوچرخهسواری و رانندگی آفلاین",
+ "pt" to "CoMaps - Mapas e Navegação - Offline e Privada",
+ "ta-IN" to "இணைவரைபடங்கள் - மலையேறு, வண்டி, தனிமையில் இயக்கு",
+ "en-GB" to "CoMaps - Hike, Bike, Drive Offline with Privacy",
+ "mt" to "CoMaps - Imxi, Suq Rota u Karozza bil-Privatezza",
+ "bn" to "CoMaps - অফলাইনে হাইক, বাইক, ড্রাইভ করুন",
+ "brh" to "CoMaps - آفلائن مش لگ، سائکل سواری و موٹر سواری",
+ "en-CA" to "CoMaps - Hike, Bike, Drive Offline with Privacy",
+ "eo" to "CoMaps - Migru, Biciklu, Veturigu Eksterrete",
+ "ast" to "CoMaps - Mapas ensin conexón con privacidá",
+ )
+ .getBestLocale(localeList) ?: "Unknown App",
+ isInstalled = true,
+ imageModel =
+ "https://f-droid.org/repo/app.comaps.fdroid/en-US/icon_Xqa9hlGluoI0ME9XbHrLOaAJ_dX9wWMtDhLgpFYbUOI=.png",
+ ),
AppDiscoverItem(
packageName = "net.thunderbird.android",
name =
@@ -20,7 +131,7 @@ fun getNewApps(localeList: LocaleListCompat) =
"ar" to "ثَندَربِرْد: حرّر صندوق بريدك",
"be" to "Thunderbird: Вольная Пошта",
"bg" to "Thunderbird: Освободете пощата",
- "br" to "Thunderbird: Dieubit hor boest degemer",
+ "br" to "Thunderbird: Dieubit ho poest degemer",
"ca" to "Thunderbird: allibereu la vostra safata d'entrada",
"co" to "Thunderbird : Messaghjeria libera",
"cs-CZ" to "Thunderbird: Svobodná pošta",
@@ -38,7 +149,8 @@ fun getNewApps(localeList: LocaleListCompat) =
"fr-FR" to "Thunderbird : Courriel libre",
"fy" to "Thunderbird: Befrij jo Postfek",
"ga" to "Thunderbird: Saor Do Phost",
- "gd" to "Thunderbird: Saorsa dhan phost agad",
+ "gd" to "Thunderbird: Saorsa dhan phost",
+ "gl-ES" to "Thunderbird: O teu correo libre",
"hi-IN" to "थंडरबर्ड - इन्बॉक्स मुक्त करें",
"hr" to "Thunderbird: Oslobodite svoju poštu",
"hu-HU" to "Thunderbird: Szabad levelezés",
@@ -47,6 +159,7 @@ fun getNewApps(localeList: LocaleListCompat) =
"it-IT" to "Thunderbird: libera la tua casella di posta",
"iw-IL" to "ת'אנדרבירד: שחרר את תיבת הדואר שלך",
"ja-JP" to "Thunderbird: 受信トレイをもっと自由に",
+ "ko-KR" to "Thunderbird: 이메일을 자유롭게",
"lt" to "Thunderbird: lais. savo gaut.",
"mnw" to "ဂစေံလလဳ: သၠးကဠာလိက်မၞး",
"nl-NL" to "Thunderbird: Bevrijd uw e-mail",
@@ -64,7 +177,7 @@ fun getNewApps(localeList: LocaleListCompat) =
"sv-SE" to "Thunderbird: Frigör din inkorg",
"ta-IN" to "இடிபறவை: உங்கள் உள்பெட்டி விடுவி",
"tr-TR" to "Thunderbird: Özgür E-posta",
- "uk" to "Thunderbird: Вільна е-пошта",
+ "uk" to "Thunderbird: Вивільніть Вхідні",
"vi" to "Thunderbird: Giải phóng hộp thư của bạn",
"zh-CN" to "Thunderbird:解放收件箱",
"zh-TW" to "Thunderbird: 釋放你的收件匣",
@@ -83,60 +196,6 @@ fun getNewApps(localeList: LocaleListCompat) =
imageModel =
"https://f-droid.org/repo/io.element.android.x/en-US/icon_YBz4_OnMHd7E2Bd_oEza9ImLSlHTpL_C-ovHcKljeFA=.png",
),
- AppDiscoverItem(
- packageName = "org.breezyweather",
- name = mapOf("en-US" to "Breezy Weather").getBestLocale(localeList) ?: "Unknown App",
- isInstalled = true,
- imageModel =
- "https://f-droid.org/repo/org.breezyweather/en-US/icon_e18rWq0tKc__3173BVXmiiasM_F7yrUbs6kh7lQffto=.png",
- ),
- AppDiscoverItem(
- packageName = "helium314.keyboard",
- name =
- mapOf(
- "ar" to "HeliBoard",
- "bg" to "HeliBoard",
- "bn" to "হেলিবোর্ড",
- "ca" to "HeliBoard",
- "cs-CZ" to "HeliBoard",
- "de-DE" to "HeliBoard",
- "en-US" to "HeliBoard",
- "es-ES" to "HeliBoard",
- "et" to "HeliBoard",
- "fi-FI" to "HeliBoard",
- "fr-FR" to "HeliBoard",
- "gl-ES" to "HeliBoard",
- "hu-HU" to "HeliBoard",
- "id" to "HeliBoard",
- "is-IS" to "HeliBoard",
- "it-IT" to "HeliBoard",
- "iw-IL" to "הליבורד HeliBoard",
- "lv" to "HeliBoard",
- "nl-NL" to "HeliBoard",
- "pa-PK" to "ہیلیبورڈ",
- "pl-PL" to "HeliBoard",
- "pt" to "HeliBoard",
- "pt-BR" to "HeliBoard",
- "ru-RU" to "HeliBoard",
- "ta-IN" to "எலிபோர்டு",
- "tr-TR" to "HeliBoard",
- "uk" to "HeliBoard",
- "zh-CN" to "HeliBoard",
- )
- .getBestLocale(localeList) ?: "Unknown App",
- isInstalled = true,
- imageModel =
- "https://f-droid.org/repo/helium314.keyboard/en-US/icon_-nkf9NO4Zp7Y0YX1AOcQdZMf32R_bPt8AZC2Ycn2v1I=.png",
- ),
- AppDiscoverItem(
- packageName = "dev.imranr.obtainium.fdroid",
- name =
- mapOf("de" to "Obtainium", "en-US" to "Obtainium", "ru-RU" to "Obtainium")
- .getBestLocale(localeList) ?: "Unknown App",
- isInstalled = false,
- imageModel =
- "https://f-droid.org/repo/dev.imranr.obtainium.fdroid/en-US/icon_8o6Dm3kPEr-C-8U1QdRd9B6DrkXPqLJQ7or0KN7ut_4=.png",
- ),
)
fun getRecentlyUpdatedApps(localeList: LocaleListCompat) =
@@ -148,6 +207,7 @@ fun getRecentlyUpdatedApps(localeList: LocaleListCompat) =
"ar" to "K-9 Mail",
"be" to "Пошта K-9",
"bg" to "K-9 Поща",
+ "br" to "Postel K-9",
"ca" to "K-9 Mail",
"co" to "K-9 Mail",
"cs-CZ" to "K-9 Mail",
@@ -165,6 +225,7 @@ fun getRecentlyUpdatedApps(localeList: LocaleListCompat) =
"fy" to "K-9 Mail",
"ga" to "K-9 Mail",
"gd" to "Post K-9",
+ "gl-ES" to "K-9 Mail",
"he" to "K-9 דוא\"ל",
"hi-IN" to "K-9 Mail",
"hr" to "K-9 Mail",
@@ -258,14 +319,14 @@ fun getRecentlyUpdatedApps(localeList: LocaleListCompat) =
packageName = "com.kunzisoft.keepass.libre",
name =
mapOf(
- "ar" to "KeePassDX Passkey Vault",
+ "ar" to "خزنة مفتاح مرور KeePassDX",
"cs-CZ" to "Trezor pro klíče KeePassDX",
"de-DE" to "KeePassDX Passkey-Tresor",
"en-US" to "KeePassDX Passkey Vault",
"es-ES" to "KeePassDX Passkey Vault",
"et" to "KeePassDX salasõnahoidla",
"fr-FR" to "KeePassDX - Passkey local",
- "hr" to "KeePassDX Passkey Vault",
+ "hr" to "KeePassDX trezor za lozinke",
"hu-HU" to "KeePassDX jelszótároló",
"id" to "KeePassDX Passkey Vault",
"it-IT" to "Cassaforte passkey KeePassDX",
@@ -277,10 +338,12 @@ fun getRecentlyUpdatedApps(localeList: LocaleListCompat) =
"pl-PL" to "KeePassDX – Bezpieczny sejf",
"ru-RU" to "KeePassDX - менеджер паролей",
"sq" to "Kasafortë Kyçklm. KeePassDX",
+ "ta-IN" to "KeePassDX பாச்கி வால்ட்",
"th" to "ตู้นิรภัยพาสคีย์คียพาสดีเอ็กซ์",
- "tr-TR" to "KeePassDX Passkey Vault",
+ "tr-TR" to "KeePassDX Passkey Kasası",
+ "ur" to "کی پاس ڈی ایکس پاس کی تجوری",
"zh-CN" to "KeePassDX 密码库",
- "zh-TW" to "KeePassDX 密碼(金鑰/單字)保險庫",
+ "zh-TW" to "KeePassDX 密碼金鑰保險庫",
)
.getBestLocale(localeList) ?: "Unknown App",
isInstalled = false,
@@ -300,59 +363,95 @@ fun getMostDownloadedApps(localeList: LocaleListCompat) =
"pt-PT" to "Organic Maps - Mapas offline",
"zh-CN" to "Organic Maps・离线地图与导航 & GPS",
"iw-IL" to "Organic Maps・מפת אופליין ו-GPS",
+ "ms-MY" to "Organic Maps: Peta & Navigasi",
"lv" to "Organic Maps・Kartes・Navigācija",
+ "am" to "Organic Maps・ከመስመር ውጭ ካርታ & GPS",
+ "en-SG" to "Organic Maps・Offline Map & GPS",
+ "af" to "Organic Maps・Aflynkaart & GPS",
"kk" to "Organic Maps: Карталар",
"hr" to "Organic Maps: offline karte",
"th" to "Organic Maps: แผนที่gps",
"si-LK" to "Organic Maps・ඔෆ්ලයින් සිතියම්",
"sl" to "Organic Maps・Offline Karte",
"ru-RU" to "Organic Maps: GPS карты офлайн",
+ "es-419" to "Organic Maps: mapas sin conexión y GPS",
"ne-NP" to "Organic Map・अफलाइन नक्सा & GPS",
+ "jv" to "Organic Maps・Peta Offline & GPS",
"fr-FR" to "Organic Maps: gps hors ligne",
"es-ES" to "Organic Maps: mapas offline",
"mk-MK" to "Organic Maps・Мапи и навигација",
+ "ta-IN" to "Organic Maps・ஆஃப்லைன் வரைபடம்",
"be" to "Organic Maps: GPS карты офлайн",
+ "or" to "Organic Maps・ଅଫଲାଇନ୍ ମ୍ୟାପ୍ ଓ GPS",
+ "en-ZA" to "Organic Maps・Offline Map & GPS",
+ "fr-CA" to "Organic Maps: gps hors ligne",
"lt" to "Organic Maps・žemėlapis & GPS",
"ms" to "Organic Maps: Peta & Navigasi",
+ "sq" to "Organic Maps・Hartë Offline & GPS",
+ "fa-AE" to "Organic Maps・نقشه آفلاین و GPS",
"en-US" to "Organic Maps・Offline Map & GPS",
+ "bs" to "Organic Maps: Offline mape i GPS",
"tr-TR" to "Organic Maps: Haritalar ve GPS",
"ro" to "Organic Maps: hărți offline",
+ "en-GB" to "Organic Maps・Offline Map & GPS",
+ "ug" to "Organic Maps・تورسىز خەرىتە GPS",
"hi-IN" to "Organic Maps・ऑफ़लाइन मैप",
+ "bo" to "Organic Maps・དྲ་རྒྱ་མེད་ས་ཁྲ། GPS",
"mr-IN" to "Organic Maps ऑफलाईन नकाशे",
"bn-BD" to "Organic Maps・मैप्स और नेविगेशन",
"ca" to "Organic Map・Mapa Offline i GPS",
"it-IT" to "Organic Maps: Mappe Offline",
"uk" to "Organic Maps: GPS карти офлайн",
+ "fa-AF" to "Organic Maps・نقشه آفلاین و GPS",
+ "mn-MN" to "Organic Maps・Офлайн газрын зураг ба GPS",
"zh-TW" to "Organic Maps・離線地圖與導航 GPS",
+ "as" to "Organic Maps・অফলাইন মেপ আৰু GPS",
"zh-HK" to "Organic Maps: 地圖與導航",
"gl-ES" to "Organic Maps・Mapa fóra de liña",
+ "mi" to "Organic Maps・Mahere Tuimotu GPS",
+ "en-AU" to "Organic Maps・Offline Map & GPS",
"bg" to "Organic Maps: офлайн GPS карти",
+ "my-MM" to "Organic Maps・အော့ဖ်လိုင်းမြေပုံနှင့် GPS",
"cs-CZ" to "Organic Maps・mapy offline, GPS",
"ml-IN" to "Organic Maps・ഓഫ്ലൈൻ മാപ്പ്",
"nl-NL" to "Organic Maps: offline kaarten",
+ "en-CA" to "Organic Maps・Offline Map & GPS",
"de-DE" to "Organic Maps Offline Karten",
+ "zu" to "Organic Maps・Imephu Offline ne-GPS",
"ja-JP" to "Organic Maps: マップ & ナビゲーション",
"da-DK" to "Organic Maps: Offline Kort",
"fa" to "Organic Maps・نقشه آفلاین و GPS",
+ "en-IN" to "Organic Maps・Offline Map & GPS",
+ "hy-AM" to "Organic Maps・Offline Map & GPS",
"ur" to "Organic Maps・Offline Map & GPS",
+ "mai" to "Organic Maps・ऑफलाइन नक्शा आ GPS",
"ko-KR" to "Organic Maps・GPS 지도 길찾기 앱 & 地图",
"id" to "Organic Maps: pemetaan offline",
+ "ky-KG" to "Organic Maps・Офлайн карта жана GPS",
"sr" to "Organic Maps・Mape i navigacija",
"el-GR" to "Organic Maps: Χάρτες & GPS",
"eu-ES" to "Organic Maps・Offline Map & GPS",
"hu-HU" to "Organic Maps offline navigáció",
"lo-LA" to "Organic Maps・ແຜນທີ່ອອບໄລນ໌",
"no-NO" to "Organic Maps・kart & navigasjon",
+ "rm" to "Organic Maps: Carta Offline & GPS",
+ "sw" to "Organic Maps・Ramani Offline na GPS",
+ "te-IN" to "Organic Maps・ఆఫ్లైన్ మ్యాప్",
+ "uz" to "Organic Maps: Oflayn xarita va GPS",
"sv-SE" to "Organic Maps: kartor & GPS",
"et" to "Organic Maps: gps offline",
"pl-PL" to "Organic Map・mapy i GPS offline",
"kn-IN" to "Organic Maps・ಆಫ್ಲೈನ್ ಮ್ಯಾಪ್",
+ "fil" to "Organic Maps・Offline na Mapa at GPS",
"pt-BR" to "Organic Maps - Mapas offline",
"ar" to "Organic Maps・خرائط بدون إنترنت",
"ka-GE" to "Organic Map: ნავიგაცია, რუკები",
"sk" to "Organic Map・Offline Mapy & GPS",
+ "pa" to "Organic Maps: ਆਫਲਾਈਨ ਨਕਸ਼ੇ",
"gu" to "Organic Maps・ઓફલાઇન નકશો",
+ "is-IS" to "Organic Maps・Ótengd kort og GPS",
"az-AZ" to "Organic Maps – Xəritə və GPS",
+ "es-US" to "Organic Maps: mapas sin conexión y GPS",
"fi-FI" to "Organic Maps: offline kartat",
)
.getBestLocale(localeList) ?: "Unknown App",
diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsItems.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsItems.kt
index 7ecdf198f..2ebfbfbcf 100644
--- a/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsItems.kt
+++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsItems.kt
@@ -8,6 +8,71 @@ import org.fdroid.ui.utils.getPreviewVersion
fun getUpdates(localeList: LocaleListCompat) =
listOf(
+ AppUpdateItem(
+ repoId = 1L,
+ packageName = "app.comaps.fdroid",
+ name =
+ mapOf(
+ "pt-PT" to "CoMaps - Mapas e Navegação - Offline e Privada",
+ "zh-CN" to "CoMaps — 隐私保护下的离线远足、骑行、驾驶",
+ "lv" to "CoMaps - dodies ceļā bezsaistē ar privātumu",
+ "hr" to "CoMaps - Pješačite, biciklirajte, vozite Offline i",
+ "sl" to "CoMaps – potujte brez povezave, zasebno",
+ "ru-RU" to "CoMaps - Карты и путешествия с приватностью",
+ "fr-FR" to "CoMaps - Randonnée, Vélo, Conduite hors ligne",
+ "es-ES" to "CoMaps - Senderismo, ciclismo y conducción offline",
+ "lt" to "CoMaps – keliaukite atsijungę ir privačiai",
+ "en-US" to "CoMaps - Hike, Bike, Drive Offline with Privacy",
+ "tr-TR" to "CoMaps - Gizlilikle Yürüyün, Sürün, Gezin",
+ "ro" to "CoMaps - călătorește offline cu confidențialitate",
+ "bn-BD" to "কোম্যাপস - অফলাইনে হাইকিং, সাইকেলিং ও ড্রাইভিং",
+ "ca" to "CoMaps - Camina, pedala o condueix privadament",
+ "it-IT" to "CoMaps - Navigazione Offline con Privacy",
+ "uk" to "CoMaps — ходьба та поїздки офлайн із приватність",
+ "zh-TW" to "CoMaps — 隱私保護下的離線遠足、騎行、駕駛",
+ "gl-ES" to "CoMaps - Aplicación de mapas privada, sen conexión",
+ "bg" to "CoMaps - Хайкинг, Велосипед, Пътуване без Интернет",
+ "cs-CZ" to "CoMaps – pěšky, na kole a autem offline a soukromě",
+ "nl-NL" to "CoMaps - Wandel, fiets, rijd offline met privacy",
+ "de-DE" to "CoMaps – Offline navigieren mit Datenschutz",
+ "da-DK" to "CoMaps - vandr, cykl og kør offline med privatliv",
+ "id" to "CoMaps- Navigasi Offline dengan Privasi",
+ "sr" to "CoMaps - пешачење, бицикл и вожња, са приватношћу",
+ "el-GR" to "CoMaps - Πλοήγηση εκτός σύνδεσης με ιδιωτικότητα",
+ "eu-ES" to "CoMaps - mapak offline eta dena pribatuan",
+ "hu-HU" to "CoMaps – túrázás, kerékpározás, autózás offline",
+ "no-NO" to "CoMaps - Gå, Sykle, Kjør Uten Internett",
+ "sv-SE" to "Comaps- Vandra, Cykla, Kör Offline, Privat",
+ "et" to "CoMaps - sinu privaatne kaart",
+ "pl-PL" to "CoMaps - Nawiguj offline z zachowaniem prywatności",
+ "kn-IN" to "ಸಹ ನಕ್ಷೆಗಳು - ಆಫ್ಲೈನ್ ಮತ್ತು ಗೌಪ್ಯತೆಯೊಂದಿಗೆ",
+ "pt-BR" to "CoMaps - Mapas e Navegação Offline com Privacidade",
+ "ar" to "CoMaps - تنزه وتنقل وقُد دون إنترنت مع الخصوصية",
+ "sk" to "CoMaps - kráčaj, bicykluj a jazdi v súkromí",
+ "fi-FI" to "CoMaps - Navigoi yksityisesti ilman verkkoyhteyttä",
+ "kw" to "CoMaps - Viajya gans Privetter",
+ "fa-IR" to "CoMaps - کوهنوردی، دوچرخهسواری و رانندگی آفلاین",
+ "pt" to "CoMaps - Mapas e Navegação - Offline e Privada",
+ "ta-IN" to "இணைவரைபடங்கள் - மலையேறு, வண்டி, தனிமையில் இயக்கு",
+ "en-GB" to "CoMaps - Hike, Bike, Drive Offline with Privacy",
+ "mt" to "CoMaps - Imxi, Suq Rota u Karozza bil-Privatezza",
+ "bn" to "CoMaps - অফলাইনে হাইক, বাইক, ড্রাইভ করুন",
+ "brh" to "CoMaps - آفلائن مش لگ، سائکل سواری و موٹر سواری",
+ "en-CA" to "CoMaps - Hike, Bike, Drive Offline with Privacy",
+ "eo" to "CoMaps - Migru, Biciklu, Veturigu Eksterrete",
+ "ast" to "CoMaps - Mapas ensin conexón con privacidá",
+ )
+ .getBestLocale(localeList) ?: "Unknown App",
+ installedVersionName = "2026.05.06-10-FDroid",
+ update =
+ getPreviewVersion(
+ versionName = "2026.05.06-11-FDroid",
+ size = 65470491,
+ ),
+ whatsNew = "foo bar",
+ iconModel =
+ "https://f-droid.org/repo/app.comaps.fdroid/en-US/icon_Xqa9hlGluoI0ME9XbHrLOaAJ_dX9wWMtDhLgpFYbUOI=.png",
+ ),
AppUpdateItem(
repoId = 1L,
packageName = "app.organicmaps",
@@ -18,64 +83,104 @@ fun getUpdates(localeList: LocaleListCompat) =
"pt-PT" to "Organic Maps - Mapas offline",
"zh-CN" to "Organic Maps・离线地图与导航 & GPS",
"iw-IL" to "Organic Maps・מפת אופליין ו-GPS",
+ "ms-MY" to "Organic Maps: Peta & Navigasi",
"lv" to "Organic Maps・Kartes・Navigācija",
+ "am" to "Organic Maps・ከመስመር ውጭ ካርታ & GPS",
+ "en-SG" to "Organic Maps・Offline Map & GPS",
+ "af" to "Organic Maps・Aflynkaart & GPS",
"kk" to "Organic Maps: Карталар",
"hr" to "Organic Maps: offline karte",
"th" to "Organic Maps: แผนที่gps",
"si-LK" to "Organic Maps・ඔෆ්ලයින් සිතියම්",
"sl" to "Organic Maps・Offline Karte",
"ru-RU" to "Organic Maps: GPS карты офлайн",
+ "es-419" to "Organic Maps: mapas sin conexión y GPS",
"ne-NP" to "Organic Map・अफलाइन नक्सा & GPS",
+ "jv" to "Organic Maps・Peta Offline & GPS",
"fr-FR" to "Organic Maps: gps hors ligne",
"es-ES" to "Organic Maps: mapas offline",
"mk-MK" to "Organic Maps・Мапи и навигација",
+ "ta-IN" to "Organic Maps・ஆஃப்லைன் வரைபடம்",
"be" to "Organic Maps: GPS карты офлайн",
+ "or" to "Organic Maps・ଅଫଲାଇନ୍ ମ୍ୟାପ୍ ଓ GPS",
+ "en-ZA" to "Organic Maps・Offline Map & GPS",
+ "fr-CA" to "Organic Maps: gps hors ligne",
"lt" to "Organic Maps・žemėlapis & GPS",
"ms" to "Organic Maps: Peta & Navigasi",
+ "sq" to "Organic Maps・Hartë Offline & GPS",
+ "fa-AE" to "Organic Maps・نقشه آفلاین و GPS",
"en-US" to "Organic Maps・Offline Map & GPS",
+ "bs" to "Organic Maps: Offline mape i GPS",
"tr-TR" to "Organic Maps: Haritalar ve GPS",
"ro" to "Organic Maps: hărți offline",
+ "en-GB" to "Organic Maps・Offline Map & GPS",
+ "ug" to "Organic Maps・تورسىز خەرىتە GPS",
"hi-IN" to "Organic Maps・ऑफ़लाइन मैप",
+ "bo" to "Organic Maps・དྲ་རྒྱ་མེད་ས་ཁྲ། GPS",
"mr-IN" to "Organic Maps ऑफलाईन नकाशे",
"bn-BD" to "Organic Maps・मैप्स और नेविगेशन",
"ca" to "Organic Map・Mapa Offline i GPS",
"it-IT" to "Organic Maps: Mappe Offline",
"uk" to "Organic Maps: GPS карти офлайн",
+ "fa-AF" to "Organic Maps・نقشه آفلاین و GPS",
+ "mn-MN" to "Organic Maps・Офлайн газрын зураг ба GPS",
"zh-TW" to "Organic Maps・離線地圖與導航 GPS",
+ "as" to "Organic Maps・অফলাইন মেপ আৰু GPS",
"zh-HK" to "Organic Maps: 地圖與導航",
"gl-ES" to "Organic Maps・Mapa fóra de liña",
+ "mi" to "Organic Maps・Mahere Tuimotu GPS",
+ "en-AU" to "Organic Maps・Offline Map & GPS",
"bg" to "Organic Maps: офлайн GPS карти",
+ "my-MM" to "Organic Maps・အော့ဖ်လိုင်းမြေပုံနှင့် GPS",
"cs-CZ" to "Organic Maps・mapy offline, GPS",
"ml-IN" to "Organic Maps・ഓഫ്ലൈൻ മാപ്പ്",
"nl-NL" to "Organic Maps: offline kaarten",
+ "en-CA" to "Organic Maps・Offline Map & GPS",
"de-DE" to "Organic Maps Offline Karten",
+ "zu" to "Organic Maps・Imephu Offline ne-GPS",
"ja-JP" to "Organic Maps: マップ & ナビゲーション",
"da-DK" to "Organic Maps: Offline Kort",
"fa" to "Organic Maps・نقشه آفلاین و GPS",
+ "en-IN" to "Organic Maps・Offline Map & GPS",
+ "hy-AM" to "Organic Maps・Offline Map & GPS",
"ur" to "Organic Maps・Offline Map & GPS",
+ "mai" to "Organic Maps・ऑफलाइन नक्शा आ GPS",
"ko-KR" to "Organic Maps・GPS 지도 길찾기 앱 & 地图",
"id" to "Organic Maps: pemetaan offline",
+ "ky-KG" to "Organic Maps・Офлайн карта жана GPS",
"sr" to "Organic Maps・Mape i navigacija",
"el-GR" to "Organic Maps: Χάρτες & GPS",
"eu-ES" to "Organic Maps・Offline Map & GPS",
"hu-HU" to "Organic Maps offline navigáció",
"lo-LA" to "Organic Maps・ແຜນທີ່ອອບໄລນ໌",
"no-NO" to "Organic Maps・kart & navigasjon",
+ "rm" to "Organic Maps: Carta Offline & GPS",
+ "sw" to "Organic Maps・Ramani Offline na GPS",
+ "te-IN" to "Organic Maps・ఆఫ్లైన్ మ్యాప్",
+ "uz" to "Organic Maps: Oflayn xarita va GPS",
"sv-SE" to "Organic Maps: kartor & GPS",
"et" to "Organic Maps: gps offline",
"pl-PL" to "Organic Map・mapy i GPS offline",
"kn-IN" to "Organic Maps・ಆಫ್ಲೈನ್ ಮ್ಯಾಪ್",
+ "fil" to "Organic Maps・Offline na Mapa at GPS",
"pt-BR" to "Organic Maps - Mapas offline",
"ar" to "Organic Maps・خرائط بدون إنترنت",
"ka-GE" to "Organic Map: ნავიგაცია, რუკები",
"sk" to "Organic Map・Offline Mapy & GPS",
+ "pa" to "Organic Maps: ਆਫਲਾਈਨ ਨਕਸ਼ੇ",
"gu" to "Organic Maps・ઓફલાઇન નકશો",
+ "is-IS" to "Organic Maps・Ótengd kort og GPS",
"az-AZ" to "Organic Maps – Xəritə və GPS",
+ "es-US" to "Organic Maps: mapas sin conexión y GPS",
"fi-FI" to "Organic Maps: offline kartat",
)
.getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "2026.02.18-4-FDroid",
- update = getPreviewVersion(versionName = "2026.02.18-5-FDroid", size = 70355961),
+ installedVersionName = "2026.05.27-10-FDroid",
+ update =
+ getPreviewVersion(
+ versionName = "2026.05.27-11-FDroid",
+ size = 71778828,
+ ),
whatsNew = "foo bar",
iconModel =
"https://f-droid.org/repo/app.organicmaps/en-US/icon_dE7f4P95-uKZwu7cI89Q0xSi_-gvU4DD-XnLoDG9RLg=.png",
@@ -84,34 +189,37 @@ fun getUpdates(localeList: LocaleListCompat) =
repoId = 1L,
packageName = "at.bitfire.davdroid",
name = mapOf("ca" to "DAVx⁵", "en-US" to "DAVx⁵").getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "4.5.8-ose",
- update = getPreviewVersion(versionName = "4.5.9-ose", size = 15974669),
+ installedVersionName = "4.5.12-ose",
+ update =
+ getPreviewVersion(
+ versionName = "4.5.13-ose",
+ size = 16018266,
+ ),
whatsNew = null,
iconModel =
"https://f-droid.org/repo/at.bitfire.davdroid/en-US/icon_NexNou7vmaD45rbdc8kjLj0Rv7FW128Mde9OQpHFXPE=.png",
),
- AppUpdateItem(
- repoId = 1L,
- packageName = "ch.protonvpn.android",
- name =
- mapOf("en-US" to "ProtonVPN - Secure and Free VPN").getBestLocale(localeList)
- ?: "Unknown App",
- installedVersionName = "5.16.30.9",
- update = getPreviewVersion(versionName = "5.16.31.0", size = 54161941),
- whatsNew = "foo bar",
- iconModel =
- "https://f-droid.org/repo/ch.protonvpn.android/en-US/icon_xAphlcwMgx7oE7sxB15sxUFIO8geUokWkG01K1fubas=.png",
- ),
)
fun getInstalledApps(localeList: LocaleListCompat) =
listOf(
+ InstalledAppItem(
+ packageName = "ch.protonvpn.android",
+ name =
+ mapOf("en-US" to "ProtonVPN - Secure and Free VPN").getBestLocale(localeList)
+ ?: "Unknown App",
+ installedVersionName = "5.18.75.0",
+ installedVersionCode = 1,
+ lastUpdated = 1780506857489L,
+ iconModel =
+ "https://f-droid.org/repo/ch.protonvpn.android/en-US/icon_xAphlcwMgx7oE7sxB15sxUFIO8geUokWkG01K1fubas=.png",
+ ),
InstalledAppItem(
packageName = "com.aurora.store",
name = mapOf("en-US" to "Aurora Store").getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "4.8.1",
+ installedVersionName = "4.8.3",
installedVersionCode = 1,
- lastUpdated = 1771544205000L,
+ lastUpdated = 1779007734975L,
iconModel =
"https://f-droid.org/repo/com.aurora.store/en-US/icon_tbAhwq51NNd0liZcTg0cQNNvazxrWlj7bPCe_1TkCV8=.png",
),
@@ -119,18 +227,18 @@ fun getInstalledApps(localeList: LocaleListCompat) =
packageName = "com.duckduckgo.mobile.android",
name =
mapOf("en-US" to "DuckDuckGo Privacy Browser").getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "5.268.1",
+ installedVersionName = "5.281.1",
installedVersionCode = 1,
- lastUpdated = 1772699310000L,
+ lastUpdated = 1780614376764L,
iconModel =
"https://f-droid.org/repo/com.duckduckgo.mobile.android/en-US/icon_QyKhnj_C0Pek3xtJAfPFTvywRXDutsVh7r-9t-doP9E=.png",
),
InstalledAppItem(
packageName = "com.foobnix.pro.pdf.reader",
name = mapOf("en-US" to "Librera Reader").getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "9.3.63-fdroid",
+ installedVersionName = "9.3.75-fdroid",
installedVersionCode = 1,
- lastUpdated = 1772574994000L,
+ lastUpdated = 1777245514905L,
iconModel =
"https://f-droid.org/repo/com.foobnix.pro.pdf.reader/en-US/icon_y15Jxhzp6YrmjLC-wtc27B6XLTcArf7yK-2WlpNaoe0=.png",
),
@@ -141,6 +249,7 @@ fun getInstalledApps(localeList: LocaleListCompat) =
"ar" to "K-9 Mail",
"be" to "Пошта K-9",
"bg" to "K-9 Поща",
+ "br" to "Postel K-9",
"ca" to "K-9 Mail",
"co" to "K-9 Mail",
"cs-CZ" to "K-9 Mail",
@@ -158,6 +267,7 @@ fun getInstalledApps(localeList: LocaleListCompat) =
"fy" to "K-9 Mail",
"ga" to "K-9 Mail",
"gd" to "Post K-9",
+ "gl-ES" to "K-9 Mail",
"he" to "K-9 דוא\"ל",
"hi-IN" to "K-9 Mail",
"hr" to "K-9 Mail",
@@ -191,36 +301,82 @@ fun getInstalledApps(localeList: LocaleListCompat) =
"zh-TW" to "K-9 Mail",
)
.getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "16.1",
+ installedVersionName = "19.2",
installedVersionCode = 1,
- lastUpdated = 1770745842000L,
+ lastUpdated = 1780699361309L,
iconModel =
"https://f-droid.org/repo/com.fsck.k9/en-US/icon_-2bZW0ZnkKqPVher2SxQK8hXGGSjgaBHoa8x6vW0v8w=.png",
),
InstalledAppItem(
packageName = "com.github.andreyasadchy.xtra",
name = mapOf("en-US" to "Xtra").getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "2.54.3",
+ installedVersionName = "2.56.2",
installedVersionCode = 1,
- lastUpdated = 1772397992000L,
+ lastUpdated = 1780506857489L,
iconModel =
"https://f-droid.org/repo/com.github.andreyasadchy.xtra/en-US/icon_yfnxF0cNrXwFX2a93BUclISVaIQzoRmbOuxtGDAwz28=.png",
),
+ InstalledAppItem(
+ packageName = "com.github.catfriend1.syncthingfork",
+ name =
+ mapOf(
+ "en-US" to "Syncthing-Fork",
+ "vi" to "Syncthing-Fork",
+ "he" to "Syncthing-Fork",
+ "pt-PT" to "Syncthing-Fork",
+ "zh-CN" to "Syncthing-Fork",
+ "ru-RU" to "Syncthing-Fork",
+ "es-419" to "Syncthing-Fork",
+ "fr-FR" to "Syncthing-Fork",
+ "es-ES" to "Syncthing-Fork",
+ "be" to "Syncthing-Fork",
+ "tr-TR" to "Syncthing-Fork",
+ "ro" to "Syncthing-Fork",
+ "ca" to "Syncthing-Fork",
+ "it-IT" to "Syncthing-Fork",
+ "uk" to "Syncthing-Fork",
+ "zh-TW" to "Syncthing-Fork",
+ "bg" to "Syncthing-Fork",
+ "cs-CZ" to "Syncthing-Fork",
+ "nl-NL" to "Syncthing-Fork",
+ "de-DE" to "Syncthing-Fork",
+ "ja-JP" to "Syncthing-Fork",
+ "da-DK" to "Syncthing-Fork",
+ "ko-KR" to "Syncthing-Fork",
+ "id" to "Syncthing-Fork",
+ "sr" to "Syncthing-Fork",
+ "hu-HU" to "Syncthing-Fork",
+ "sv-SE" to "Syncthing-Fork",
+ "et" to "Syncthing-Fork",
+ "pl-PL" to "Syncthing-Fork",
+ "pt-BR" to "Syncthing-Fork",
+ "ar" to "Syncthing-Fork",
+ "sk" to "Syncthing-Fork",
+ "es-US" to "Syncthing-Fork",
+ "fi-FI" to "Syncthing-Fork",
+ )
+ .getBestLocale(localeList) ?: "Unknown App",
+ installedVersionName = "2.1.0.0",
+ installedVersionCode = 1,
+ lastUpdated = 1778740722978L,
+ iconModel =
+ "https://f-droid.org/repo/com.github.catfriend1.syncthingfork/en-US/icon_egWOwTWZpQ_1CxmdrwP5wRta5EVmCCuzQWxjI5JTs0w=.png",
+ ),
InstalledAppItem(
packageName = "com.github.libretube",
name = mapOf("en-US" to "LibreTube").getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "0.30.0",
+ installedVersionName = "31.4",
installedVersionCode = 1,
- lastUpdated = 1769702789000L,
+ lastUpdated = 1780094298321L,
iconModel =
"https://f-droid.org/repo/com.github.libretube/en-US/icon_p6_cfoYk-2IlerJpH4rKIQmkI76zXtS3R9-RZp-3Ggk=.png",
),
InstalledAppItem(
packageName = "com.inspiredandroid.linuxcommandbibliotheca",
name = mapOf("en-US" to "Linux Command Library").getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "3.5.13",
+ installedVersionName = "4.0.6",
installedVersionCode = 1,
- lastUpdated = 1772463524000L,
+ lastUpdated = 1780012218733L,
iconModel =
"https://f-droid.org/repo/com.inspiredandroid.linuxcommandbibliotheca/en-US/icon_DrA_e_52nvGa_kTMSapBw4MCqKpccl8WZqrG5enB7rI=.png",
),
@@ -257,48 +413,4 @@ fun getInstalledApps(localeList: LocaleListCompat) =
iconModel =
"https://f-droid.org/repo/com.junkfood.seal/en-US/icon_j8mCLA_OX-sJn7TRAdvoaUMW1f6djRv6RMzAnNdaG4I=.png",
),
- InstalledAppItem(
- packageName = "com.kunzisoft.keepass.libre",
- name =
- mapOf(
- "ar" to "KeePassDX Passkey Vault",
- "cs-CZ" to "Trezor pro klíče KeePassDX",
- "de-DE" to "KeePassDX Passkey-Tresor",
- "en-US" to "KeePassDX Passkey Vault",
- "es-ES" to "KeePassDX Passkey Vault",
- "et" to "KeePassDX salasõnahoidla",
- "fr-FR" to "KeePassDX - Passkey local",
- "hr" to "KeePassDX Passkey Vault",
- "hu-HU" to "KeePassDX jelszótároló",
- "id" to "KeePassDX Passkey Vault",
- "it-IT" to "Cassaforte passkey KeePassDX",
- "ja-JP" to "KeePassDX パスワード管理ツール",
- "lt" to "KeePassDX slaptažodžių seifas",
- "lv" to "KeePassDX: paroļu pārvaldnieks",
- "mk-MK" to "KeePassDX Passkey Vault",
- "nl-NL" to "KeePassDX Passkey-kluis",
- "pl-PL" to "KeePassDX – Bezpieczny sejf",
- "ru-RU" to "KeePassDX - менеджер паролей",
- "sq" to "Kasafortë Kyçklm. KeePassDX",
- "th" to "ตู้นิรภัยพาสคีย์คียพาสดีเอ็กซ์",
- "tr-TR" to "KeePassDX Passkey Vault",
- "zh-CN" to "KeePassDX 密码库",
- "zh-TW" to "KeePassDX 密碼(金鑰/單字)保險庫",
- )
- .getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "4.3.2",
- installedVersionCode = 1,
- lastUpdated = 1768680637000L,
- iconModel =
- "https://f-droid.org/repo/com.kunzisoft.keepass.libre/en-US/icon_eLwXEQD9l2URrUS3t8esDXnsKGBaH02E-ddEYhV_i7Q=.png",
- ),
- InstalledAppItem(
- packageName = "com.looker.droidify",
- name = mapOf("en-US" to "Droid-ify").getBestLocale(localeList) ?: "Unknown App",
- installedVersionName = "0.7.0",
- installedVersionCode = 1,
- lastUpdated = 1771667689000L,
- iconModel =
- "https://f-droid.org/repo/com.looker.droidify/en-US/icon_lApsnfhrMqGkCaW-IdnYwx86CUjdTutncPvXPJythaM=.png",
- ),
)
diff --git a/app/src/full/res/drawable/check.xml b/app/src/full/res/drawable/check.xml
deleted file mode 100644
index 3f26c70ce..000000000
--- a/app/src/full/res/drawable/check.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/full/res/drawable/ic_add_circle_outline.xml b/app/src/full/res/drawable/ic_add_circle_outline.xml
deleted file mode 100644
index c6c1d9a0a..000000000
--- a/app/src/full/res/drawable/ic_add_circle_outline.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/full/res/layout/swap_app_list_item.xml b/app/src/full/res/layout/swap_app_list_item.xml
deleted file mode 100644
index 8530a552e..000000000
--- a/app/src/full/res/layout/swap_app_list_item.xml
+++ /dev/null
@@ -1,97 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/full/res/values-night/colors.xml b/app/src/full/res/values-night/colors.xml
index 9e5b2edef..748b3a003 100644
--- a/app/src/full/res/values-night/colors.xml
+++ b/app/src/full/res/values-night/colors.xml
@@ -1,7 +1,5 @@
- #ffcf6679
- #ffb8b8b8
diff --git a/app/src/full/res/values-night/themes.xml b/app/src/full/res/values-night/themes.xml
index f90ef08f1..19d46ca6c 100644
--- a/app/src/full/res/values-night/themes.xml
+++ b/app/src/full/res/values-night/themes.xml
@@ -68,8 +68,4 @@
@color/md_theme_surfaceContainerHighest
-
diff --git a/app/src/full/res/values/colors.xml b/app/src/full/res/values/colors.xml
index 9e78fbf4e..115446eb4 100644
--- a/app/src/full/res/values/colors.xml
+++ b/app/src/full/res/values/colors.xml
@@ -1,8 +1,5 @@
- #ffCC0000
- #ff999999
- #ffdd2c00#ff1976d2#ff8ab000
@@ -10,10 +7,6 @@
@color/fdroid_red#ff757575
- #cc222222
-
- #fff4511e
-
#005197
@@ -63,11 +56,6 @@
#E6E8F0#E0E2EA
- #FFB9B9
- #540303
- #0000ff
-
- #ff7900#27aae1#1c6bbc#ff21488c
diff --git a/app/src/full/res/values/styles.xml b/app/src/full/res/values/styles.xml
index 07bf466f4..3690ff410 100644
--- a/app/src/full/res/values/styles.xml
+++ b/app/src/full/res/values/styles.xml
@@ -1,52 +1,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
@@ -66,130 +33,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/full/res/values/themes.xml b/app/src/full/res/values/themes.xml
index e6f057110..398d579c7 100644
--- a/app/src/full/res/values/themes.xml
+++ b/app/src/full/res/values/themes.xml
@@ -85,15 +85,4 @@
-
-
-
-
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f898a8c63..6d6127967 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -35,7 +35,9 @@
android:name="org.fdroid.App"
android:allowBackup="true"
android:banner="@mipmap/ic_banner"
+ android:dataExtractionRules="@xml/backup_extraction_rules"
android:enableOnBackInvokedCallback="true"
+ android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
@@ -163,7 +165,7 @@
diff --git a/app/src/main/kotlin/org/fdroid/App.kt b/app/src/main/kotlin/org/fdroid/App.kt
index 9a88da17a..b0bac13d0 100644
--- a/app/src/main/kotlin/org/fdroid/App.kt
+++ b/app/src/main/kotlin/org/fdroid/App.kt
@@ -38,6 +38,7 @@ import org.fdroid.ui.crash.CrashActivity
import org.fdroid.ui.crash.NoRetryPolicy
import org.fdroid.ui.utils.applyNewTheme
import org.fdroid.updates.AppUpdateWorker
+import org.fdroid.updates.SelfUpdateReceiver
@HiltAndroidApp
class App : Application(), Configuration.Provider, SingletonImageLoader.Factory {
@@ -100,6 +101,9 @@ class App : Application(), Configuration.Provider, SingletonImageLoader.Factory
// bail out here if we are the ACRA process to not initialize anything in crash process
if (isAcraProces()) return
+ // don't show self-update notification unless we enable it first
+ SelfUpdateReceiver.disable(this)
+
RepoUpdateWorker.scheduleOrCancel(applicationContext, settingsManager.repoUpdates)
AppUpdateWorker.scheduleOrCancel(applicationContext, settingsManager.autoUpdateApps)
}
diff --git a/app/src/main/kotlin/org/fdroid/ui/About.kt b/app/src/main/kotlin/org/fdroid/ui/About.kt
index 76b215100..e3bf6c78e 100644
--- a/app/src/main/kotlin/org/fdroid/ui/About.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/About.kt
@@ -67,7 +67,7 @@ fun About(version: String = VERSION_NAME, onBackClicked: (() -> Unit)?) {
)
}
},
- title = { Text(stringResource(R.string.about_title_full)) },
+ title = { Text(stringResource(R.string.about_title_full), maxLines = 1) },
scrollBehavior = scrollBehavior,
)
},
diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt
index edd354359..559164e1f 100644
--- a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt
@@ -93,7 +93,7 @@ fun MyApps(
}
} else
TopAppBar(
- title = { Text(stringResource(R.string.menu_apps_my)) },
+ title = { Text(stringResource(R.string.menu_apps_my), maxLines = 1) },
actions = {
TopAppBarButton(
imageVector = Icons.AutoMirrored.Default.ManageSearch,
diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt
index f76acfcce..81999968b 100644
--- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt
@@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.Icons.AutoMirrored
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.automirrored.filled.Message
+import androidx.compose.material.icons.automirrored.filled.QueueMusic
import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.AccountBalanceWallet
import androidx.compose.material.icons.filled.Airplay
@@ -17,6 +18,7 @@ import androidx.compose.material.icons.filled.BrowserUpdated
import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.Calculate
import androidx.compose.material.icons.filled.CalendarMonth
+import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.Casino
import androidx.compose.material.icons.filled.Castle
import androidx.compose.material.icons.filled.Category
@@ -39,6 +41,7 @@ import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.EnhancedEncryption
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.Fastfood
+import androidx.compose.material.icons.filled.FiberSmartRecord
import androidx.compose.material.icons.filled.FileCopy
import androidx.compose.material.icons.filled.FitnessCenter
import androidx.compose.material.icons.filled.FlashlightOn
@@ -80,6 +83,7 @@ import androidx.compose.material.icons.filled.SignalCellularAlt
import androidx.compose.material.icons.filled.Sos
import androidx.compose.material.icons.filled.SportsMartialArts
import androidx.compose.material.icons.filled.SportsSoccer
+import androidx.compose.material.icons.filled.StackedLineChart
import androidx.compose.material.icons.filled.Storefront
import androidx.compose.material.icons.filled.Style
import androidx.compose.material.icons.filled.TaskAlt
@@ -111,6 +115,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
"Browser" -> Icons.Default.OpenInBrowser
"Calculator" -> Icons.Default.Calculate
"Calendar & Agenda" -> Icons.Default.CalendarMonth
+ "Camera" -> Icons.Default.CameraAlt
"Card Game" -> Icons.Default.Style
"Casual Game" -> Icons.Default.Gamepad
"Clock" -> Icons.Default.AccessTime
@@ -146,6 +151,8 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
"Launcher" -> Icons.Default.Home
"Local Media Player" -> Icons.Default.LocalPlay
"Location Tracker & Sharer" -> Icons.Default.MyLocation
+ "Lyrics" -> AutoMirrored.Default.QueueMusic
+ "Market & Price" -> Icons.Default.StackedLineChart
"Meditation" -> Icons.Default.SelfImprovement
"Messaging" -> AutoMirrored.Default.Message
"Money" -> Icons.Default.Money
@@ -167,6 +174,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
"Radio" -> Icons.Default.Radio
"Reading" -> AutoMirrored.Default.MenuBook
"Recipe Manager" -> Icons.Default.RestaurantMenu
+ "Recorder" -> Icons.Default.FiberSmartRecord
"Religion" -> Icons.Default.Church
"Role-Playing Game" -> Icons.Default.Diversity3
"Remote Access" -> Icons.Default.BrowserUpdated
@@ -215,6 +223,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
"Browser" -> CategoryGroups.network
"Calculator" -> CategoryGroups.tools
"Calendar & Agenda" -> CategoryGroups.productivity
+ "Camera" -> CategoryGroups.device
"Card Game" -> CategoryGroups.games
"Casual Game" -> CategoryGroups.games
"Clock" -> CategoryGroups.productivity
@@ -250,6 +259,8 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
"Launcher" -> CategoryGroups.device
"Local Media Player" -> CategoryGroups.media
"Location Tracker & Sharer" -> CategoryGroups.tools
+ "Lyrics" -> CategoryGroups.interests
+ "Market & Price" -> CategoryGroups.interests
"Meditation" -> CategoryGroups.interests
"Messaging" -> CategoryGroups.communication
"Money" -> CategoryGroups.wallets
@@ -271,6 +282,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
"Radio" -> CategoryGroups.media
"Reading" -> CategoryGroups.media
"Recipe Manager" -> CategoryGroups.interests
+ "Recorder" -> CategoryGroups.tools
"Religion" -> CategoryGroups.interests
"Role-Playing Game" -> CategoryGroups.games
"Remote Access" -> CategoryGroups.network
diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/Discover.kt b/app/src/main/kotlin/org/fdroid/ui/discover/Discover.kt
index c6544d0f5..5581079f4 100644
--- a/app/src/main/kotlin/org/fdroid/ui/discover/Discover.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/discover/Discover.kt
@@ -48,7 +48,7 @@ fun Discover(
Scaffold(
topBar = {
TopAppBar(
- title = { Text(stringResource(R.string.app_name)) },
+ title = { Text(stringResource(R.string.app_name), maxLines = 1) },
actions = {
topBarMenuItems.forEach { dest ->
BadgedBox(
diff --git a/app/src/main/kotlin/org/fdroid/ui/history/History.kt b/app/src/main/kotlin/org/fdroid/ui/history/History.kt
index e9b0ffc91..3bbd5c7f7 100644
--- a/app/src/main/kotlin/org/fdroid/ui/history/History.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/history/History.kt
@@ -42,7 +42,7 @@ fun History(
topBar = {
TopAppBar(
navigationIcon = { if (onBackClicked != null) BackButton(onClick = onBackClicked) },
- title = { Text(stringResource(R.string.install_history)) },
+ title = { Text(stringResource(R.string.install_history), maxLines = 1) },
actions = {
if (!items.isNullOrEmpty())
TopAppBarButton(
diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt
index 3dc4b1914..970c8a9f0 100644
--- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt
@@ -66,7 +66,8 @@ fun AddRepo(
}
} else {
stringResource(R.string.repo_add_new_title)
- }
+ },
+ maxLines = 1,
)
},
)
diff --git a/app/src/main/kotlin/org/fdroid/ui/settings/Settings.kt b/app/src/main/kotlin/org/fdroid/ui/settings/Settings.kt
index 20ec99e3b..80cdb2c07 100644
--- a/app/src/main/kotlin/org/fdroid/ui/settings/Settings.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/settings/Settings.kt
@@ -94,7 +94,7 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked:
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
- title = { Text(stringResource(R.string.menu_settings)) },
+ title = { Text(stringResource(R.string.menu_settings), maxLines = 1) },
)
}
) { paddingValues ->
diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/TopAppBarOverflowButton.kt b/app/src/main/kotlin/org/fdroid/ui/utils/TopAppBarOverflowButton.kt
index 211423f04..bff7cc261 100644
--- a/app/src/main/kotlin/org/fdroid/ui/utils/TopAppBarOverflowButton.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/utils/TopAppBarOverflowButton.kt
@@ -21,7 +21,7 @@ fun TopAppBarOverflowButton(
var menuExpanded by remember { mutableStateOf(false) }
TopAppBarButton(
Icons.Default.MoreVert,
- contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
+ contentDescription = stringResource(R.string.menu_overflow_description),
onClick = { menuExpanded = !menuExpanded },
)
DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) {
diff --git a/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt b/app/src/main/kotlin/org/fdroid/updates/SelfUpdateReceiver.kt
similarity index 52%
rename from app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt
rename to app/src/main/kotlin/org/fdroid/updates/SelfUpdateReceiver.kt
index 75bd1d37a..c6f82b099 100644
--- a/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt
+++ b/app/src/main/kotlin/org/fdroid/updates/SelfUpdateReceiver.kt
@@ -1,32 +1,53 @@
-package org.fdroid.install
+package org.fdroid.updates
import android.content.BroadcastReceiver
+import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.Intent.ACTION_MY_PACKAGE_REPLACED
-import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.os.Build.VERSION.SDK_INT
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+import android.content.pm.PackageManager.DONT_KILL_APP
+import android.os.Build
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import mu.KotlinLogging
import org.fdroid.NotificationManager
@AndroidEntryPoint
-class AppUpdateReceiver : BroadcastReceiver() {
+class SelfUpdateReceiver : BroadcastReceiver() {
private val log = KotlinLogging.logger {}
@Inject lateinit var notificationManager: NotificationManager
+ companion object {
+ fun enable(context: Context) {
+ setEnabledState(context, COMPONENT_ENABLED_STATE_ENABLED)
+ }
+
+ fun disable(context: Context) {
+ setEnabledState(context, COMPONENT_ENABLED_STATE_DISABLED)
+ }
+
+ private fun setEnabledState(context: Context, state: Int) {
+ val component = ComponentName(context, SelfUpdateReceiver::class.java)
+ context.packageManager.setComponentEnabledSetting(
+ component,
+ state,
+ DONT_KILL_APP,
+ )
+ }
+ }
+
override fun onReceive(context: Context, intent: Intent) {
- if (intent.action != ACTION_MY_PACKAGE_REPLACED) {
+ if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) {
log.warn { "Unknown action: ${intent.action}" }
return
}
log.info { "Intent received, we just updated ourselves!" }
val intent =
context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
- addFlags(FLAG_ACTIVITY_NEW_TASK)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (intent == null) {
log.error { "Could not get launch intent for ourselves" }
@@ -39,6 +60,6 @@ class AppUpdateReceiver : BroadcastReceiver() {
}
// show notification on Android 10+, because we aren't allowed to start activity from background
// see: https://developer.android.com/guide/components/activities/background-starts
- if (SDK_INT >= 29) notificationManager.showSelfUpdateNotification()
+ if (Build.VERSION.SDK_INT >= 29) notificationManager.showSelfUpdateNotification()
}
}
diff --git a/app/src/main/kotlin/org/fdroid/updates/UpdateInstaller.kt b/app/src/main/kotlin/org/fdroid/updates/UpdateInstaller.kt
index fa9b2d5af..ce1170c46 100644
--- a/app/src/main/kotlin/org/fdroid/updates/UpdateInstaller.kt
+++ b/app/src/main/kotlin/org/fdroid/updates/UpdateInstaller.kt
@@ -20,6 +20,7 @@ import org.fdroid.database.FDroidDatabase
import org.fdroid.index.RepoManager
import org.fdroid.install.AppInstallManager
import org.fdroid.ui.apps.AppUpdateItem
+import org.fdroid.ui.utils.isAppInForeground
import org.fdroid.utils.IoDispatcher
/**
@@ -67,7 +68,15 @@ constructor(
// Update all non-self apps first, then our own package at the end.
updateAppsInParallel(otherApps, canRequestPreApproval)
- ownApp?.let { update -> updateApp(update, canRequestPreApproval) }
+ // If available, we update ourselves last
+ ownApp?.let { update ->
+ if (context.isAppInForeground()) {
+ // enable the receiver only if the app is currently in the foreground,
+ // so the user can easily re-launch it. It will get disabled again in the app's onCreate
+ SelfUpdateReceiver.enable(context)
+ }
+ updateApp(update, canRequestPreApproval)
+ }
}
private suspend fun updateAppsInParallel(
@@ -114,13 +123,13 @@ constructor(
private suspend fun updateApp(update: AppUpdateItem, canAskPreApprovalNow: Boolean) {
val app = db.getAppDao().getApp(update.repoId, update.packageName)
appInstallManager.install(
- packageName = update.packageName,
- 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/main/kotlin/org/fdroid/updates/UpdatesManager.kt b/app/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt
index ab4f782bc..df8bb5528 100644
--- a/app/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt
+++ b/app/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt
@@ -7,6 +7,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.client.engine.ProxyConfig
import javax.inject.Inject
import javax.inject.Singleton
+import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.measureTimedValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@@ -76,7 +77,7 @@ constructor(
init {
coroutineScope.launch {
// delay initial check for updates a bit, so we don't hammer the DB during start-up
- delay(1500)
+ delay(1500.milliseconds)
// Auto-refresh updates when installed apps change.
installedAppsCache.installedApps.collect { loadUpdates(it) }
}
diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml
index 84c5b1784..943d32333 100644
--- a/app/src/main/res/values-af/strings.xml
+++ b/app/src/main/res/values-af/strings.xml
@@ -50,7 +50,7 @@
SkakelsWeergawesMeer
- Nog opsies
+ Nog opsiesMinderTerugKanselleer
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 17e534f85..feacf6fe2 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -79,7 +79,7 @@
أُضيفت في %sالروابطالمزيد
- خيارات أكثر
+ خيارات أكثرتقليلارجعاستبدال
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index ee76a6798..e09f7ed6c 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -69,7 +69,7 @@
%1$s quraşdırıldıYükləDaha çox
- Digər seçimlər
+ Digər seçimlərDaha azBu tətbiq fəaliyyətinizi izləyir və bu barədə hesabat verirYeniləmə uğursuz oldu. İnternetə qoşulmusunuzmu?
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 6b1e76da5..75df0fde5 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -51,7 +51,7 @@
СпасылкіВерсііПадрабязней
- Дадатковыя параметры
+ Дадатковыя параметрыСхавацьНазадСкасаваць
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 39af4dd81..28d2686cf 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -174,7 +174,7 @@
НесъвместимоВсички останали хранилища са наред.Повече
- Още опции
+ Още опцииПо-малкоНастройкиБиткойн
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index a357f257e..7c86d0a1c 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -14,7 +14,7 @@
সক্ষম করোকমআরো
- আরও বিকল্প
+ আরও বিকল্পসংস্করণলিঙ্কযোগ করো
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index c3ed54487..576cec376 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -138,7 +138,7 @@
FlattrActualitzant dipòsitsMés
- Més opcions
+ Més opcionsMenysPermisosSuggerir actualitzacions a versions inestables
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index e290ee2b3..d265f5c76 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -123,7 +123,7 @@
Nestabilní aktualizaceDoporučovat aktualizace na nestabilní verzeVíce
- Další možnosti
+ Další možnostiMéněŠpatný otiskNastavení
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index ee2b90bee..336f9ffc5 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -40,7 +40,7 @@
TilføjLinksMere
- Flere valgmuligheder
+ Flere valgmulighederMindreTilbageAnnuller
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 8cd94499f..812f2ed73 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -154,7 +154,7 @@
Täglich nach Aktualisierungen suchenWöchentlich nach Aktualisierungen suchenMehr
- Mehr Optionen
+ Mehr OptionenWenigerKeine Apps installiert. \n \nEs sind Apps auf deinem Gerät installiert, aber keine davon ist von F-Droid abrufbar. Das könnte daran liegen, dass du deine Paketquellen aktualisieren musst oder diese tatsächlich keine deiner Apps beinhalten.Glückwunsch! \nDeine Apps sind auf dem neuesten Stand.
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index b67451b25..6c64df42e 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -86,7 +86,7 @@
Εγκαταστημένο (από άγνωστη πηγή)ΣύνδεσμοιΠερισσότερα
- Περισσότερες επιλογές
+ Περισσότερες επιλογέςΛιγότεραΕπιστροφήΛανθασμένο δακτυλικό αποτύπωμα
diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
index 1731b9776..36d0c6ca9 100644
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -430,7 +430,7 @@
This app tracks and reports your activityLessMore
- More options
+ More optionsDownload%1$s installedDownloading %1$s
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index d61859af2..88d80828b 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -804,7 +804,7 @@
Tabulaj, kartaj kaj plurpersonaj ludojNur kiam malfermi la aplikaĵonKontrolas pri ĝisdatigoj nur je starto • Aplikaĵoj povas iĝi neaktualaj
- Pliaj ebloj
+ Pliaj eblojMem-ĝisdatigoMontras sciigon post kiam la aplikaĵo estas ĝisdatiginta sin mem por ke vi povu facile restarti ĝin.%s estas ĝisdatigita. Frapetu por ekigi ĝin
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 363ee4d31..d05411f70 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -212,7 +212,7 @@
¿Quieres instalar una actualización de esta aplicación integrada? Tus datos no se perderán. La aplicación actualizada podrá acceder a:¿Quieres instalar una actualización de esta aplicación integrada? Tus datos no se perderán. No requiere ningún acceso especial.Más
- Más opciones
+ Más opcionesMenosBitcoinLitecoin
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 5b8f966b8..5b608a39c 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -44,7 +44,7 @@
LisaLingidRohkem
- Lisavalikud
+ LisavalikudVähemTagasiTühista
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index f8b27c723..5f89c7e40 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -218,7 +218,7 @@
Deskargak huts egin du!%1$sk hornitua.Gehiago
- Aukera gehiago
+ Aukera gehiagoGutxiago%2$s
\ndeskargatzen\n%1$s(e)tik
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index d15aa1f02..9b6ed690a 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -75,7 +75,7 @@
نصب شده (از منبع ناشناس)پیوندهابیشتر
- گزینههای بیشتر
+ گزینههای بیشترکمتربازگشتاثر انگشت بد
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 0b717313a..70dc07cdc 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -138,7 +138,7 @@
Valitse sovelluksetLinkitLisää
- Lisäasetukset
+ LisäasetuksetVähemmänLataa päivitykset automaattisestiPäivitykset ladataan automaattisesti ja sinua pyydetään asentamaan ne
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 9cfd057a4..240a085bf 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -215,7 +215,7 @@
Une erreur a eu lieu lors de la connexion à l\'appareil, impossible de faire l\'échange !TempsPlus
- Plus d\'options
+ Plus d\'optionsMoinsAutorisationsParamètres
diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml
index 4783abce6..ed9a8df85 100644
--- a/app/src/main/res/values-ga/strings.xml
+++ b/app/src/main/res/values-ga/strings.xml
@@ -873,7 +873,7 @@
Cluichí boird, cártaí agus cóisireNuair a osclaítear an aip amháinSeiceáil le haghaidh nuashonruithe ach amháin nuair a osclaítear iad • Beidh aipeanna as dáta
- Tuilleadh roghanna
+ Tuilleadh roghannaFéinnuashonrúTugann sé fógra nuair a rinneadh an aip a nuashonrú agus a dhúnadh í, ionas gur féidir leat í a sheoladh arís go héascaNuashonraíodh %s. Tapáil le hoscailt
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 59c558ff8..21eadd136 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -67,7 +67,7 @@
Instalado (dende unha fonte descoñecida)LigazónsMáis
- Máis opcións
+ Máis opciónsMenosVolverActivar
diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml
index 1c343ff5c..87dc753df 100644
--- a/app/src/main/res/values-gu/strings.xml
+++ b/app/src/main/res/values-gu/strings.xml
@@ -57,7 +57,7 @@
અનુપ્રયોગની વિગતો%1$s સ્થાપિત થઇ ગયેલ છેવધુ
- વધુ વિકલ્પો
+ વધુ વિકલ્પોઓછુસ્થાપિત કરોડાઉનલોડ થાય છે, %1$s
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index d151e645c..9f26b1ca3 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -217,7 +217,7 @@
Flattrההורדה נכשלה!עוד
- עוד אפשרויות
+ עוד אפשרויותפחותהגדרותמתבצעת הורדה
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index b3ac8d668..cea7fb637 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -36,7 +36,7 @@
जोड़ेसंधियांज़्यादा
- ज़्यादा विकल्प
+ ज़्यादा विकल्पकमपीछेरद्द करें
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index bfac81a1f..d8b301477 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -46,7 +46,7 @@
DodajPovezniceViše
- Više opcija
+ Više opcijaManjeNatragOdustani
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 1e086f52e..9fb7bafe2 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -81,7 +81,7 @@
Telepítve (ismeretlen forrásból)HivatkozásokTöbb
- További lehetőségek
+ További lehetőségekKevesebbVisszaRossz ujjlenyomat
diff --git a/app/src/main/res/values-hy/strings.xml b/app/src/main/res/values-hy/strings.xml
index e109561a4..2e18956dd 100644
--- a/app/src/main/res/values-hy/strings.xml
+++ b/app/src/main/res/values-hy/strings.xml
@@ -31,7 +31,7 @@
ՀղումներՏարբերակներԱվել
- Այլ ընտրանքներ
+ Այլ ընտրանքներՊակասԵտՉեղարկել
diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml
index 9cbfe96de..a14561028 100644
--- a/app/src/main/res/values-id/strings.xml
+++ b/app/src/main/res/values-id/strings.xml
@@ -34,7 +34,7 @@
TambahkanTautanLebih banyak
- Pilihan lainnya
+ Pilihan lainnyaLebih sedikitKembaliBatalkan
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index f52e8b786..7788f96ed 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -39,7 +39,7 @@
Bæta viðTenglarMeira
- Fleiri valkostir
+ Fleiri valkostirMinnaTil bakaHætta við
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 4e3edd746..13673998d 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -127,7 +127,7 @@
Installato (da %s)Installato (da fonte sconosciuta)Altro
- Altre opzioni
+ Altre opzioniIndietroRiduciImpronta digitale non corrispondente
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index ad919d67c..e11777563 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -214,7 +214,7 @@
Flattrリポジトリ詳細を表示
- その他のオプション
+ その他のオプション詳細を隠すダウンロード中
\n%2$s
diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml
index 656afd30a..95342d931 100644
--- a/app/src/main/res/values-ka/strings.xml
+++ b/app/src/main/res/values-ka/strings.xml
@@ -19,7 +19,7 @@
დაქეშილი აპების დატოვება%1$s-ის გადმოწერამეტი
- სხვა ვარიანტები
+ სხვა ვარიანტებინაკლებიწაშლაგანახლება
diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml
index 9aa2ff1e6..a1bfa94d7 100644
--- a/app/src/main/res/values-kk/strings.xml
+++ b/app/src/main/res/values-kk/strings.xml
@@ -198,7 +198,7 @@
АзырақМәтінТолығырақ
- Басқа опциялар
+ Басқа опциялар%s жойылмадыТаңбашаТек OLED экрандары үшін ұсынылады.
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 73785d90d..87c625792 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -140,7 +140,7 @@
호환되지 않음링크더 보기
- 추가 옵션
+ 추가 옵션덜 보기뒤로잘못된 지문
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index 25d43c138..1124b5c5c 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -92,7 +92,7 @@
%1$s ir uzstādītaLejupielādētVairāk
- Citas opcijas
+ Citas opcijasMazākNevar atjaunināt. Vai ierīcei ir savienojums ar internetu?Neparakstīts
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index da8554091..41e1e6257 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -173,7 +173,7 @@
Оваа апликација ја следи и јавува вашата активностПомалкуПовеќе
- Повеќе опции
+ Повеќе опцииАдреса на складиштетоИзберете Bluetooth метод за испраќањеНе е пронајден метод за испраќање преке Bluetooth, изберете еден!
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index b303a9495..ecdda225c 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -51,7 +51,7 @@
%1$s നെ സ്ഥാപിച്ചുഡൗൺലോഡ് ചെയ്യുകകൂടുതല്
- കൂടുതൽ ഓപ്ഷനുകൾ
+ കൂടുതൽ ഓപ്ഷനുകൾകുറവ്ഈ പ്രയോഗം നിങ്ങളുടെ പ്രവര്ത്തനങ്ങള് നിരീക്ഷിക്കുകയും പ്രസിദ്ധപ്പെടുത്തുകയും ചെയ്യുന്നുപുതുക്കാന് കഴിയുന്നില്ലേ, നിങ്ങൾ ഇന്റർനെറ്റിലേക്ക് കണക്റ്റുചെയ്തിട്ടുണ്ടോ?
diff --git a/app/src/main/res/values-mn/strings.xml b/app/src/main/res/values-mn/strings.xml
index a5493c8d7..193bdea0f 100644
--- a/app/src/main/res/values-mn/strings.xml
+++ b/app/src/main/res/values-mn/strings.xml
@@ -440,7 +440,7 @@
Энэ апп нь таны үйл ажиллагааг хянах, мэдээлэхБагаДэлгэрэнгүй
- Бусад сонголт
+ Бусад сонголтТатаж авах%1$s суулгасанТатаж байна %1$s
diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml
index a181c25d5..97218526a 100644
--- a/app/src/main/res/values-mr/strings.xml
+++ b/app/src/main/res/values-mr/strings.xml
@@ -329,7 +329,7 @@
होयकमीअधिक
- आणखी पर्याय
+ आणखी पर्यायशोधनाहीइंस्टॉल करत आहे…
diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml
index 445ed5cda..e40f3f555 100644
--- a/app/src/main/res/values-my/strings.xml
+++ b/app/src/main/res/values-my/strings.xml
@@ -41,7 +41,7 @@
ေပါင္းထည့္မည္လင့္ခ္ေနာက္ထပ္
- နောက်ထပ် ရွေးစရာများ
+ နောက်ထပ် ရွေးစရာများခ်ံဳမည္ေနာက္သို႔မလုပ်တော့
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index 0c06f1090..38bb72459 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -191,7 +191,7 @@
Sjekk etter oppdateringer annenhver ukeLystMer
- Flere alternativer
+ Flere alternativerMindreInnstillingerLaster ned\n%2$s fra\n%1$s
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 6bae2c23f..5d37faa6c 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -207,7 +207,7 @@
Instabiele updatesStel updates voor instabiele versies voorMeer
- Meer opties
+ Meer optiesMinderBitcoinLitecoin
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 1224150c4..4e67ad8d5 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -211,7 +211,7 @@
Pobieranie nie powiodło się!RepozytoriumWięcej
- Więcej opcji
+ Więcej opcjiMniejPobieranie\n%2$s z\n%1$sUprawnienia
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index d05718d98..a01f2a59a 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -202,7 +202,7 @@
FlattrFalha ao baixar!Mais
- Mais opções
+ Mais opçõesMenosBaixando\n%2$s de\n%1$sPermissões
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 0307d3821..82c0ffc05 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -197,7 +197,7 @@
Ocorreu um erro ao estabelecer a ligação e não será possível a troca!precisa de acesso aMais
- Mais opções
+ Mais opçõesMenosPermissõesQuer instalar uma atualização desta app? Os dados existentes não serão perdidos. A app atualizada terá acesso a:
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 96f7563bb..9853167a7 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -51,7 +51,7 @@
%1$s foi instaladoDescarregarMais
- Mais opções
+ Mais opçõesMenosEsta aplicação monitora e reporta a sua atividadeNão é possível atualizar. Você está conectado à Internet?
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index b026fa001..58a220e3c 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -54,7 +54,7 @@
Instalat (din sursă necunoscută)Adăugat pe %sMai mult
- Mai multe opțiuni
+ Mai multe opțiuniMai puținÎnapoiLegături
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 98c08ca1e..eeac5ea59 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -148,7 +148,7 @@
НастройкиНесовместимаПодробнее
- Дополнительные параметры
+ Дополнительные параметрыСвернутьНазадНеверный отпечаток
diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml
index cdb9ceb50..9e36a3a94 100644
--- a/app/src/main/res/values-si/strings.xml
+++ b/app/src/main/res/values-si/strings.xml
@@ -13,7 +13,7 @@
යාවත්කාලීනඑෆ්-ඩ්රොයිඩ්තව
- තවත් විකල්ප
+ තවත් විකල්පබාගන්න%1$s ස්ථාපිතයි%1$s බාගත වෙමින්
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index d4202c2b4..e8639d92d 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -207,7 +207,7 @@
Prihlasovacie meno neuvedené, prihlasovacie údaje sa nezmeniliLicenciaViac
- Ďalšie možnosti
+ Ďalšie možnostiMenejNastaveniaPoslať e-mail autorovi
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 6d2339d8e..36f784b2b 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -75,7 +75,7 @@
Prenašanje %1$s%1$s nameščenVeč
- Več možnosti
+ Več možnostiManjTa aplikacija vam sledi in poroča o vaši aktivnostiNi možno posodobiti, ste povezani na internet?
diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml
index bd006d826..6ddf86ec7 100644
--- a/app/src/main/res/values-sq/strings.xml
+++ b/app/src/main/res/values-sq/strings.xml
@@ -19,7 +19,7 @@
JoShtojeMë tepër
- Mundësi të tjera
+ Mundësi të tjeraMë pakMbrapshtAnuloje
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index f1c8793d7..8acb21dcd 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -191,7 +191,7 @@
Преузимање није успелоОмогућује %1$s.Више
- Још опција
+ Још опцијаМањеПреузимање\n%2$s од\n%1$sДозволе
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 3dcd8e57d..616e6bbc1 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -130,7 +130,7 @@
LänkarKällkodMer
- Fler alternativ
+ Fler alternativMindreBakåtFelaktigt fingeravtryck
diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml
index 85129446b..9131a1521 100644
--- a/app/src/main/res/values-sw/strings.xml
+++ b/app/src/main/res/values-sw/strings.xml
@@ -48,7 +48,7 @@
PakuaSasisha yoteZaidi
- Chaguo zaidi
+ Chaguo zaidiKidogoOngeza kiooMatoleo
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index c8757e912..51ee5e586 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -34,7 +34,7 @@
சேர்இணைப்புகள்மேலும்
- மேலும் விருப்பங்கள்
+ மேலும் விருப்பங்கள்குறைவானபின்செல்கரத்துசெய்
diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml
index 1b4dede6e..219ee5b5a 100644
--- a/app/src/main/res/values-te/strings.xml
+++ b/app/src/main/res/values-te/strings.xml
@@ -22,7 +22,7 @@
యాప్ వివరాలుF-డ్రాయిడ్ గురించిమరింత
- మరిన్ని ఆప్షన్లు
+ మరిన్ని ఆప్షన్లుWi-Fiక్రీడలు & ఆరోగ్యంతొలగించు
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 5c3acd980..e9b2927fe 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -32,7 +32,7 @@
เพิ่มลิ้งก์มากกว่า
- ตัวเลือกอื่น
+ ตัวเลือกอื่นน้อยกว่ากลับยกเลิก
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 09516cddf..6bb342134 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -140,7 +140,7 @@
UyumsuzBağlantılarDaha fazla
- Diğer seçenekler
+ Diğer seçeneklerDaha azGeriYanlış parmak izi
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index f2901ec9c..1a19cab24 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -201,7 +201,7 @@
Завантаження \n%2$s з \n%1$sПошук пакунків репозиторію в \n%1$sДокладніше
- Більше опцій
+ Більше опційЗгорнутиНалаштуванняДозволи
diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml
index 45b2048b1..535748d4a 100644
--- a/app/src/main/res/values-ur/strings.xml
+++ b/app/src/main/res/values-ur/strings.xml
@@ -93,7 +93,7 @@
یہ ایپ آپ کی سرگرمیوں کو ٹریک اور رپورٹ کرتی ہے۔کممزید
- مزید اختیارات
+ مزید اختیاراتڈاؤن لوڈ کریں%1$s انسٹال ہو گیاF-Droid کے بارے میں
diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml
index 8369bce02..a816b4a5d 100644
--- a/app/src/main/res/values-uz/strings.xml
+++ b/app/src/main/res/values-uz/strings.xml
@@ -2,7 +2,7 @@
YoʻqBatafsil
- Yana
+ YanaOzroqOchishYaqin atrofda
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 7bcc5a200..823be2a19 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -205,7 +205,7 @@
Tất cả kho khác không có lỗi.Giúp bạn của bạn vào điểm truy cậpMở rộng
- Lựa chọn khác
+ Lựa chọn khácThu gọnĐang tải\n%2$s từ\n%1$sQuyền
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 2c87cbc8f..a7d50d476 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -115,7 +115,7 @@
已安装(来自未知来源)链接展开
- 更多选项
+ 更多选项收起返回指纹无效
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 959bed720..1a7b2eede 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -174,7 +174,7 @@
新版的應用程式使用了一個不相同的鑰匙簽署。若要安裝新版本,你必須先卸載舊版本。(註:卸載將會刪除應用程式內的資料)下載失敗!顯示更多
- 更多選項
+ 更多選項顯示較少與裝置連接時發生了問題,未能進行交換!正在載入…
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index f940186ca..61ae46fa4 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -39,7 +39,7 @@
新增連結展開
- 更多選項
+ 更多選項摺疊返回取消
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2529c1861..e71195b8f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
-
+F-Droid
@@ -53,7 +53,6 @@
Filterselected
- CategoryHide anti-featuresFilterUse filters to only show apps from specific categories or repositories. You can also change the sort order.
@@ -107,79 +106,20 @@
by %sDelete
- Prompt to send crash reports
- Gather data about crashes and ask to send them to the
- developer
-
- Keep cached appsUpdates
- Unstable updates
-
- Suggest updates to unstable versions
- Hide all notifications
- Prevent all actions from showing in the status bar and notification
- drawer.
-
- Send install history
- Send %s Metrics report
- %s install history as CSV file
- %s Metrics report as JSON fileInstall history
- Install history and metrics
- %s Metrics report
- View the private log of all installs and uninstalls
- Keep install history
- Store a log of all installs and uninstalls in a private store
- Send usage data
- Sends anonymous data weekly to F-Droid Metrics (requires "Keep install history")
- The %s Metrics report is viewable in the install history viewer
- Send version and UUID to servers
- Include this app\'s version and a random, unique ID when
- downloading, takes effect next app restart.
- Force old index format
- In case there are bugs or compatibility issues, use the XML app index
- OtherDebug logCreates a file with logcat output useful for debugging. Careful, it could contain sensitive information!Logcat saved successfullyError saving logcat
- Automatic update interval
- Over Wi-Fi
-
- Over data
- Always use this connection when available
- Only use this connection when I click to download
- Never download anything using this connection
-
- Automatically download updates
-
- Updates are downloaded automatically and you are notified to install
- them
- Automatically install updates
- Download and update apps in the background, showing a notification
-
- Show available updates
-
- Show a notification when updates are available
- Force old installer
- Disable the Session Installer, including support for automatic background updates.
- Privileged Extension
- Use Privileged Extension to install, update, and remove packages
- Name of your Local Repo
- The advertised title of your Local Repo: %s
- Use encrypted HTTPS:// connection for Local Repo
- Scan removable storageScanning %s…
- Look for package repos on removable storage like SD Cards
- and USB thumb drives
- Use system colorsApply dynamic colors from your system settings
@@ -206,13 +146,6 @@
Proxy is expected in host:port formatInvalid proxy format
- Authentication required
- Username
- Password
- Change password
- Empty username, credentials not changed
-
- App detailsNo such app found.Repository
@@ -230,18 +163,6 @@
Prefer repositoryExpand repository list
- Buy the developers of %1$s a coffee!
- %1$s is created by %2$s. Buy them a coffee!
-
- Different signer to installed version
- To show incompatible versions here anyway, enable the \"%1$s\" setting.
- No versions with compatible signer
- No versions compatible with device
- The installed version is not compatible with any available versions. Uninstalling the app will enable you to view and install compatible versions.
-
-This often occurs with apps installed via Google Play or other sources, if they are signed by a different certificate.
-
- This might also be interesting to you:More apps by %1$sWhat are Anti-Features?
@@ -253,7 +174,6 @@ This often occurs with apps installed via Google Play or other sources, if they
Apps on F-Droid are free and open source software (FOSS), they respect your privacy, and they\'re available for anyone to see the source code, build on and improve.\n\nApp distribution is transparent, privacy-respecting, and accountable, setting a standard that challenges the mobile ecosystem to do better.\n\nF-Droid\'s impact is measured not just in apps published, but in the freedom, trust, and digital rights it helps protect.\n\nHelp F-Droid to protect community-powered innovation and continue our mission of openness, privacy, and user empowerment.This program is Free Software. You can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.Version
- WebsiteSupport forumSource codeLicense
@@ -269,20 +189,9 @@ This often occurs with apps installed via Google Play or other sources, if they
SuggestedIncompatibleInstalled
- New
- Not installedInstalled (from %s)Installed (from unknown source)
- Version %1$s available
- Version %1$s
- Version %1$s (recommended)
- Added on %s
- Cancel downloadUpdate
- File installed to %s
- F-Droid needs the storage permission to install this to storage. Please allow it on the next screen to proceed with installation.
- Size: %1$s
- Could not launch app.Copy linkThis app is not compatible with your deviceCan\'t update this app, because all versions have an incompatible signature\n\nIf you don\'t receive updates through other means, you may need to uninstall and then reinstall this app. The app\'s data will be lost.
@@ -309,43 +218,14 @@ This often occurs with apps installed via Google Play or other sources, if they
Signature mismatch, can\'t installSorry! There was an unexpected error installing this app
- Some apps are hidden based on your Anti-Features settings. Touch to change.
- No version recommended for installation.
- Downloading %1$s
- %1$s installed
- Downloaded, ready to install
- Update ignored
- Vulnerability ignored
- Download canceled
-
Installed appsCongratulations 🥳All your apps are up to dateShare installed apps
- Apps installed by F-Droid as CSV file
- Updates ignored
- Updates ignored for version %1$s
- We found a vulnerability with %1$s. Touch to read more in the Anti-Features section.
- We found a vulnerability with %1$s. We recommend updating to the newest version immediately.
- Ignore
-
-
- DownloadUpdate all
-
- Hide apps
-
- Show apps
-
- Download update for %1$d app:
- Download updates for %1$d apps:
-
-
- All updates disabled by data/Wi-Fi settingsOK
@@ -360,62 +240,42 @@ This often occurs with apps installed via Google Play or other sources, if they
LinksVersionsMore
- More options
+ More optionsLessBackCancel
- Enable
- Add key
- OverwriteClear searchSort search
- No Bluetooth send method found, choose one!
- Choose Bluetooth send method
-
A repository is an additional source of apps. Third-party ones you add here have different standards than those providing apps built by F-Droid itself.\n\nPlease ensure that the repository you are adding is trustworthy.Scan QR codeFetching repository…Adding repository…Enter repository URL manually
- Repository address
- Fingerprint (optional)Included apps:This repository was already added.
- %1$s is already setup, this will add new key information.
- %1$s is already setup, confirm that you want to re-enable it.
- %1$s is already setup and enabled.
- First delete %1$s in order to add this with a conflicting key.
- This is a copy of %1$s, add it as a mirror?Invalid repository.\n\nContact the maintainer and let them know about the issue.Error connecting to the repository.Archive repositories can not be added directly. Tap the repository in the list and enable the archive there.
- Could not find repo address in shared text.This mirror was already added (%s).The URL you are trying to add (%s) is a mirror of an existing repository.The URL you are trying to add (%s) is a mirror of a new repository. Both the original repository and the mirror will be added.Bad fingerprint
- This is not a valid URL.Ignoring malformed repo URI: %sYour device admin doesn\'t allow installing apps from unknown sources, that includes new reposUnknown sources can\'t be added by this user, that includes new repos
- Repositories
- Last update: %snever
- Add additional sources of appsSettingsSearchNew repository
-
- OpenOpen
@@ -425,7 +285,6 @@ This often occurs with apps installed via Google Play or other sources, if they
InstallUninstall
- Select for wipeIgnore all updatesIgnore this updateAllow beta updates
@@ -446,118 +305,32 @@ This often occurs with apps installed via Google Play or other sources, if they
UpdateBitcoinLitecoin
-
- Flattr
-
- Liberapay
-
- OpenCollective
-
- Latest
-
+
Categories
-
+
Nearby
-
- Updates
-
- Show metrics report
-
- Show install history
-
- No recent apps found
- Upgrading database…
- Once your list of apps has been updated, the latest apps should
- show here
-
- Once you enable a repository and let it update, the latest apps
- should show here
-
-
- No categories to display
-
- My apps
- Manage installed apps
-
-
- Not installed
- New in version %s
-
- This app has features you may not like.
- Anti-features
- Ads
- Tracking
- Non-Free Addons
- Non-Free Network Services
- Tethered Network Services
- Non-Free Dependencies
- Non-Free Assets
- Signed Using An Unsafe Algorithm
- Known Vulnerability
- Source Code No Longer Available
- Not Safe for Work
- Other Anti-Features
- This app contains advertising
- This app tracks and reports your activity
- This app promotes non-free add-ons
- This app promotes or depends entirely on a non-free network service
- This app depends entirely on a certain instance of a network service
- This app depends on other non-free apps
- This app contains non-free assets
- This app has a weak security signatureThis app contains a known security vulnerability
- The source code is no longer available, no updates possible
- This app contains content that should not be publicized or visible everywhereDisplay
- Expert mode
- Show extra info and enable extra settings
-
- Search appsSearch installed apps
- App compatibility
- Include incompatible versions
- Show app versions that are incompatible with the device
- Include Anti-Features
- Show apps that have known Anti-Features
- Include touchscreen apps
- Show apps that require touchscreen regardless of hardware support
-
- Local RepoF-Droid is ready to swapTouch to view details and allow others to swap your apps.Deleting current repo…
- Adding %s to repo…
- Writing signed index file (index.jar)…Linking APKs into the repo…
- Copying app icons into the repo…IconNext
- SkipTry againUse DNS cacheUse cached results to minimize DNS queries.
- Prefer foreign mirrors
- Try mirrors that are located outside your country first, e.g. if foreign protections are stronger.
- Use Tor
- Force download traffic through Tor for increased privacy. Requires Orbot
- Proxy
- Enable HTTP proxy
- Configure HTTP proxy for all network requests
- Proxy host
- Your proxy\'s hostname (e.g. 127.0.0.1)
- Proxy port
- Your proxy\'s port number (e.g. 8118)PrivacyPrevent screenshotsBlocks screenshots from being taken and hides app content from recent apps screenPanic button app
- an unknown appNo app has been setNoneConfirm panic app
@@ -570,82 +343,30 @@ This often occurs with apps installed via Google Play or other sources, if they
Exit appThis app will be closedDestructive actions
- Will be uninstalled and all data deleted
- Apps to be uninstalled and all data wiped
- Add apps to be uninstalled and wiped
- Hide %s
- App will hide itself
- Remember how to restore
- In a panic event, this will remove %1$s from the launcher. Only typing \"%2$d\" in the fake %3$s app can restore it.Reset reposForce the repo setup back to defaults
-
- Calculator
- Hide %s now
- Are you sure you want to remove %1$s from the launcher? Only typing \"%2$d\" in the fake %3$s app can restore it.
- Warning: Any app shortcut on the home screen will also be removed and needs to be re-added manually.
- Hide with search button
- Long pressing the search button will hide the app
-
-
- Downloading\n%2$s / %3$s (%4$d%%) from\n%1$sDownloading at %s/s%1$s remaining
-
- Downloading\n%2$s from\n%1$s
- The requested file was not found.Updating repositories
- Updating appsNo internet
- No data or Wi-Fi enabled
- Processing %2$s / %3$s (%4$d%%) from %1$s
- Connecting to\n%1$s
- Saving app details
- Saving app details (%1$d/%2$d) from %3$s
- All repositories are up to date
- All other repos didn\'t create errors.
- Error during update: %s
- Cannot update, are you connected to the internet?
-
- No permissions
-
- Permissions
- App has unexpected permission: %s
- You don\'t have any available app that can handle %s.You don\'t have any available app that can handle this.Theme
- Use pure black background in dark theme
- Recommended only for OLED screens.
- Unsigned
- UnverifiedRepository listA repository is a source of apps. This list shows all currently added repositories. Disabled repositories are not used.\n\nIf an app is in more than one repository, the repository higher in the list is automatically preferred. You can reorder repositories by long pressing and dragging them.RepositoryA repository is a source of apps.\n\nMirrors are used to distribute the load of downloading apps across multiple servers. Mirrors closer to you may be faster.\n\nOfficial mirrors are defined by the repository owner. They cannot be deleted, only disabled. You can define additional custom mirrors by adding them just like a normal repository.Show apps
- Repository archiveShow archived apps and outdated versions of appsThis repository does not appear to have an archive.Check for archiveSigning key fingerprint (SHA-256)
- DescriptionOfficial mirrorsUser mirrors
@@ -653,10 +374,7 @@ This often occurs with apps installed via Google Play or other sources, if they
1 app%d apps
- Last repo update: %sLast check for repo update: %s
- Unknown
- Archive repo currently not availableDelete repository?Deleting a repository means
apps from it will no longer be available.\n\nNote: All
@@ -664,23 +382,15 @@ This often occurs with apps installed via Google Play or other sources, if they
Delete mirror?You can continue to install apps from this repository using the other mirrors.
- Disabled "%1$s".\n\nYou will
- need to re-enable this repository to install apps from it.
- Disable repositoryIf you\'ve set this repository as preferred for any apps, that preference will be lostAll apps from this repository will disappear. Already installed apps won\'t be affected.Disable
- Saved package repository %1$s.
- Looking for package repository at\n%1$sShare repositoryShare mirrorShow repository QR codeBasic auth
- Username: %s
- Password: ***
- EditUpdate errors
@@ -695,51 +405,11 @@ This often occurs with apps installed via Google Play or other sources, if they
Your device is not on the same Wi-Fi as the Local Repo you just added! Try joining
this network: %s
- Requires: %1$s
- targets %sLanguage
- System defaultWi-FiHotspot
- Connectivity
- Development
- Games
- Graphics
- Internet
- Money
- Multimedia
- Navigation
- Phone & SMS
- Reading
- Science & Education
- Security
- Sports & Health
- System
- Theming
- Time
- Writing
- nightly
-
-
- View %d
- View all %d
-
-
- No apps installed.\n\nThere are apps on your device, but they are not
- available from F-Droid. This could be because you need to update your repositories, or the repositories
- genuinely don\'t have your apps available.
-
- Congratulations!\nYour apps are up to date.
-
- No matching applications available.
-
- Failed to install due to an unknown error
- Failed to uninstall due to an unknown error
-
- Could not share app file
-
No internet? Get apps from people near you!Find people nearby
@@ -766,14 +436,11 @@ This often occurs with apps installed via Google Play or other sources, if they
Visible via hotspot(blank)(hidden)
- Setting up hotspot…
- Stopping hotspot…Tap to open available networksTap to switch to a Wi-Fi networkOpen QR scannerWelcome to F-Droid!Do you want to get apps from %1$s now?
- Don\'t show this againOne person needs to scan the code, or type the URL of the other in a browser.
@@ -803,99 +470,34 @@ This often occurs with apps installed via Google Play or other sources, if they
Send F-DroidApps
- Could not find people nearby to swap with.ConnectingConfirm swapThe QR code you scanned doesn\'t look like a swap code.Use Bluetooth
- Loading…
- Error occurred while connecting to device, can\'t swap with it!You\'re trying to connect to someone with an outdated version of F-Droid. To connect they have to upgrade their F-Droid app. They can use swap to update their F-Droid app from you.
- Nearby not enabled
- Before swapping with nearby devices, make your device visible.Using %1$s
- That choice did not match any removable storage devices, try
- again!
- Choose your removable SD card or USBInvalid URL for swapping: %1$sWi-Fi hotspot enabledCould not enable Wi-Fi hotspot!Nearby closed since it was idle.
- needs access to
- Do you want to install an update
- to this existing app? Your existing data will not
- be lost. The updated app will get access to:
-
- Do you want to install an update
- to this built-in app? Your existing data will not
- be lost. The updated app will get access to:
-
- Do you want to install an update
- to this existing app? Your existing data will not
- be lost. It does not require any special access.
-
- Do you want to install an update
- to this built-in app? Your existing data will not
- be lost. It does not require any special access.
-
- New
- All
- This may cost you money
- Do you want to replace this app with the factory version? All data will be removed.
- Do you want to uninstall this app?
- Download failed!
- Waiting to start download…Error installing %s
- Error uninstalling %s
-
- New:
- Provided by %1$s.
- Downloading…
- Downloading, %1$d%% completeInstalling…
-
- Uninstalling…
-
- No automatic app updates
-
- Check for updates hourly
-
- Check for updates every 4 hours
-
- Check for updates every 12 hours
-
- Check for updates daily
-
- Check for updates weekly
-
- Check for updates every 2 weeks
+
Earliest next update: %sNext update when conditions are fulfilled
- 1 hour
- 1 day
- 1 week
- 1 month
- 1 year
- Forever
-
LightDarkFollow systemF-Droid has crashedF-Droid had crashed
- An unexpected error occurred
- forcing the app to stop. Would you like to e-mail the
- details to help fix the issue?
-
- You can add extra information and comments here:An unexpected error occurred.
This is not your fault.
@@ -909,44 +511,14 @@ This often occurs with apps installed via Google Play or other sources, if they
Error saving crash report to file
-
- +%1$d more…
- +%1$d more…
- Update available
- Ready to install
- Update ready to install
- Install failed
- Downloading \"%1$s\"…
- Downloading update for \"%1$s\"…
- Installing \"%1$s\"…
- Successfully installed
-
-
- %1$d update
- %1$d updates
- %1$d update available%1$d updates available
-
- %1$d app installed
- %1$d apps installed
-
- Tap to update all
- Update available
- Downloading…
- Downloading update…
- Ready to install
- Update ready to installInstalling
- Successfully installedInstall failed
- Update
- Cancel
- InstallInstallationsDisplays app installation notifications.
@@ -989,53 +561,16 @@ This often occurs with apps installed via Google Play or other sources, if they
%s was updated. Tap to open
-
- Category %1$s
- Repository %1$s
- Author %1$s
-
-
- View %1$d app in the %2$s category
- View all %1$d apps from the %2$s category
-
-
- Updated todayYour camera doesn\'t seem to have an autofocus. It might be difficult to scan the code.Grant camera permission to scan the QR code. Tap to grant camera permission in settings.
- This app was built for an older version of Android and cannot be updated automatically.
- The selected repository has no compatible app versions. To receive updates, select a preferred repository with compatible versions.
- UndoError
- Installation cancelledTurn on Wi-Fi
-
- Updated %1$d day ago
- Updated %1$d days ago
-
-
- Updated %1$d week ago
- Updated %1$d weeks ago
-
-
- Updated %1$d month ago
- Updated %1$d months ago
-
-
- Updated %1$d year ago
- Updated %1$d years ago
- Copied to clipboard
- Copied URL to clipboard
- Copied permission name to clipboardPasteIPFS gatewaysDownload apps from IPFS web endpoints. (Only works for F-Droid repositories with IPFS support.)
-
- Download apps using IPFS web endpoints: %1$d enabled
-
- Download apps using IPFS web endpoints: disabledDownload apps using IPFS web endpointsAdd IPFS gatewayAdd
diff --git a/legacy/src/main/res/xml/backup_extraction_rules.xml b/app/src/main/res/xml/backup_extraction_rules.xml
similarity index 100%
rename from legacy/src/main/res/xml/backup_extraction_rules.xml
rename to app/src/main/res/xml/backup_extraction_rules.xml
diff --git a/legacy/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
similarity index 100%
rename from legacy/src/main/res/xml/backup_rules.xml
rename to app/src/main/res/xml/backup_rules.xml
diff --git a/app/src/test/java/org/fdroid/updates/UpdateInstallerTest.kt b/app/src/test/java/org/fdroid/updates/UpdateInstallerTest.kt
index 31a3c78a5..6f217db8f 100644
--- a/app/src/test/java/org/fdroid/updates/UpdateInstallerTest.kt
+++ b/app/src/test/java/org/fdroid/updates/UpdateInstallerTest.kt
@@ -1,12 +1,14 @@
package org.fdroid.updates
import android.content.Context
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
+import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
@@ -25,6 +27,7 @@ import org.fdroid.download.PackageName
import org.fdroid.index.RepoManager
import org.fdroid.install.AppInstallManager
import org.fdroid.ui.apps.AppUpdateItem
+import org.fdroid.ui.utils.isAppInForeground
import org.junit.Before
import org.junit.Test
@@ -47,6 +50,7 @@ internal class UpdateInstallerTest {
@Before
fun setUp() {
+ mockkStatic("org.fdroid.ui.utils.UiUtilsKt")
every { context.packageName } returns OWN_PACKAGE_NAME
every { db.getAppDao() } returns appDao
}
@@ -166,6 +170,7 @@ internal class UpdateInstallerTest {
),
makeAppUpdateItem(packageName = otherPkg),
)
+ every { context.isAppInForeground() } returns true
createUpdateInstaller().updateAll(updates, canAskPreApprovalNow = false)
advanceUntilIdle()
@@ -178,6 +183,11 @@ internal class UpdateInstallerTest {
currentVersionName = "2.0",
lastUpdated = 9999L,
)
+ context.packageManager.setComponentEnabledSetting(
+ any(),
+ COMPONENT_ENABLED_STATE_ENABLED,
+ any(),
+ )
}
coVerifyOrder {
diff --git a/config/checkstyle/checkstyle.gradle b/config/checkstyle/checkstyle.gradle
deleted file mode 100644
index 22e93b2e2..000000000
--- a/config/checkstyle/checkstyle.gradle
+++ /dev/null
@@ -1,13 +0,0 @@
-apply plugin: 'checkstyle'
-
-checkstyle {
- toolVersion = '10.12.0'
-}
-
-task checkstyle(type: Checkstyle) {
- configFile file("${project.rootDir}/config/checkstyle/checkstyle.xml")
- source 'src/main/java', 'src/test/java', 'src/androidTest/java'
- include '**/*.java'
-
- classpath = files()
-}
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
deleted file mode 100644
index 2cac3af52..000000000
--- a/config/checkstyle/checkstyle.xml
+++ /dev/null
@@ -1,150 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
deleted file mode 100644
index c52362723..000000000
--- a/config/checkstyle/suppressions.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/config/errorprone.gradle b/config/errorprone.gradle
deleted file mode 100644
index b2e44c20c..000000000
--- a/config/errorprone.gradle
+++ /dev/null
@@ -1,24 +0,0 @@
-import net.ltgt.gradle.errorprone.CheckSeverity
-
-dependencies {
- errorprone("com.google.errorprone:error_prone_core:2.24.1")
-}
-
-tasks.withType(JavaCompile).configureEach {
- options.errorprone {
- disableWarningsInGeneratedCode = true
- excludedPaths = '.*/cc/mvdan/accesspoint/.*'
- check("CatchFail", CheckSeverity.OFF)
- check("ClassCanBeStatic", CheckSeverity.OFF)
- check("DateFormatConstant", CheckSeverity.OFF)
- check("DefaultCharset", CheckSeverity.OFF)
- check("FormatString", CheckSeverity.OFF)
- check("JavaLangClash", CheckSeverity.OFF)
- check("MissingCasesInEnumSwitch", CheckSeverity.OFF)
- check("MissingOverride", CheckSeverity.OFF)
- check("NonAtomicVolatileUpdate", CheckSeverity.OFF)
- check("OperatorPrecedence", CheckSeverity.OFF)
- check("StringSplitter", CheckSeverity.OFF)
- check("UnsynchronizedOverridesSynchronized", CheckSeverity.OFF)
- }
-}
diff --git a/config/nightly-repo/repo.xml b/config/nightly-repo/repo.xml
deleted file mode 100644
index 71db2bf5a..000000000
--- a/config/nightly-repo/repo.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
- This repo is nightly builds of F-Droid, it should only be used for testing!
-
- 13
-
- 1
-
- ignore
-
-
- 308201e53082014ea0030201020204503d3768300d06092a864886f70d01010505003037310b30090603550406130255533110300e060355040a1307416e64726f6964311630140603550403130d416e64726f6964204465627567301e170d3132303832383231323630305a170d3432303832313231323630305a3037310b30090603550406130255533110300e060355040a1307416e64726f6964311630140603550403130d416e64726f696420446562756730819f300d06092a864886f70d010101050003818d0030818902818100b5ba553eacbc4de5b45af812d9695140dafbc0a8a9c13ac9a7e24b2665371ce5072e5dfef60f705d58fdb2d0e2190264e42d83a6fd80cfd54690e9e3c3735fa8dce684ee99ac879b1b11e1c8a9cbb9dc6b23064b025f9db7dc87d48ee4bc038affd80d854c0ed5d88d93d6e8127e62344727e23886b97f5d10e2265c9c9b5bd10203010001300d06092a864886f70d0101050500038181006dae218bdbff79801b1935448c663319843a7b2eb5f5c8837f010e58da25ba4d23bc6650b53c93f9c42b379299f4659b4cc3c505aa1a7c08c8a1a58fffe78d29df2cf69b27c34a0ab5f44cf7e323e34f8252d9f6e4d67171ce38bab64623910811dae6b12203385b32d962dbd51e8a6b0dcab3fa4d1f4020cee69a5f3c6ddf69
-
-
-
-
-
diff --git a/config/pmd/pmd.gradle b/config/pmd/pmd.gradle
deleted file mode 100644
index b25d4dd24..000000000
--- a/config/pmd/pmd.gradle
+++ /dev/null
@@ -1,26 +0,0 @@
-apply plugin: 'pmd'
-
-pmd {
- toolVersion = '7.17.0'
- consoleOutput = true
-}
-
-task pmdMain(type: Pmd) {
- dependsOn 'assembleDebug'
- ruleSetFiles = files("${project.rootDir}/config/pmd/rules.xml", "${project.rootDir}/config/pmd/rules-main.xml")
- ruleSets = [] // otherwise defaults clash with the list in rules.xml
- source 'src/main/java'
- include '**/*.java'
- exclude '**/vendored/**/*.java'
-}
-
-task pmdTest(type: Pmd) {
- dependsOn 'assembleDebug'
- ruleSetFiles = files("${project.rootDir}/config/pmd/rules.xml", "${project.rootDir}/config/pmd/rules-test.xml")
- ruleSets = [] // otherwise defaults clash with the list in rules.xml
- source 'src/test/java', 'src/androidTest/java'
- include '**/*.java'
- exclude '**/vendored/**/*.java'
-}
-
-task pmd(dependsOn: [pmdMain, pmdTest]) {}
diff --git a/config/pmd/rules-main.xml b/config/pmd/rules-main.xml
deleted file mode 100644
index 5a66d5b8b..000000000
--- a/config/pmd/rules-main.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- Rules for the project code aka "main".
-
-
-
-
-
diff --git a/config/pmd/rules-test.xml b/config/pmd/rules-test.xml
deleted file mode 100644
index 3ae045bec..000000000
--- a/config/pmd/rules-test.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Rules for the test harness code aka "androidTest" and "test".
-
-
-
-
-
-
-
-
-
diff --git a/config/pmd/rules.xml b/config/pmd/rules.xml
deleted file mode 100644
index 8cbe65608..000000000
--- a/config/pmd/rules.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
- Rules for the whole project
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/fastlane b/fastlane
new file mode 160000
index 000000000..55dc705cf
--- /dev/null
+++ b/fastlane
@@ -0,0 +1 @@
+Subproject commit 55dc705cf107cd758777c578ea18df65d71d5e5d
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3988669a9..b530ec483 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -128,6 +128,7 @@ androidx-compose-material3 = { module = "androidx.compose.material3:material3",
androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "material3AdaptiveNav" }
androidx-compose-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
+hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidxHiltCompiler" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 9367bbcfe..0ba685cc8 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -1848,6 +1848,11 @@
+
+
+
+
+
@@ -3873,6 +3878,11 @@
+
+
+
+
+
@@ -4328,6 +4338,11 @@
+
+
+
+
+
diff --git a/legacy/build.gradle b/legacy/build.gradle
deleted file mode 100644
index 8dfb58968..000000000
--- a/legacy/build.gradle
+++ /dev/null
@@ -1,267 +0,0 @@
-import com.android.build.api.variant.ResValue
-import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-
-plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.plugin.compose'
-}
-
-// add -Pstrict.release to the gradle command line to enable
-if (project.hasProperty('strict.release')) {
- println "Running strict release"
-} else {
- apply from: '../config/checkstyle/checkstyle.gradle'
- apply from: '../config/pmd/pmd.gradle'
-}
-
-// yes, this actually needs both quotes https://stackoverflow.com/a/41391841
-def privilegedExtensionApplicationId = '"org.fdroid.fdroid.privileged"'
-
-android {
- namespace "org.fdroid.fdroid"
- buildToolsVersion "35.0.0"
-
- compileSdk libs.versions.compileSdk.get().toInteger()
-
- defaultConfig {
- versionCode 1023051
- versionName "1.23.1"
- applicationId "org.fdroid"
-
- testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
- minSdkVersion 24
- targetSdkVersion 31
- /*
- The Android Testing Support Library collects analytics to continuously improve the testing
- experience. More specifically, it uploads a hash of the package name of the application
- under test for each invocation. If you do not wish to upload this data, you can opt-out by
- passing the following argument to the test runner: disableAnalytics "true".
- */
- testInstrumentationRunnerArguments disableAnalytics: 'true'
- vectorDrawables.useSupportLibrary = true
- }
-
- ext {
- APP_NAME = "@string/app_name"
- APP_NAME_DEBUG = "@string/app_name_debug"
- }
-
- buildTypes {
- // use proguard on debug too since we have unknowingly broken
- // release builds before.
- configureEach {
- manifestPlaceholders = [applicationLabel: APP_NAME]
- minifyEnabled true
- shrinkResources true
- buildConfigField "String", "PRIVILEGED_EXTENSION_PACKAGE_NAME", privilegedExtensionApplicationId
- buildConfigField "String", "ACRA_REPORT_EMAIL", '"reports@f-droid.org"'
- // String needs both quotes
- buildConfigField "String", "ACRA_REPORT_FILE_NAME", '"ACRA-report.stacktrace.json"'
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- testProguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro'
- }
- debug {
- getIsDefault().set(true)
- minifyEnabled false
- shrinkResources false
- manifestPlaceholders = [applicationLabel: APP_NAME_DEBUG]
- applicationIdSuffix ".debug"
- versionNameSuffix "-debug"
- // testProguardFiles gets partially ignored for instrumentation tests
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro'
- println 'buildTypes.debug defaultConfig.versionCode ' + defaultConfig.versionCode
- }
- }
-
- flavorDimensions "base"
- productFlavors {
- full {
- getIsDefault().set(true)
- dimension "base"
- applicationIdSuffix ".fdroid"
- }
- basic {
- dimension "base"
- targetSdkVersion 36
- applicationIdSuffix ".basic"
- }
- }
-
- androidComponents {
- onVariants(selector().all()) { variant ->
- variant.resValues.put(
- variant.makeResValueKey("string", "applicationId"),
- new ResValue(variant.applicationId.get(), null)
- )
- }
- }
-
- compileOptions {
- compileOptions.encoding = "UTF-8"
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
- }
-
- kotlin {
- compilerOptions {
- jvmTarget = JvmTarget.JVM_17
- }
- }
-
- aaptOptions {
- cruncherEnabled = false
- }
-
- buildFeatures {
- buildConfig true
- resValues = true
- compose true
- aidl true
- }
-
- testOptions {
- unitTests {
- includeAndroidResources = true
- // prevent tests from dying on android.util.Log calls
- returnDefaultValues = true
- all {
- // All the usual Gradle options.
- testLogging {
- events "skipped", "failed", "standardOut", "standardError"
- showStandardStreams = true
- }
- systemProperty 'robolectric.dependency.repo.url', 'https://repo1.maven.org/maven2'
- }
- }
- }
-
- sourceSets {
- test {
- java.srcDirs += "$projectDir/src/testShared/java"
- }
-
- androidTest {
- java.srcDirs += "$projectDir/src/testShared/java"
- }
- }
-
- lintOptions {
- checkReleaseBuilds false
- abortOnError true
-
- htmlReport true
- xmlReport false
- textReport false
-
- lintConfig file("lint.xml")
- }
-
- /* Do not include files that are not verified by the JAR signature */
- packagingOptions {
- // this can be removed once JAR/APKv1 signatures are no longer supported
- exclude 'META-INF/**/*.properties'
- exclude 'META-INF/*.version'
- exclude 'META-INF/COPYRIGHT'
- exclude 'META-INF/INDEX.LIST'
- exclude 'META-INF/LICENSE'
- exclude 'META-INF/LICENSE.txt'
- exclude 'META-INF/LICENSE.md'
- exclude 'META-INF/LICENSE-notice.md'
- exclude 'META-INF/NOTICE'
- exclude 'META-INF/NOTICE.txt'
- exclude 'META-INF/gradle/**'
- exclude '.readme'
- }
-}
-
-dependencies {
- implementation project(":libs:download")
- implementation project(":libs:index")
- implementation project(":libs:database")
- implementation libs.androidx.appcompat
- implementation libs.androidx.preference.ktx
- implementation libs.androidx.gridlayout
- implementation libs.androidx.recyclerview
- implementation libs.androidx.vectordrawable
- implementation libs.androidx.constraintlayout
- implementation libs.androidx.swiperefreshlayout
- implementation libs.androidx.lifecycle.livedata.ktx
- implementation libs.androidx.documentfile
- implementation libs.androidx.localbroadcastmanager
- implementation libs.androidx.work.runtime
- implementation libs.guava // somehow needed for work-runtime to function
-
- implementation libs.material
-
- //noinspection UseTomlInstead
- implementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false }
- implementation libs.zxing.core
- implementation libs.guardianproject.netcipher
- implementation libs.commons.io
- implementation libs.commons.net
- implementation libs.acra.mail
- implementation libs.acra.dialog
- implementation libs.adapterdelegates4
- implementation libs.slf4j.api
- implementation libs.logback.android
- implementation libs.microutils.kotlin.logging
-
- implementation libs.rxjava
- implementation libs.rxandroid
-
- implementation libs.glide
- implementation libs.glide.compose
- annotationProcessor libs.glide.compiler
-
- implementation libs.okhttp
- implementation libs.bcprov.jdk15to18
- fullImplementation libs.guardianproject.panic
- fullImplementation libs.bcpkix.jdk15to18
- fullImplementation libs.jmdns
- fullImplementation libs.nanohttpd
-
- implementation platform(libs.androidx.compose.bom)
- implementation libs.androidx.compose.material3
- implementation libs.androidx.compose.material.icons.extended
- implementation libs.androidx.lifecycle.viewmodel.compose
- implementation libs.androidx.compose.ui.tooling.preview
- implementation libs.androidx.activity.compose
- implementation libs.accompanist.drawablepainter
- debugImplementation libs.androidx.compose.ui.tooling
-
- testImplementation libs.androidx.test.core
- testImplementation libs.junit
- testImplementation libs.robolectric
- testImplementation libs.mockk
- testImplementation libs.mockito.core
- testImplementation libs.turbine
- testImplementation libs.hamcrest
- testImplementation libs.slf4j.simple
-
- androidTestImplementation libs.androidx.test.core
- androidTestImplementation libs.androidx.core.testing
- androidTestImplementation libs.androidx.test.runner
- androidTestImplementation libs.androidx.test.rules
- androidTestImplementation libs.androidx.test.ext.junit
- androidTestImplementation libs.androidx.test.ext.junit.ktx
- androidTestImplementation libs.androidx.test.monitor
- androidTestImplementation libs.androidx.espresso.core
- androidTestImplementation libs.androidx.test.uiautomator
- androidTestImplementation libs.androidx.work.testing
- androidTestImplementation libs.kotlin.test
- androidTestImplementation libs.kotlin.reflect
- androidTestImplementation libs.mockk.android
- androidTestImplementation libs.turbine
-}
-
-// org.fdroid.fdroid.updater.UpdateServiceTest needs app-full-debug.apk
-android.productFlavors.all { flavor ->
- if (flavor.name == "full") {
- project.afterEvaluate { project ->
- def dep = tasks.getByName("assembleFullDebug")
- project.tasks.withType(Test) { task ->
- task.dependsOn dep
- }
- }
- }
-}
diff --git a/legacy/lint.xml b/legacy/lint.xml
deleted file mode 100644
index f4b6952d4..000000000
--- a/legacy/lint.xml
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/legacy/proguard-rules.pro b/legacy/proguard-rules.pro
deleted file mode 100644
index 0b41eb204..000000000
--- a/legacy/proguard-rules.pro
+++ /dev/null
@@ -1,59 +0,0 @@
--dontobfuscate
--dontoptimize
--keepattributes SourceFile,LineNumberTable,Exceptions
--keep class org.fdroid.fdroid.** {*;}
--keep class kotlin.LazyKt
--dontwarn android.test.**
-
--dontwarn javax.naming.**
--dontnote org.apache.http.**
--dontnote android.net.http.**
--dontnote **ILicensingService
-
-# Needed for espresso https://stackoverflow.com/a/21706087
--dontwarn org.xmlpull.v1.**
-
-# StrongHttpsClient and its support classes are totally unused, so the
-# ch.boye.httpclientandroidlib.** classes are also unneeded
--dontwarn info.guardianproject.netcipher.client.**
-
-# These libraries are known to break if minification is enabled on them. They
-# use reflection to instantiate classes, for example. If the keep flags are
-# removed, proguard will strip classes which are required, which may result in
-# crashes.
--keep class kellinwood.security.zipsigner.** {*;}
--keep class org.bouncycastle.** {*;}
-
-# This keeps class members used for SystemInstaller IPC.
-# Reference: https://gitlab.com/fdroid/fdroidclient/issues/79
--keepclassmembers class * implements android.os.IInterface {
- public *;
-}
-
--keepattributes *Annotation*,EnclosingMethod,Signature
--keepnames class com.fasterxml.jackson.** { *; }
--dontwarn com.fasterxml.jackson.databind.ext.**
--keep class org.codehaus.** { *; }
--keepclassmembers public final enum org.codehaus.jackson.annotate.JsonAutoDetect$Visibility {
-public static final org.codehaus.jackson.annotate.JsonAutoDetect$Visibility *; }
--keep public class org.fdroid.** {
- *;
-}
-
--dontwarn org.bouncycastle.jsse.**
--dontwarn org.conscrypt.**
--dontwarn org.openjsse.**
-
-# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
--keep class androidx.work.multiprocess.RemoteWorkManagerClient {
- public (...);
-}
-
--keep class org.acra.config.MailSenderConfiguration {
- public (...);
-}
-
-# Logging
--keep class ch.qos.logback.classic.android.LogcatAppender
--keepclassmembers class ch.qos.logback.** { *; }
--keepclassmembers class org.slf4j.impl.** { *; }
diff --git a/legacy/src/androidTest/AndroidManifest.xml b/legacy/src/androidTest/AndroidManifest.xml
deleted file mode 100644
index 8f67de234..000000000
--- a/legacy/src/androidTest/AndroidManifest.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/legacy/src/androidTest/assets/org.fdroid.extendedpermissionstest.apk b/legacy/src/androidTest/assets/org.fdroid.extendedpermissionstest.apk
deleted file mode 100644
index 9e2377dc2..000000000
Binary files a/legacy/src/androidTest/assets/org.fdroid.extendedpermissionstest.apk and /dev/null differ
diff --git a/legacy/src/androidTest/assets/org.fdroid.permissions.minmax.apk b/legacy/src/androidTest/assets/org.fdroid.permissions.minmax.apk
deleted file mode 100644
index 880342cc6..000000000
Binary files a/legacy/src/androidTest/assets/org.fdroid.permissions.minmax.apk and /dev/null differ
diff --git a/legacy/src/androidTest/assets/org.fdroid.permissions.minmax.zip b/legacy/src/androidTest/assets/org.fdroid.permissions.minmax.zip
deleted file mode 100644
index 068ad82fd..000000000
Binary files a/legacy/src/androidTest/assets/org.fdroid.permissions.minmax.zip and /dev/null differ
diff --git a/legacy/src/androidTest/assets/org.fdroid.permissions.sdk14.apk b/legacy/src/androidTest/assets/org.fdroid.permissions.sdk14.apk
deleted file mode 100644
index 046b8ddf4..000000000
Binary files a/legacy/src/androidTest/assets/org.fdroid.permissions.sdk14.apk and /dev/null differ
diff --git a/legacy/src/androidTest/assets/org.fdroid.permissions.sdk14.zip b/legacy/src/androidTest/assets/org.fdroid.permissions.sdk14.zip
deleted file mode 100644
index 3ab3d0f59..000000000
Binary files a/legacy/src/androidTest/assets/org.fdroid.permissions.sdk14.zip and /dev/null differ
diff --git a/legacy/src/androidTest/assets/simpleIndex.jar b/legacy/src/androidTest/assets/simpleIndex.jar
deleted file mode 100644
index e69de29bb..000000000
diff --git a/legacy/src/androidTest/java/org/fdroid/database/PrimaryConstructorTest.kt b/legacy/src/androidTest/java/org/fdroid/database/PrimaryConstructorTest.kt
deleted file mode 100644
index f3e9d7292..000000000
--- a/legacy/src/androidTest/java/org/fdroid/database/PrimaryConstructorTest.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.fdroid.database
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.junit.Test
-import org.junit.runner.RunWith
-import kotlin.reflect.full.primaryConstructor
-import kotlin.test.assertNotNull
-
-@RunWith(AndroidJUnit4::class)
-internal class PrimaryConstructorTest {
-
- private val classes = listOf(
- AntiFeature::class,
- Category::class,
- ReleaseChannel::class,
- // recent minification removes the primary constructor of CoreRepository
- // so we need to ensure it is still there for our reflection diffing
- Class.forName("org.fdroid.database.CoreRepository").kotlin,
- )
-
- @Test
- fun testPrimaryConstructor() {
- classes.forEach {
- assertNotNull(
- actual = it.primaryConstructor,
- message = "${it.simpleName} has no primary constructor",
- )
- }
- }
-
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java b/legacy/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java
deleted file mode 100644
index db94bcfdb..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.fdroid.fdroid;
-
-import static org.junit.Assert.fail;
-
-import android.content.Context;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-public class AssetUtils {
-
- private static final String TAG = "Utils";
-
- /**
- * This requires {@link Context} from {@link android.app.Instrumentation#getContext()}
- */
- @Nullable
- public static File copyAssetToDir(Context context, String assetName, File directory) {
- File tempFile = null;
- InputStream input = null;
- OutputStream output = null;
- try {
- tempFile = File.createTempFile(assetName, ".testasset", directory);
- Log.i(TAG, "Copying asset file " + assetName + " to directory " + directory);
- input = context.getAssets().open(assetName);
- output = new FileOutputStream(tempFile);
- Utils.copy(input, output);
- } catch (IOException e) {
- Log.e(TAG, "Check the context is from Instrumentation.getContext()");
- fail(e.getMessage());
- } finally {
- Utils.closeQuietly(output);
- Utils.closeQuietly(input);
- }
- return tempFile;
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/LocalizationTest.java b/legacy/src/androidTest/java/org/fdroid/fdroid/LocalizationTest.java
deleted file mode 100644
index 094bd5dd3..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/LocalizationTest.java
+++ /dev/null
@@ -1,224 +0,0 @@
-package org.fdroid.fdroid;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.content.res.AssetManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-import android.util.Log;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IllegalFormatException;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Runs through all of the translated strings and tests them with the same format
- * values that the source strings expect. This is to ensure that the formats in
- * the translations are correct in number and in type (e.g. {@code s} or {@code s}.
- * It reads the source formats and then builds {@code formats} to represent the
- * position and type of the formats. Then it runs through all of the translations
- * with formats of the correct number and type.
- */
-@RunWith(AndroidJUnit4.class)
-public class LocalizationTest {
- public static final String TAG = "LocalizationTest";
-
- private final Pattern androidFormat = Pattern.compile("(%[a-z0-9]\\$?[a-z]?)");
- private final Locale[] locales = Locale.getAvailableLocales();
- private final HashSet localeNames = new HashSet<>(locales.length);
-
- private AssetManager assets;
- private Configuration config;
- private Resources resources;
-
- @Before
- public void setUp() {
- for (Locale locale : Languages.LOCALES_TO_TEST) {
- localeNames.add(locale.toString());
- }
- for (Locale locale : locales) {
- localeNames.add(locale.toString());
- }
-
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- Context context = instrumentation.getTargetContext();
- assets = context.getAssets();
- config = context.getResources().getConfiguration();
- config.locale = Locale.ENGLISH;
- // Resources() requires DisplayMetrics, but they are only needed for drawables
- resources = new Resources(assets, new DisplayMetrics(), config);
- }
-
- @Test
- public void testLoadAllPlural() throws IllegalAccessException {
- Field[] fields = R.plurals.class.getDeclaredFields();
-
- HashMap haveFormats = new HashMap<>();
- for (Field field : fields) {
- //Log.i(TAG, field.getName());
- int resId = field.getInt(int.class);
- CharSequence string = resources.getQuantityText(resId, 4);
- //Log.i(TAG, field.getName() + ": '" + string + "'");
- Matcher matcher = androidFormat.matcher(string);
- int matches = 0;
- char[] formats = new char[5];
- while (matcher.find()) {
- String match = matcher.group(0);
- char formatType = match.charAt(match.length() - 1);
- switch (match.length()) {
- case 2:
- formats[matches] = formatType;
- matches++;
- break;
- case 4:
- formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
- break;
- case 5:
- formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
- break;
- default:
- throw new IllegalStateException(field.getName() + " has bad format: " + match);
- }
- }
- haveFormats.put(field.getName(), new String(formats).trim());
- }
-
- for (Locale locale : locales) {
- config.locale = locale;
- // Resources() requires DisplayMetrics, but they are only needed for drawables
- resources = new Resources(assets, new DisplayMetrics(), config);
- for (Field field : fields) {
- String formats = null;
- try {
- int resId = field.getInt(int.class);
- for (int quantity = 0; quantity < 567; quantity++) {
- resources.getQuantityString(resId, quantity);
- }
-
- formats = haveFormats.get(field.getName());
- switch (formats) {
- case "d":
- resources.getQuantityString(resId, 1, 1);
- break;
- case "s":
- resources.getQuantityString(resId, 1, "ONE");
- break;
- case "ds":
- resources.getQuantityString(resId, 2, 1, "TWO");
- break;
- default:
- if (!TextUtils.isEmpty(formats)) {
- throw new IllegalStateException("Pattern not included in tests: " + formats);
- }
- }
- } catch (IllegalFormatException | Resources.NotFoundException e) {
- Log.i(TAG, locale + " " + field.getName());
- throw new IllegalArgumentException("Bad '" + formats + "' format in " + locale + " "
- + field.getName() + ": " + e.getMessage());
- }
- }
- }
- }
-
- @Test
- public void testLoadAllStrings() throws IllegalAccessException {
- Field[] fields = R.string.class.getDeclaredFields();
-
- HashMap haveFormats = new HashMap<>();
- for (Field field : fields) {
- String string = resources.getString(field.getInt(int.class));
- Matcher matcher = androidFormat.matcher(string);
- int matches = 0;
- char[] formats = new char[5];
- while (matcher.find()) {
- String match = matcher.group(0);
- char formatType = match.charAt(match.length() - 1);
- switch (match.length()) {
- case 2:
- formats[matches] = formatType;
- matches++;
- break;
- case 4:
- formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
- break;
- case 5:
- formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
- break;
- default:
- throw new IllegalStateException(field.getName() + " has bad format: " + match);
- }
- }
- haveFormats.put(field.getName(), new String(formats).trim());
- }
-
- for (Locale locale : locales) {
- config.locale = locale;
- // Resources() requires DisplayMetrics, but they are only needed for drawables
- resources = new Resources(assets, new DisplayMetrics(), config);
- for (Field field : fields) {
- int resId = field.getInt(int.class);
- resources.getString(resId);
-
- String formats = haveFormats.get(field.getName());
- try {
- switch (formats) {
- case "d":
- resources.getString(resId, 1);
- break;
- case "dd":
- resources.getString(resId, 1, 2);
- break;
- case "ds":
- resources.getString(resId, 1, "TWO");
- break;
- case "dds":
- resources.getString(resId, 1, 2, "THREE");
- break;
- case "sds":
- resources.getString(resId, "ONE", 2, "THREE");
- break;
- case "s":
- resources.getString(resId, "ONE");
- break;
- case "ss":
- resources.getString(resId, "ONE", "TWO");
- break;
- case "sss":
- resources.getString(resId, "ONE", "TWO", "THREE");
- break;
- case "ssss":
- resources.getString(resId, "ONE", "TWO", "THREE", "FOUR");
- break;
- case "ssd":
- resources.getString(resId, "ONE", "TWO", 3);
- break;
- case "sssd":
- resources.getString(resId, "ONE", "TWO", "THREE", 4);
- break;
- default:
- if (!TextUtils.isEmpty(formats)) {
- throw new IllegalStateException("Pattern not included in tests: " + formats);
- }
- }
- } catch (Exception e) {
- Log.i(TAG, locale + " " + field.getName());
- throw new IllegalArgumentException("Bad format in '" + locale + "' '" + field.getName() + "': "
- + e.getMessage());
- }
- }
- }
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java b/legacy/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java
deleted file mode 100644
index 0d9097ebd..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java
+++ /dev/null
@@ -1,278 +0,0 @@
-package org.fdroid.fdroid;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.action.ViewActions.swipeDown;
-import static androidx.test.espresso.action.ViewActions.swipeLeft;
-import static androidx.test.espresso.action.ViewActions.swipeRight;
-import static androidx.test.espresso.action.ViewActions.swipeUp;
-import static androidx.test.espresso.action.ViewActions.typeText;
-import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.Matchers.not;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import android.Manifest;
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.core.content.ContextCompat;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.IdlingPolicies;
-import androidx.test.espresso.ViewInteraction;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.rule.GrantPermissionRule;
-import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
-import androidx.test.uiautomator.UiSelector;
-
-import org.fdroid.fdroid.views.StatusBanner;
-import org.fdroid.fdroid.views.main.MainActivity;
-import org.hamcrest.Matchers;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class MainActivityEspressoTest {
- public static final String TAG = "MainActivityEspressoTest";
-
- /**
- * Emulators older than {@code android-25} seem to fail at running Espresso tests.
- *
- * ARM emulators are too slow to run these tests in a useful way. The sad
- * thing is that it would probably work if Android didn't put up the ANR
- * "Process system isn't responding" on boot each time. There seems to be no
- * way to increase the ANR timeout.
- */
- private static boolean canRunEspresso() {
- if (Build.VERSION.SDK_INT < 25
- || Build.SUPPORTED_ABIS[0].startsWith("arm") && isEmulator()) {
- Log.e(TAG, "SKIPPING TEST: ARM emulators are too slow to run these tests in a useful way");
- return false;
- }
- return true;
- }
-
- @BeforeClass
- public static void classSetUp() {
- IdlingPolicies.setIdlingResourceTimeout(10, TimeUnit.MINUTES);
- IdlingPolicies.setMasterPolicyTimeout(10, TimeUnit.MINUTES);
- if (!canRunEspresso()) {
- return;
- }
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- try {
- UiDevice.getInstance(instrumentation)
- .executeShellCommand("pm grant "
- + instrumentation.getTargetContext().getPackageName()
- + " android.permission.SET_ANIMATION_SCALE");
- } catch (IOException e) {
- e.printStackTrace();
- }
- SystemAnimations.disableAll(ApplicationProvider.getApplicationContext());
-
- // dismiss the ANR or any other system dialogs that might be there
- UiObject button = new UiObject(new UiSelector().text("Wait").enabled(true));
- try {
- button.click();
- } catch (UiObjectNotFoundException e) {
- Log.d(TAG, e.getLocalizedMessage());
- }
- new UiWatchers().registerAnrAndCrashWatchers();
-
- Context context = instrumentation.getTargetContext();
- ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
- ActivityManager activityManager = ContextCompat.getSystemService(context, ActivityManager.class);
- activityManager.getMemoryInfo(mi);
- long percentAvail = mi.availMem / mi.totalMem;
- Log.i(TAG, "RAM: " + mi.availMem + " / " + mi.totalMem + " = " + percentAvail);
- }
-
- @AfterClass
- public static void classTearDown() {
- SystemAnimations.enableAll(ApplicationProvider.getApplicationContext());
- }
-
- public static boolean isEmulator() {
- return Build.FINGERPRINT.startsWith("generic")
- || Build.FINGERPRINT.startsWith("unknown")
- || Build.MODEL.contains("google_sdk")
- || Build.MODEL.contains("Emulator")
- || Build.MODEL.contains("Android SDK built for x86")
- || Build.MANUFACTURER.contains("Genymotion")
- || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
- || "google_sdk".equals(Build.PRODUCT);
- }
-
- @Before
- public void setUp() {
- assumeTrue(canRunEspresso());
- }
-
- /**
- * Placate {@link android.os.StrictMode}
- *
- * @see Run finalizers before counting for StrictMode
- */
- @After
- public void tearDown() {
- System.gc();
- System.runFinalization();
- System.gc();
- }
-
- @Rule
- public ActivityTestRule activityTestRule =
- new ActivityTestRule<>(MainActivity.class);
-
- @Rule
- public GrantPermissionRule accessCoarseLocationPermissionRule = GrantPermissionRule.grant(
- Manifest.permission.ACCESS_COARSE_LOCATION);
-
- @Rule
- public GrantPermissionRule readExternalStoragePermissionRule = GrantPermissionRule.grant(
- Manifest.permission.READ_EXTERNAL_STORAGE);
-
- @Test
- public void bottomNavFlavorCheck() {
- onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
- onView(withText(R.string.menu_settings)).check(matches(isDisplayed()));
- onView(withText("THIS SHOULD NOT SHOW UP ANYWHERE!!!")).check(doesNotExist());
-
- assertTrue(BuildConfig.FLAVOR.startsWith("full") || BuildConfig.FLAVOR.startsWith("basic"));
-
- if (BuildConfig.FLAVOR.startsWith("basic")) {
- onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
- onView(withText(R.string.main_menu__categories)).check(doesNotExist());
- onView(withText(R.string.main_menu__swap_nearby)).check(doesNotExist());
- }
-
- if (BuildConfig.FLAVOR.startsWith("full")) {
- onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
- onView(withText(R.string.main_menu__categories)).check(matches(isDisplayed()));
- onView(withText(R.string.main_menu__swap_nearby)).check(matches(isDisplayed()));
- }
- }
-
- @LargeTest
- @Test
- public void showSettings() {
- ViewInteraction settingsBottonNavButton = onView(
- allOf(withText(R.string.menu_settings), isDisplayed()));
- settingsBottonNavButton.perform(click());
- onView(withText(R.string.preference_manage_installed_apps)).check(matches(isDisplayed()));
- if (BuildConfig.FLAVOR.startsWith("basic") && BuildConfig.APPLICATION_ID.endsWith(".debug")) {
- // TODO fix me by sorting out the flavor applicationId for debug builds in app/build.gradle
- Log.i(TAG, "Skipping the remainder of showSettings test because it just crashes on basic .debug builds");
- return;
- }
- ViewInteraction manageInstalledAppsButton = onView(
- allOf(withText(R.string.preference_manage_installed_apps), isDisplayed()));
- manageInstalledAppsButton.perform(click());
- onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
- onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
-
- onView(withText(R.string.menu_manage)).perform(click());
- onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
-
- manageInstalledAppsButton.perform(click());
- onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
- onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
-
- onView(withText(R.string.menu_manage)).perform(click());
- onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
-
- onView(withText(R.string.about_title)).perform(click());
- onView(withId(R.id.version)).check(matches(isDisplayed()));
- onView(withId(R.id.ok_button)).perform(click());
-
- onView(withId(android.R.id.list_container)).perform(swipeUp()).perform(swipeUp()).perform(swipeUp());
- }
-
- @LargeTest
- @Test
- public void showUpdates() {
- ViewInteraction updatesBottonNavButton = onView(allOf(withText(R.string.main_menu__updates), isDisplayed()));
- updatesBottonNavButton.perform(click());
- onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
- }
-
- @LargeTest
- @Test
- public void showCategories() {
- onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
- onView(allOf(withText(R.string.main_menu__categories), isDisplayed())).perform(click());
- onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
- .perform(swipeDown())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeDown())
- .perform(swipeDown())
- .perform(swipeRight())
- .perform(swipeLeft())
- .perform(swipeLeft())
- .perform(swipeLeft())
- .perform(swipeLeft())
- .perform(click());
- }
-
- @LargeTest
- @Test
- public void showLatest() {
- onView(Matchers.instanceOf(StatusBanner.class)).check(matches(not(isDisplayed())));
- onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
- onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
- onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
- .perform(swipeDown())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeDown())
- .perform(swipeUp())
- .perform(swipeDown())
- .perform(swipeDown())
- .perform(swipeDown())
- .perform(swipeDown())
- .perform(click());
- }
-
- @LargeTest
- @Test
- public void showSearch() {
- onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
- onView(withId(R.id.fab_search)).check(doesNotExist());
- onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
- onView(allOf(withId(R.id.fab_search), isDisplayed())).perform(click());
- onView(withId(R.id.sort)).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.search), isDisplayed()))
- .perform(click())
- .perform(typeText("test"));
- onView(allOf(withId(R.id.sort), isDisplayed())).perform(click());
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/Netstat.java b/legacy/src/androidTest/java/org/fdroid/fdroid/Netstat.java
deleted file mode 100644
index 26416afc4..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/Netstat.java
+++ /dev/null
@@ -1,373 +0,0 @@
-package org.fdroid.fdroid;
-
-import androidx.annotation.NonNull;
-
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Replacer for the netstat utility, by reading the /proc filesystem it can find out the
- * open connections of the system
- * From http://www.ussg.iu.edu/hypermail/linux/kernel/0409.1/2166.html :
- * It will first list all listening TCP sockets, and next list all established
- * TCP connections. A typical entry of /proc/net/tcp would look like this (split
- * up into 3 parts because of the length of the line):
- *
- * 46: 010310AC:9C4C 030310AC:1770 01
- * | | | | | |--> connection state
- * | | | | |------> remote TCP port number
- * | | | |-------------> remote IPv4 address
- * | | |--------------------> local TCP port number
- * | |---------------------------> local IPv4 address
- * |----------------------------------> number of entry
- *
- * 00000150:00000000 01:00000019 00000000
- * | | | | |--> number of unrecovered RTO timeouts
- * | | | |----------> number of jiffies until timer expires
- * | | |----------------> timer_active (see below)
- * | |----------------------> receive-queue
- * |-------------------------------> transmit-queue
- *
- * 1000 0 54165785 4 cd1e6040 25 4 27 3 -1
- * | | | | | | | | | |--> slow start size threshold,
- * | | | | | | | | | or -1 if the threshold
- * | | | | | | | | | is >= 0xFFFF
- * | | | | | | | | |----> sending congestion window
- * | | | | | | | |-------> (ack.quick<<1)|ack.pingpong
- * | | | | | | |---------> Predicted tick of soft clock
- * | | | | | | (delayed ACK control data)
- * | | | | | |------------> retransmit timeout
- * | | | | |------------------> location of socket in memory
- * | | | |-----------------------> socket reference count
- * | | |-----------------------------> inode
- * | |----------------------------------> unanswered 0-window probes
- * |---------------------------------------------> uid
- *
- * @author Ciprian Dobre
- */
-public class Netstat {
-
- /**
- * Possible values for states in /proc/net/tcp
- */
- private static final String[] STATES = {
- "ESTBLSH", "SYNSENT", "SYNRECV", "FWAIT1", "FWAIT2", "TMEWAIT",
- "CLOSED", "CLSWAIT", "LASTACK", "LISTEN", "CLOSING", "UNKNOWN",
- };
- /**
- * Pattern used when parsing through /proc/net/tcp
- */
- private static final Pattern NET_PATTERN = Pattern.compile(
- "\\d+:\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+)\\s+" +
- "[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+\\s+([\\d]+)\\s+[\\d]+\\s+([\\d]+)");
-
- /**
- * Utility method that converts an address from a hex representation as founded in /proc to String representation
- */
- private static String getAddress(final String hexa) {
- try {
- // first let's convert the address to Integer
- final long v = Long.parseLong(hexa, 16);
- // in /proc the order is little endian and java uses big endian order we also need to invert the order
- final long adr = (v >>> 24) | (v << 24) |
- ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00);
- // and now it's time to output the result
- return ((adr >> 24) & 0xff) + "." + ((adr >> 16) & 0xff) + "." + ((adr >> 8) & 0xff) + "." + (adr & 0xff);
- } catch (Exception ex) {
- ex.printStackTrace();
- return "0.0.0.0"; // NOPMD
- }
- }
-
- private static int getInt16(final String hexa) {
- try {
- return Integer.parseInt(hexa, 16);
- } catch (Exception ex) {
- ex.printStackTrace();
- return -1;
- }
- }
-
- /*
- private static String getPName(final int pid) {
- final Pattern pattern = Pattern.compile("Name:\\s*(\\S+)");
- try {
- BufferedReader in = new BufferedReader(new FileReader("/proc/" + pid + "/status"));
- String line;
- while ((line = in.readLine()) != null) {
- final Matcher matcher = pattern.matcher(line);
- if (matcher.find()) {
- return matcher.group(1);
- }
- }
- in.close();
- } catch (Throwable t) {
- // ignored
- }
- return "UNKNOWN";
- }
- */
-
- /**
- * Method used to question for the connections currently opened
- *
- * @return The list of connections (as Connection objects)
- */
- public static List getConnections() {
-
- final ArrayList net = new ArrayList<>();
-
- // read from /proc/net/tcp the list of currently opened socket connections
- try {
- BufferedReader in = new BufferedReader(new FileReader("/proc/net/tcp"));
- String line;
- while ((line = in.readLine()) != null) { // NOPMD
- Matcher matcher = NET_PATTERN.matcher(line);
- if (matcher.find()) {
- final Connection c = new Connection();
- c.setProtocol(Connection.TCP_CONNECTION);
- net.add(c);
- final String localPortHexa = matcher.group(2);
- final String remoteAddressHexa = matcher.group(3);
- final String remotePortHexa = matcher.group(4);
- final String statusHexa = matcher.group(5);
- //final String uid = matcher.group(6);
- //final String inode = matcher.group(7);
- c.setLocalPort(getInt16(localPortHexa));
- c.setRemoteAddress(getAddress(remoteAddressHexa));
- c.setRemotePort(getInt16(remotePortHexa));
- try {
- c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
- } catch (Exception ex) {
- c.setStatus(STATES[11]); // unknown
- }
- c.setPID(-1); // unknown
- c.setPName("UNKNOWN");
- }
- }
- in.close();
- } catch (Throwable t) { // NOPMD
- // ignored
- }
-
- // read from /proc/net/udp the list of currently opened socket connections
- try {
- BufferedReader in = new BufferedReader(new FileReader("/proc/net/udp"));
- String line;
- while ((line = in.readLine()) != null) { // NOPMD
- Matcher matcher = NET_PATTERN.matcher(line);
- if (matcher.find()) {
- final Connection c = new Connection();
- c.setProtocol(Connection.UDP_CONNECTION);
- net.add(c);
- final String localPortHexa = matcher.group(2);
- final String remoteAddressHexa = matcher.group(3);
- final String remotePortHexa = matcher.group(4);
- final String statusHexa = matcher.group(5);
- //final String uid = matcher.group(6);
- //final String inode = matcher.group(7);
- c.setLocalPort(getInt16(localPortHexa));
- c.setRemoteAddress(getAddress(remoteAddressHexa));
- c.setRemotePort(getInt16(remotePortHexa));
- try {
- c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
- } catch (Exception ex) {
- c.setStatus(STATES[11]); // unknown
- }
- c.setPID(-1); // unknown
- c.setPName("UNKNOWN");
- }
- }
- in.close();
- } catch (Throwable t) { // NOPMD
- // ignored
- }
-
- // read from /proc/net/raw the list of currently opened socket connections
- try {
- BufferedReader in = new BufferedReader(new FileReader("/proc/net/raw"));
- String line;
- while ((line = in.readLine()) != null) { // NOPMD
- Matcher matcher = NET_PATTERN.matcher(line);
- if (matcher.find()) {
- final Connection c = new Connection();
- c.setProtocol(Connection.RAW_CONNECTION);
- net.add(c);
- //final String localAddressHexa = matcher.group(1);
- final String localPortHexa = matcher.group(2);
- final String remoteAddressHexa = matcher.group(3);
- final String remotePortHexa = matcher.group(4);
- final String statusHexa = matcher.group(5);
- //final String uid = matcher.group(6);
- //final String inode = matcher.group(7);
- c.setLocalPort(getInt16(localPortHexa));
- c.setRemoteAddress(getAddress(remoteAddressHexa));
- c.setRemotePort(getInt16(remotePortHexa));
- try {
- c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
- } catch (Exception ex) {
- c.setStatus(STATES[11]); // unknown
- }
- c.setPID(-1); // unknown
- c.setPName("UNKNOWN");
- }
- }
- in.close();
- } catch (Throwable t) { // NOPMD
- // ignored
- }
- return net;
- }
-
- /**
- * Information about a given connection
- *
- * @author Ciprian Dobre
- */
- public static class Connection {
-
- /**
- * Types of connection protocol
- ***/
- static final byte TCP_CONNECTION = 0;
- static final byte UDP_CONNECTION = 1;
- static final byte RAW_CONNECTION = 2;
- /**
- * serialVersionUID
- */
- private static final long serialVersionUID = 1988671591829311032L;
- /**
- * The protocol of the connection (can be tcp, udp or raw)
- */
- protected byte protocol;
-
- /**
- * The owner of the connection (username)
- */
- protected String powner;
-
- /**
- * The pid of the owner process
- */
- protected int pid;
-
- /**
- * The name of the program owning the connection
- */
- protected String pname;
-
- /**
- * Local port
- */
- protected int localPort;
-
- /**
- * Remote address of the connection
- */
- protected String remoteAddress;
-
- /**
- * Remote port
- */
- protected int remotePort;
-
- /**
- * Status of the connection
- */
- protected String status;
-
- public final byte getProtocol() {
- return protocol;
- }
-
- final void setProtocol(final byte protocol) {
- this.protocol = protocol;
- }
-
- final String getProtocolAsString() {
- switch (protocol) {
- case TCP_CONNECTION:
- return "TCP";
- case UDP_CONNECTION:
- return "UDP";
- case RAW_CONNECTION:
- return "RAW";
- }
- return "UNKNOWN";
- }
-
- public final String getPOwner() {
- return powner;
- }
-
- public final void setPOwner(final String owner) {
- this.powner = owner;
- }
-
- public final int getPID() {
- return pid;
- }
-
- final void setPID(final int pid) {
- this.pid = pid;
- }
-
- public final String getPName() {
- return pname;
- }
-
- final void setPName(final String pname) {
- this.pname = pname;
- }
-
- public final int getLocalPort() {
- return localPort;
- }
-
- final void setLocalPort(final int localPort) {
- this.localPort = localPort;
- }
-
- public final String getRemoteAddress() {
- return remoteAddress;
- }
-
- final void setRemoteAddress(final String remoteAddress) {
- this.remoteAddress = remoteAddress;
- }
-
- public final int getRemotePort() {
- return remotePort;
- }
-
- final void setRemotePort(final int remotePort) {
- this.remotePort = remotePort;
- }
-
- public final String getStatus() {
- return status;
- }
-
- final void setStatus(final String status) {
- this.status = status;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "[Prot=" + getProtocolAsString() +
- ",POwner=" + powner +
- ",PID=" + pid +
- ",PName=" + pname +
- ",LPort=" + localPort +
- ",RAddress=" + remoteAddress +
- ",RPort=" + remotePort +
- ",Status=" + status +
- "]";
- }
- }
-}
\ No newline at end of file
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/SystemAnimations.java b/legacy/src/androidTest/java/org/fdroid/fdroid/SystemAnimations.java
deleted file mode 100644
index f46d5495a..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/SystemAnimations.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.fdroid.fdroid;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.IBinder;
-import android.util.Log;
-
-import java.lang.reflect.Method;
-
-/**
- * @see Disable animations for Espresso tests
- */
-class SystemAnimations {
- public static final String TAG = "SystemAnimations";
-
- private static final float DISABLED = 0.0f;
- private static final float DEFAULT = 1.0f;
-
- static void disableAll(Context context) {
- int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
- if (permStatus == PackageManager.PERMISSION_GRANTED) {
- Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
- setSystemAnimationsScale(DISABLED);
- } else {
- Log.i(TAG, "Disabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
- }
- }
-
- static void enableAll(Context context) {
- int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
- if (permStatus == PackageManager.PERMISSION_GRANTED) {
- Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
- setSystemAnimationsScale(DEFAULT);
- } else {
- Log.i(TAG, "Enabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
- }
- }
-
- private static void setSystemAnimationsScale(float animationScale) {
- try {
- Class> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
- Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
- Class> serviceManagerClazz = Class.forName("android.os.ServiceManager");
- Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
- Class> windowManagerClazz = Class.forName("android.view.IWindowManager");
- Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
- Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
-
- IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
- Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
- float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
- for (int i = 0; i < currentScales.length; i++) {
- currentScales[i] = animationScale;
- }
- setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
- } catch (Exception e) {
- Log.e(TAG, "Could not change animation scale to " + animationScale + " :'(");
- }
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/UiWatchers.java b/legacy/src/androidTest/java/org/fdroid/fdroid/UiWatchers.java
deleted file mode 100644
index 002dc235a..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/UiWatchers.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.fdroid.fdroid;
-
-import android.util.Log;
-
-import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
-import androidx.test.uiautomator.UiSelector;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@SuppressWarnings("MemberName")
-class UiWatchers {
- private static final String LOG_TAG = UiWatchers.class.getSimpleName();
- private final List mErrors = new ArrayList<>();
-
- /**
- * We can use the UiDevice registerWatcher to register a small script to be executed when the
- * framework is waiting for a control to appear. Waiting may be the cause of an unexpected
- * dialog on the screen and it is the time when the framework runs the registered watchers.
- * This is a sample watcher looking for ANR and crashes. it closes it and moves on. You should
- * create your own watchers and handle error logging properly for your type of tests.
- */
- void registerAnrAndCrashWatchers() {
- UiDevice.getInstance().registerWatcher("ANR", () -> {
- UiObject window = new UiObject(new UiSelector().className(
- "com.android.server.am.AppNotRespondingDialog"));
- String errorText = null;
- if (window.exists()) {
- try {
- errorText = window.getText();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "dialog gone?", e);
- }
- onAnrDetected(errorText);
- postHandler("Wait");
- return true; // triggered
- }
- return false; // no trigger
- });
- // class names may have changed
- UiDevice.getInstance().registerWatcher("ANR2", () -> {
- UiObject window = new UiObject(new UiSelector().packageName("android")
- .textContains("isn't responding."));
- if (window.exists()) {
- String errorText = null;
- try {
- errorText = window.getText();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "dialog gone?", e);
- }
- onAnrDetected(errorText);
- postHandler("Wait");
- return true; // triggered
- }
- return false; // no trigger
- });
- UiDevice.getInstance().registerWatcher("CRASH", () -> {
- UiObject window = new UiObject(new UiSelector().className(
- "com.android.server.am.AppErrorDialog"));
- if (window.exists()) {
- String errorText = null;
- try {
- errorText = window.getText();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "dialog gone?", e);
- }
- onCrashDetected(errorText);
- postHandler("OK");
- return true; // triggered
- }
- return false; // no trigger
- });
- UiDevice.getInstance().registerWatcher("CRASH2", () -> {
- UiObject window = new UiObject(new UiSelector().packageName("android")
- .textContains("has stopped"));
- if (window.exists()) {
- String errorText = null;
- try {
- errorText = window.getText();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "dialog gone?", e);
- }
- onCrashDetected(errorText);
- postHandler("OK");
- return true; // triggered
- }
- return false; // no trigger
- });
- Log.i(LOG_TAG, "Registered GUI Exception watchers");
- }
-
- private void onAnrDetected(String errorText) {
- mErrors.add(errorText);
- }
-
- private void onCrashDetected(String errorText) {
- mErrors.add(errorText);
- }
-
- /**
- * Current implementation ignores the exception and continues.
- */
- private void postHandler(String buttonText) {
- // TODO: Add custom error logging here
- String formatedOutput = String.format("UI Exception Message: %-20s\n", UiDevice
- .getInstance().getCurrentPackageName());
- Log.e(LOG_TAG, formatedOutput);
- UiObject buttonOK = new UiObject(new UiSelector().text(buttonText).enabled(true));
- // sometimes it takes a while for the OK button to become enabled
- buttonOK.waitForExists(5000);
- try {
- buttonOK.click();
- } catch (UiObjectNotFoundException e) {
- e.printStackTrace();
- }
- }
-}
\ No newline at end of file
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java b/legacy/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java
deleted file mode 100644
index c629173d2..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package org.fdroid.fdroid.compat;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Build;
-import android.os.Environment;
-import android.util.Log;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.fdroid.fdroid.AssetUtils;
-import org.fdroid.fdroid.data.SanitizedFile;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-import java.util.UUID;
-
-
-/**
- * This test needs to run on the emulator, even though it technically could
- * run as a plain JUnit test, because it is testing the specifics of
- * Android's symlink handling.
- */
-@RunWith(AndroidJUnit4.class)
-public class FileCompatTest {
-
- private static final String TAG = "FileCompatTest";
-
- private SanitizedFile sourceFile;
- private SanitizedFile destFile;
-
- @Before
- public void setUp() {
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- File dir = getWriteableDir(instrumentation);
- sourceFile = SanitizedFile.knownSanitized(
- AssetUtils.copyAssetToDir(instrumentation.getContext(), "simpleIndex.jar", dir));
- destFile = new SanitizedFile(dir, "dest-" + UUID.randomUUID() + ".testproduct");
- assertFalse(destFile.exists());
- assertTrue(sourceFile.getAbsolutePath() + " should exist.", sourceFile.exists());
- }
-
- @After
- public void tearDown() {
- if (!sourceFile.delete()) {
- Log.w(TAG, "Can't delete " + sourceFile.getAbsolutePath() + ".");
- }
-
- if (!destFile.delete()) {
- Log.w(TAG, "Can't delete " + destFile.getAbsolutePath() + ".");
- }
- }
-
- @Test
- public void testSymlinkRuntime() {
- FileCompat.symlinkRuntime(sourceFile, destFile);
- assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
- }
-
- @Test
- public void testSymlinkLibcore() {
- assumeTrue(Build.VERSION.SDK_INT >= 19);
- FileCompat.symlinkLibcore(sourceFile, destFile);
- assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
- }
-
- @Test
- public void testSymlinkOs() {
- assumeTrue(Build.VERSION.SDK_INT >= 21);
- FileCompat.symlinkOs(sourceFile, destFile);
- assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
- }
-
- /**
- * Prefer internal over external storage, because external tends to be FAT filesystems,
- * which don't support symlinks (which we test using this method).
- */
- public static File getWriteableDir(Instrumentation instrumentation) {
- Context context = instrumentation.getContext();
- Context targetContext = instrumentation.getTargetContext();
-
- File[] dirsToTry = new File[]{
- context.getCacheDir(),
- context.getFilesDir(),
- targetContext.getCacheDir(),
- targetContext.getFilesDir(),
- context.getExternalCacheDir(),
- context.getExternalFilesDir(null),
- targetContext.getExternalCacheDir(),
- targetContext.getExternalFilesDir(null),
- Environment.getExternalStorageDirectory(),
- };
-
- return getWriteableDir(dirsToTry);
- }
-
- private static File getWriteableDir(File[] dirsToTry) {
-
- for (File dir : dirsToTry) {
- if (dir != null && dir.canWrite()) {
- return dir;
- }
- }
-
- return null;
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java b/legacy/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java
deleted file mode 100644
index 2f93ecae5..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java
+++ /dev/null
@@ -1,449 +0,0 @@
-/*
- * Copyright (C) 2016 Dominik Schürmann
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.installer;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.Instrumentation;
-import android.net.Uri;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.fdroid.fdroid.AssetUtils;
-import org.fdroid.fdroid.compat.FileCompatTest;
-import org.fdroid.fdroid.data.Apk;
-import org.fdroid.index.v2.PermissionV2;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.TreeSet;
-
-/**
- * This test checks the ApkVerifier by parsing a repo from permissionsRepo.xml
- * and checking the listed permissions against the ones specified in apks' AndroidManifest,
- * which have been specifically generated for this test.
- *
- * NOTE: This androidTest cannot run as a Robolectric test because the
- * required methods from PackageManger are not included in Robolectric's Android API.
- * java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile
- * at android.content.pm.PackageManager.getPackageArchiveInfo(PackageManager.java:3545)
- */
-@RunWith(AndroidJUnit4.class)
-public class ApkVerifierTest {
- public static final String TAG = "ApkVerifierTest";
-
- private Instrumentation instrumentation;
-
- private File sdk14Apk;
- private File minMaxApk;
- private File extendedPermissionsApk;
-
- @Before
- public void setUp() {
- instrumentation = InstrumentationRegistry.getInstrumentation();
- File dir = FileCompatTest.getWriteableDir(instrumentation);
- assertTrue(dir.isDirectory());
- assertTrue(dir.canWrite());
- sdk14Apk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
- "org.fdroid.permissions.sdk14.apk",
- dir
- );
- minMaxApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
- "org.fdroid.permissions.minmax.apk",
- dir
- );
- extendedPermissionsApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
- "org.fdroid.extendedpermissionstest.apk",
- dir
- );
- assertTrue(sdk14Apk.exists());
- assertTrue(minMaxApk.exists());
- assertTrue(extendedPermissionsApk.exists());
- }
-
- @Test
- public void testNulls() {
- assertTrue(ApkVerifier.requestedPermissionsEqual(null, null));
-
- String[] perms = new String[]{"Blah"};
- assertFalse(ApkVerifier.requestedPermissionsEqual(perms, null));
- assertFalse(ApkVerifier.requestedPermissionsEqual(null, perms));
- }
-
- @Test
- public void testWithMinMax()
- throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
- Apk apk = new Apk();
- apk.packageName = "org.fdroid.permissions.minmax";
- apk.targetSdkVersion = 24;
- ArrayList permissionsList = new ArrayList<>();
- permissionsList.add("android.permission.READ_CALENDAR");
- if (Build.VERSION.SDK_INT <= 18) {
- permissionsList.add("android.permission.WRITE_EXTERNAL_STORAGE");
- }
- if (Build.VERSION.SDK_INT >= 23) {
- permissionsList.add("android.permission.ACCESS_FINE_LOCATION");
- }
- apk.requestedPermissions = permissionsList.toArray(new String[0]);
-
- Uri uri = Uri.fromFile(minMaxApk);
- ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
- apkVerifier.verifyApk();
-
- // still not throwing, because now we only throw when minMaxApk has more permissions than expected
- permissionsList.add("ADDITIONAL_PERMISSION");
- apk.requestedPermissions = permissionsList.toArray(new String[0]);
- apkVerifier.verifyApk();
- }
-
- @Test
- public void testWithPrefix() {
- Apk apk = new Apk();
- apk.packageName = "org.fdroid.permissions.sdk14";
- apk.targetSdkVersion = 14;
- TreeSet expectedSet = new TreeSet<>(Arrays.asList(
- "android.permission.AUTHENTICATE_ACCOUNTS",
- "android.permission.MANAGE_ACCOUNTS",
- "android.permission.READ_PROFILE",
- "android.permission.WRITE_PROFILE",
- "android.permission.GET_ACCOUNTS",
- "android.permission.READ_CONTACTS",
- "android.permission.WRITE_CONTACTS",
- "android.permission.WRITE_EXTERNAL_STORAGE",
- "android.permission.READ_EXTERNAL_STORAGE",
- "android.permission.INTERNET",
- "android.permission.ACCESS_NETWORK_STATE",
- "android.permission.NFC",
- "android.permission.READ_SYNC_SETTINGS",
- "android.permission.WRITE_SYNC_SETTINGS",
- "android.permission.WRITE_CALL_LOG", // implied-permission!
- "android.permission.READ_CALL_LOG"// implied-permission!
- ));
- if (Build.VERSION.SDK_INT >= 29) {
- expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
- }
- apk.requestedPermissions = expectedSet.toArray(new String[0]);
-
- Uri uri = Uri.fromFile(sdk14Apk);
-
- ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
-
- try {
- apkVerifier.verifyApk();
- } catch (ApkVerifier.ApkVerificationException |
- ApkVerifier.ApkPermissionUnequalException e) {
- e.printStackTrace();
- fail(e.getMessage());
- }
- }
-
- /**
- * Additional permissions are okay. The user is simply
- * warned about a permission that is not used inside the apk
- */
- @Test
- public void testAdditionalPermission()
- throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
- Apk apk = new Apk();
- apk.packageName = "org.fdroid.permissions.sdk14";
- apk.targetSdkVersion = 14;
- apk.requestedPermissions = new String[]{
- "android.permission.AUTHENTICATE_ACCOUNTS",
- "android.permission.MANAGE_ACCOUNTS",
- "android.permission.READ_PROFILE",
- "android.permission.WRITE_PROFILE",
- "android.permission.GET_ACCOUNTS",
- "android.permission.READ_CONTACTS",
- "android.permission.WRITE_CONTACTS",
- "android.permission.WRITE_EXTERNAL_STORAGE",
- "android.permission.READ_EXTERNAL_STORAGE",
- "android.permission.INTERNET",
- "android.permission.ACCESS_NETWORK_STATE",
- "android.permission.NFC",
- "android.permission.READ_SYNC_SETTINGS",
- "android.permission.WRITE_SYNC_SETTINGS",
- "android.permission.WRITE_CALL_LOG", // implied-permission!
- "android.permission.READ_CALL_LOG", // implied-permission!
- "android.permission.FAKE_NEW_PERMISSION",
- };
-
- Uri uri = Uri.fromFile(sdk14Apk);
- ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
- apkVerifier.verifyApk();
- }
-
- /**
- * Missing permissions are not okay!
- * The user is then not warned about a permission that the apk uses!
- */
- @Test
- public void testMissingPermission() {
- Apk apk = new Apk();
- apk.packageName = "org.fdroid.permissions.sdk14";
- apk.targetSdkVersion = 14;
- apk.requestedPermissions = new String[]{
- //"android.permission.AUTHENTICATE_ACCOUNTS",
- "android.permission.MANAGE_ACCOUNTS",
- "android.permission.READ_PROFILE",
- "android.permission.WRITE_PROFILE",
- "android.permission.GET_ACCOUNTS",
- "android.permission.READ_CONTACTS",
- "android.permission.WRITE_CONTACTS",
- "android.permission.WRITE_EXTERNAL_STORAGE",
- "android.permission.READ_EXTERNAL_STORAGE",
- "android.permission.INTERNET",
- "android.permission.ACCESS_NETWORK_STATE",
- "android.permission.NFC",
- "android.permission.READ_SYNC_SETTINGS",
- "android.permission.WRITE_SYNC_SETTINGS",
- "android.permission.WRITE_CALL_LOG", // implied-permission!
- "android.permission.READ_CALL_LOG", // implied-permission!
- };
-
- Uri uri = Uri.fromFile(sdk14Apk);
-
- ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
-
- try {
- apkVerifier.verifyApk();
- fail();
- } catch (ApkVerifier.ApkVerificationException e) {
- e.printStackTrace();
- fail(e.getMessage());
- } catch (ApkVerifier.ApkPermissionUnequalException e) {
- e.printStackTrace();
- }
- }
-
- @Test
- public void testExtendedPerms()
- throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
- HashSet expectedSet = new HashSet<>(Arrays.asList(
- "android.permission.ACCESS_NETWORK_STATE",
- "android.permission.ACCESS_WIFI_STATE",
- "android.permission.INTERNET",
- "android.permission.READ_SYNC_STATS",
- "android.permission.READ_SYNC_SETTINGS",
- "android.permission.WRITE_SYNC_SETTINGS",
- "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
- "android.permission.READ_CONTACTS",
- "android.permission.WRITE_CONTACTS",
- "android.permission.READ_CALENDAR",
- "android.permission.WRITE_CALENDAR"
- ));
- if (Build.VERSION.SDK_INT <= 18) {
- expectedSet.add("android.permission.READ_EXTERNAL_STORAGE");
- expectedSet.add("android.permission.WRITE_EXTERNAL_STORAGE");
- }
- if (Build.VERSION.SDK_INT <= 22) {
- expectedSet.add("android.permission.GET_ACCOUNTS");
- expectedSet.add("android.permission.AUTHENTICATE_ACCOUNTS");
- expectedSet.add("android.permission.MANAGE_ACCOUNTS");
- }
- if (Build.VERSION.SDK_INT >= 23) {
- expectedSet.add("android.permission.CAMERA");
- if (Build.VERSION.SDK_INT <= 23) {
- expectedSet.add("android.permission.CALL_PHONE");
- }
- }
- Apk apk = new Apk();
- apk.packageName = "urzip.at.or.at.urzip";
- ArrayList perms = new ArrayList<>();
- perms.add(new PermissionV2("android.permission.READ_EXTERNAL_STORAGE", 18));
- perms.add(new PermissionV2("android.permission.WRITE_SYNC_SETTINGS", null));
- perms.add(new PermissionV2("android.permission.ACCESS_NETWORK_STATE", null));
- perms.add(new PermissionV2("android.permission.WRITE_EXTERNAL_STORAGE", 18));
- perms.add(new PermissionV2("android.permission.WRITE_CONTACTS", null));
- perms.add(new PermissionV2("android.permission.ACCESS_WIFI_STATE", null));
- perms.add(new PermissionV2("android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", null));
- perms.add(new PermissionV2("android.permission.WRITE_CALENDAR", null));
- perms.add(new PermissionV2("android.permission.READ_CONTACTS", null));
- perms.add(new PermissionV2("android.permission.READ_SYNC_SETTINGS", null));
- perms.add(new PermissionV2("android.permission.MANAGE_ACCOUNTS", 22));
- perms.add(new PermissionV2("android.permission.INTERNET", null));
- perms.add(new PermissionV2("android.permission.AUTHENTICATE_ACCOUNTS", 22));
- perms.add(new PermissionV2("android.permission.GET_ACCOUNTS", 22));
- perms.add(new PermissionV2("android.permission.READ_CALENDAR", null));
- perms.add(new PermissionV2("android.permission.READ_SYNC_STATS", null));
- apk.setRequestedPermissions(perms, 0);
- ArrayList perms23 = new ArrayList<>();
- perms23.add(new PermissionV2("android.permission.CAMERA", null));
- perms23.add(new PermissionV2("android.permission.CALL_PHONE", 23));
- apk.setRequestedPermissions(perms23, 23);
- HashSet actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
- for (String permission : expectedSet) {
- if (!actualSet.contains(permission)) {
- Log.i(TAG, permission + " in expected but not actual! (android-"
- + Build.VERSION.SDK_INT + ")");
- }
- }
- for (String permission : actualSet) {
- if (!expectedSet.contains(permission)) {
- Log.i(TAG, permission + " in actual but not expected! (android-"
- + Build.VERSION.SDK_INT + ")");
- }
- }
- String[] expectedPermissions = expectedSet.toArray(new String[0]);
- assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
-
- String[] badPermissions = Arrays.copyOf(expectedPermissions, expectedPermissions.length + 1);
- assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
- badPermissions[badPermissions.length - 1] = "notarealpermission";
- assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
-
- Uri uri = Uri.fromFile(extendedPermissionsApk);
- ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
- apkVerifier.verifyApk();
- }
-
- @Test
- public void testImpliedPerms() {
- TreeSet expectedSet = new TreeSet<>(Arrays.asList(
- "android.permission.ACCESS_NETWORK_STATE",
- "android.permission.ACCESS_WIFI_STATE",
- "android.permission.INTERNET",
- "android.permission.READ_CALENDAR",
- "android.permission.READ_CONTACTS",
- "android.permission.READ_EXTERNAL_STORAGE",
- "android.permission.READ_SYNC_SETTINGS",
- "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
- "android.permission.WRITE_CALENDAR",
- "android.permission.WRITE_CONTACTS",
- "android.permission.WRITE_EXTERNAL_STORAGE",
- "android.permission.WRITE_SYNC_SETTINGS",
- "org.dmfs.permission.READ_TASKS",
- "org.dmfs.permission.WRITE_TASKS"
- ));
- if (Build.VERSION.SDK_INT <= 22) { // maxSdkVersion="22"
- expectedSet.addAll(Arrays.asList(
- "android.permission.AUTHENTICATE_ACCOUNTS",
- "android.permission.GET_ACCOUNTS",
- "android.permission.MANAGE_ACCOUNTS"
- ));
- }
- if (Build.VERSION.SDK_INT >= 29) {
- expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
- }
- Apk apk = new Apk();
- apk.packageName = "urzip.at.or.at.urzip";
- apk.targetSdkVersion = 24;
- ArrayList perms = new ArrayList<>();
- perms.add(new PermissionV2("android.permission.READ_EXTERNAL_STORAGE", 18));
- perms.add(new PermissionV2("android.permission.WRITE_SYNC_SETTINGS", null));
- perms.add(new PermissionV2("android.permission.ACCESS_NETWORK_STATE", null));
- perms.add(new PermissionV2("android.permission.WRITE_EXTERNAL_STORAGE", null));
- perms.add(new PermissionV2("android.permission.WRITE_CONTACTS", null));
- perms.add(new PermissionV2("android.permission.ACCESS_WIFI_STATE", null));
- perms.add(new PermissionV2("android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", null));
- perms.add(new PermissionV2("android.permission.WRITE_CALENDAR", null));
- perms.add(new PermissionV2("android.permission.READ_CONTACTS", null));
- perms.add(new PermissionV2("android.permission.READ_SYNC_SETTINGS", null));
- perms.add(new PermissionV2("android.permission.MANAGE_ACCOUNTS", 22));
- perms.add(new PermissionV2("android.permission.INTERNET", null));
- perms.add(new PermissionV2("android.permission.AUTHENTICATE_ACCOUNTS", 22));
- perms.add(new PermissionV2("android.permission.GET_ACCOUNTS", 22));
- perms.add(new PermissionV2("android.permission.READ_CALENDAR", null));
- perms.add(new PermissionV2("org.dmfs.permission.READ_TASKS", null));
- perms.add(new PermissionV2("org.dmfs.permission.WRITE_TASKS", null));
- apk.setRequestedPermissions(perms, 0);
- HashSet actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
- for (String permission : expectedSet) {
- if (!actualSet.contains(permission)) {
- Log.i(TAG, permission + " in expected but not actual! (android-"
- + Build.VERSION.SDK_INT + ")");
- }
- }
- for (String permission : actualSet) {
- if (!expectedSet.contains(permission)) {
- Log.i(TAG, permission + " in actual but not expected! (android-"
- + Build.VERSION.SDK_INT + ")");
- }
- }
- String[] expectedPermissions = expectedSet.toArray(new String[0]);
- assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
-
- expectedSet = new TreeSet<>(Arrays.asList(
- "android.permission.ACCESS_NETWORK_STATE",
- "android.permission.ACCESS_WIFI_STATE",
- "android.permission.AUTHENTICATE_ACCOUNTS",
- "android.permission.GET_ACCOUNTS",
- "android.permission.INTERNET",
- "android.permission.MANAGE_ACCOUNTS",
- "android.permission.READ_CALENDAR",
- "android.permission.READ_CONTACTS",
- "android.permission.READ_EXTERNAL_STORAGE",
- "android.permission.READ_SYNC_SETTINGS",
- "android.permission.WRITE_CALENDAR",
- "android.permission.WRITE_CONTACTS",
- "android.permission.WRITE_EXTERNAL_STORAGE",
- "android.permission.WRITE_SYNC_SETTINGS",
- "org.dmfs.permission.READ_TASKS",
- "org.dmfs.permission.WRITE_TASKS"
- ));
- if (Build.VERSION.SDK_INT >= 29) {
- expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
- }
- expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
- apk = new Apk();
- apk.packageName = "urzip.at.or.at.urzip";
- apk.targetSdkVersion = 23;
- perms = new ArrayList<>();
- perms.add(new PermissionV2("android.permission.WRITE_SYNC_SETTINGS", null));
- perms.add(new PermissionV2("android.permission.ACCESS_NETWORK_STATE", null));
- perms.add(new PermissionV2("android.permission.WRITE_EXTERNAL_STORAGE", null));
- perms.add(new PermissionV2("android.permission.WRITE_CONTACTS", null));
- perms.add(new PermissionV2("android.permission.ACCESS_WIFI_STATE", null));
- perms.add(new PermissionV2("android.permission.WRITE_CALENDAR", null));
- perms.add(new PermissionV2("android.permission.READ_CONTACTS", null));
- perms.add(new PermissionV2("android.permission.READ_SYNC_SETTINGS", null));
- perms.add(new PermissionV2("android.permission.MANAGE_ACCOUNTS", null));
- perms.add(new PermissionV2("android.permission.INTERNET", null));
- perms.add(new PermissionV2("android.permission.AUTHENTICATE_ACCOUNTS", null));
- perms.add(new PermissionV2("android.permission.GET_ACCOUNTS", null));
- perms.add(new PermissionV2("android.permission.READ_CALENDAR", null));
- perms.add(new PermissionV2("org.dmfs.permission.READ_TASKS", null));
- perms.add(new PermissionV2("org.dmfs.permission.WRITE_TASKS", null));
- apk.setRequestedPermissions(perms, 0);
- actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
- for (String permission : expectedSet) {
- if (!actualSet.contains(permission)) {
- Log.i(TAG, permission + " in expected but not actual! (android-"
- + Build.VERSION.SDK_INT + ")");
- }
- }
- for (String permission : actualSet) {
- if (!expectedSet.contains(permission)) {
- Log.i(TAG, permission + " in actual but not expected! (android-"
- + Build.VERSION.SDK_INT + ")");
- }
- }
- assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/net/DnsCacheTest.kt b/legacy/src/androidTest/java/org/fdroid/fdroid/net/DnsCacheTest.kt
deleted file mode 100644
index 22c2e3bbd..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/net/DnsCacheTest.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-package org.fdroid.fdroid.net
-
-import java.net.InetAddress
-import java.util.Arrays
-import org.fdroid.fdroid.Preferences
-import org.junit.Assert
-import org.junit.Test
-
-class DnsCacheTest {
-
- private val url1: String = "locaihost"
- private val url2: String = "fdroid.org"
- private val url3: String = "fdroid.net"
-
- private val ip1: InetAddress = InetAddress.getByName("127.0.0.1")
- private val ip2: InetAddress = InetAddress.getByName("127.0.0.2")
- private val ip3: InetAddress = InetAddress.getByName("127.0.0.3")
-
- private val list1: MutableList = Arrays.asList(ip1, ip2, ip3)
- private val list2: MutableList = Arrays.asList(ip2)
- private val list3: MutableList = Arrays.asList(ip3)
-
- @Test
- fun basicCacheTest() {
- // test setup
- val prefs = Preferences.get()
- prefs.setDnsCacheEnabledValue(true)
- val testObject = DnsCache.get()
-
- // populate cache
- testObject.insert(url1, list1)
- testObject.insert(url2, list2)
- testObject.insert(url3, list3)
-
- // check for cached lookup results
- val testList1 = testObject.lookup(url1)
- Assert.assertEquals(3, testList1.size.toLong())
- Assert.assertEquals(ip1.hostAddress, testList1[0]!!.hostAddress)
- Assert.assertEquals(ip2.hostAddress, testList1[1]!!.hostAddress)
- Assert.assertEquals(ip3.hostAddress, testList1[2]!!.hostAddress)
-
- // toggle preference (false)
- prefs.setDnsCacheEnabledValue(false)
-
- // attempt non-cached lookup
- val testList2 = testObject.lookup(url1)
- Assert.assertNull(testList2)
-
- // toggle preference (true)
- prefs.setDnsCacheEnabledValue(true)
-
- // confirm lookup results remain in cache
- val testList3 = testObject.lookup(url2)
- Assert.assertEquals(1, testList3.size.toLong())
- Assert.assertEquals(ip2.hostAddress, testList3[0].hostAddress)
-
- // test removal
- val testList4 = testObject.remove(url2)
- Assert.assertEquals(1, testList4.size.toLong())
- Assert.assertEquals(ip2.hostAddress, testList4[0].hostAddress)
- val testList5 = testObject.lookup(url2)
- Assert.assertNull(testList5)
- }
-
- @Test
- fun dnsRetryTest() {
- // test setup
- val prefs = Preferences.get()
- prefs.setDnsCacheEnabledValue(true)
- val testObject = DnsWithCache.get()
- val testCache = DnsCache.get()
-
- // insert dummy value into cache
- testCache.insert(url2, list2)
-
- // check initial status
- val testList1 = testObject.lookup(url2)
- Assert.assertEquals(1, testList1.size.toLong())
- Assert.assertEquals(ip2.hostAddress, testList1[0].hostAddress)
-
- // mismatch with dummy value should require retry and clear cache
- val testFlag = testObject.shouldRetryRequest(url2)
- Assert.assertTrue(testFlag)
- val testList2 = testCache.lookup(url2)
- Assert.assertNull(testList2)
-
- // subsequent lookup should cache actual dns result (not testing actual values)
- val testList3 = testObject.lookup(url2)
- Assert.assertNotNull(testList3)
- val testList4 = testCache.lookup(url2)
- Assert.assertNotNull(testList4)
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java b/legacy/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java
deleted file mode 100644
index 377eda97a..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package org.fdroid.fdroid.net;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.os.Build;
-import android.util.Log;
-
-import androidx.core.util.Pair;
-import androidx.test.filters.FlakyTest;
-
-import org.fdroid.download.DownloadRequest;
-import org.fdroid.download.HttpDownloader;
-import org.fdroid.download.HttpManager;
-import org.fdroid.download.Mirror;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.index.v1.IndexV1UpdaterKt;
-import org.fdroid.index.v2.IndexV2UpdaterKt;
-import org.junit.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@FlakyTest
-public class HttpDownloaderTest {
- private static final String TAG = "HttpDownloaderTest";
-
- private final HttpManager httpManager = new HttpManager(
- Utils.getUserAgent(),
- FDroidApp.queryString,
- null,
- null,
- null,
- true
- );
- private static final Collection> URLS;
-
- // https://developer.android.com/reference/javax/net/ssl/SSLContext
- static {
- ArrayList> tempUrls = new ArrayList<>(Arrays.asList(
- new Pair<>("https://f-droid.org/repo", IndexV2UpdaterKt.SIGNED_FILE_NAME),
- // sites that use SNI for HTTPS
- new Pair<>("https://mirrors.edge.kernel.org/", "debian/dists/stable/Release"),
- new Pair<>("https://fdroid.tetaneutral.net/fdroid/repo/", IndexV1UpdaterKt.SIGNED_FILE_NAME),
- new Pair<>("https://opencolo.mm.fcix.net/fdroid/repo/", "dev.lonami.klooni/en-US/phoneScreenshots/1-game.jpg"),
- //new Pair<>("https://microg.org/fdroid/repo/index-v1.jar"),
- //new Pair<>("https://grobox.de/fdroid/repo/index.jar"),
- new Pair<>("https://guardianproject.info/fdroid/repo", IndexV2UpdaterKt.SIGNED_FILE_NAME),
- new Pair<>("https://en.wikipedia.org", "/wiki/Index.html"), // no SNI but weird ipv6 lookup issues
- new Pair<>("https://mirror.cyberbits.eu/fdroid/repo/", IndexV2UpdaterKt.SIGNED_FILE_NAME) // TLSv1.2 only and SNI
- ));
- if (Build.VERSION.SDK_INT < 26) {
- // domains that use Let's Encrypt won't work on Android 7.1 and older
- // https://gitlab.com/fdroid/fdroidclient/-/issues/2102
- tempUrls = new ArrayList<>(Arrays.asList(
- new Pair<>("https://cloudflare.f-droid.org/repo", "dev.lonami.klooni/en-US/phoneScreenshots/1-game.jpg")
- ));
- }
- URLS = tempUrls;
- }
-
- private boolean receivedProgress;
-
- @Test
- public void downloadUninterruptedTest() throws IOException, InterruptedException {
- for (Pair pair : URLS) {
- Log.i(TAG, "URL: " + pair.first + pair.second);
- File destFile = File.createTempFile("dl-", "");
- List mirrors = Mirror.fromStrings(Collections.singletonList(pair.first));
- DownloadRequest request = new DownloadRequest(pair.second, mirrors, null, null, null);
- HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
- httpDownloader.download();
- assertTrue(destFile.exists());
- assertTrue(destFile.canRead());
- destFile.deleteOnExit();
- }
- }
-
- @Test
- public void downloadUninterruptedTestWithProgress() throws IOException, InterruptedException {
- final CountDownLatch latch = new CountDownLatch(1);
- String path = "index.jar";
- List mirrors = Mirror.fromStrings(Collections.singletonList("https://cloudflare.f-droid.org/repo/"));
- receivedProgress = false;
- File destFile = File.createTempFile("dl-", "");
- final DownloadRequest request = new DownloadRequest(path, mirrors, null, null, null);
- final HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
- httpDownloader.setListener((bytesRead, totalBytes) -> {
- receivedProgress = true;
- });
- new Thread() {
- @Override
- public void run() {
- try {
- httpDownloader.download();
- latch.countDown();
- } catch (IOException | InterruptedException e) {
- e.printStackTrace();
- fail();
- }
- }
- }.start();
- latch.await(100, TimeUnit.SECONDS); // either 2 progress reports or 100 seconds
- assertTrue(destFile.exists());
- assertTrue(destFile.canRead());
- assertTrue(receivedProgress);
- destFile.deleteOnExit();
- }
-
- @Test
- public void downloadHttpBasicAuth() throws IOException, InterruptedException {
- String path = "myusername/supersecretpassword";
- List mirrors = Mirror.fromStrings(Collections.singletonList("https://httpbin.org/basic-auth/"));
- File destFile = File.createTempFile("dl-", "");
- final DownloadRequest request = new DownloadRequest(path, mirrors, null, "myusername", "supersecretpassword");
- HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
- httpDownloader.download();
- assertTrue(destFile.exists());
- assertTrue(destFile.canRead());
- destFile.deleteOnExit();
- }
-
- @Test(expected = IOException.class)
- public void downloadHttpBasicAuthWrongPassword() throws IOException, InterruptedException {
- String path = "myusername/supersecretpassword";
- List mirrors = Mirror.fromStrings(Collections.singletonList("https://httpbin.org/basic-auth/"));
- File destFile = File.createTempFile("dl-", "");
- final DownloadRequest request =
- new DownloadRequest(path, mirrors, null, "myusername", "wrongpassword");
- HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
- httpDownloader.download();
- assertFalse(destFile.exists());
- destFile.deleteOnExit();
- }
-
- @Test(expected = IOException.class)
- public void downloadHttpBasicAuthWrongUsername() throws IOException, InterruptedException {
- String path = "myusername/supersecretpassword";
- List mirrors = Mirror.fromStrings(Collections.singletonList("https://httpbin.org/basic-auth/"));
- File destFile = File.createTempFile("dl-", "");
- final DownloadRequest request =
- new DownloadRequest(path, mirrors, null, "wrongusername", "supersecretpassword");
- HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
- httpDownloader.download();
- assertFalse(destFile.exists());
- destFile.deleteOnExit();
- }
-
- @Test
- public void downloadThenCancel() throws IOException, InterruptedException {
- final CountDownLatch latch = new CountDownLatch(2);
- String path = "index.jar";
- List mirrors = Mirror.fromStrings(Collections.singletonList("https://cloudflare.f-droid.org/repo/"));
- File destFile = File.createTempFile("dl-", "");
- final DownloadRequest request = new DownloadRequest(path, mirrors, null, null, null);
- final HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
- httpDownloader.setListener((bytesRead, totalBytes) -> {
- receivedProgress = true;
- latch.countDown();
- });
- new Thread() {
- @Override
- public void run() {
- try {
- httpDownloader.download();
- fail();
- } catch (IOException e) {
- Log.e(TAG, "Error downloading: ", e);
- fail();
- } catch (InterruptedException e) {
- // success!
- }
- }
- }.start();
- latch.await(100, TimeUnit.SECONDS); // either 2 progress reports or 100 seconds
- httpDownloader.cancelDownload();
- assertTrue(receivedProgress);
- destFile.deleteOnExit();
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/work/AppUpdateWorkerTest.kt b/legacy/src/androidTest/java/org/fdroid/fdroid/work/AppUpdateWorkerTest.kt
deleted file mode 100644
index 7667b7a79..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/work/AppUpdateWorkerTest.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-package org.fdroid.fdroid.work
-
-import android.os.Build.VERSION.SDK_INT
-import android.text.format.DateUtils
-import android.util.Log
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import androidx.work.Configuration
-import androidx.work.ListenableWorker.Result
-import androidx.work.WorkInfo.Companion.STOP_REASON_NOT_STOPPED
-import androidx.work.WorkInfo.State.ENQUEUED
-import androidx.work.WorkInfo.State.SUCCEEDED
-import androidx.work.WorkManager
-import androidx.work.testing.SynchronousExecutor
-import androidx.work.testing.TestListenableWorkerBuilder
-import androidx.work.testing.WorkManagerTestInitHelper
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.mockkStatic
-import io.mockk.verify
-import java.io.IOException
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
-import kotlin.test.fail
-import kotlinx.coroutines.runBlocking
-import org.fdroid.fdroid.AppUpdateManager
-import org.fdroid.fdroid.FDroidApp
-import org.fdroid.fdroid.Preferences
-import org.fdroid.fdroid.Preferences.OVER_NETWORK_ALWAYS
-import org.fdroid.fdroid.net.ConnectivityMonitorService
-import org.fdroid.fdroid.net.ConnectivityMonitorService.FLAG_NET_METERED
-import org.fdroid.fdroid.net.ConnectivityMonitorService.FLAG_NET_NO_LIMIT
-import org.fdroid.fdroid.net.ConnectivityMonitorService.FLAG_NET_UNAVAILABLE
-import org.fdroid.fdroid.work.AppUpdateWorker.Companion.UNIQUE_WORK_NAME_APP_UPDATE
-import org.fdroid.fdroid.work.AppUpdateWorker.Companion.UNIQUE_WORK_NAME_AUTO_APP_UPDATE
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class AppUpdateWorkerTest {
-
- private val context get() = getInstrumentation().targetContext
- private val workManager get() = WorkManager.getInstance(context)
- private val preferences: Preferences by lazy { mockk() }
- private val updateManager: AppUpdateManager by lazy { mockk() }
-
- @Before
- fun setup() {
- // MockKAgentException: Mocking static is supported starting from Android P
- assumeTrue(SDK_INT >= 28)
-
- val config = Configuration.Builder()
- .setMinimumLoggingLevel(Log.DEBUG)
- .setExecutor(SynchronousExecutor())
- .build()
- WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
-
- mockkStatic(FDroidApp::getAppUpdateManager)
- every { FDroidApp.getAppUpdateManager(any()) } returns updateManager
- mockkStatic(Preferences::get)
- every { Preferences.get() } returns preferences
- every { preferences.isLocalRepoHttpsEnabled } returns false
- every { preferences.isOnDemandDownloadAllowed } returns true
- every { preferences.mirrorErrorData } returns emptyMap()
- }
-
- @Test
- @Throws(Exception::class)
- fun testHappyPath() {
- FDroidApp.networkState = FLAG_NET_NO_LIMIT
- every { updateManager.updateApps() } returns true
-
- AppUpdateWorker.updateAppsNow(context)
-
- verify { updateManager.updateApps() }
-
- val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_APP_UPDATE).get()
-
- assertEquals(1, workInfo.size)
- assertEquals(SUCCEEDED, workInfo[0].state)
- }
-
- @Test
- @Throws(Exception::class)
- fun testException() {
- every { updateManager.updateApps() } throws IOException("foo bar")
-
- AppUpdateWorker.updateAppsNow(context)
-
- val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_APP_UPDATE).get()
-
- assertEquals(1, workInfo.size)
- assertEquals(ENQUEUED, workInfo[0].state)
- assertEquals(STOP_REASON_NOT_STOPPED, workInfo[0].stopReason)
-
- verify(exactly = 1) { updateManager.updateApps() }
-
- // build the worker manually, so we can see what result it returns
- runBlocking {
- val worker = TestListenableWorkerBuilder(context, runAttemptCount = 3)
- .build()
- assertEquals(Result.retry(), worker.doWork())
- }
- verify(exactly = 2) { updateManager.updateApps() }
-
- // now build the worker with a higher runAttemptCount
- runBlocking {
- val worker = TestListenableWorkerBuilder(context, runAttemptCount = 4)
- .build()
- assertEquals(Result.failure(), worker.doWork()) // now it fails
- }
- verify(exactly = 3) { updateManager.updateApps() }
- }
-
- @Test
- @Throws(Exception::class)
- fun testNotRunningWhenNoNetwork() {
- mockkStatic(ConnectivityMonitorService::getNetworkState)
- every { ConnectivityMonitorService.getNetworkState(any()) } returns FLAG_NET_UNAVAILABLE
- FDroidApp.networkState = FLAG_NET_UNAVAILABLE
-
- try {
- AppUpdateWorker.updateAppsNow(context)
- fail()
- } catch (e: NullPointerException) {
- // can't send toast from these tests
- assertTrue(e.message?.contains("toast") == true)
- }
-
- val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_APP_UPDATE).get()
- assertEquals(0, workInfo.size)
- }
-
- @Test
- @Throws(Exception::class)
- fun testNotRunningOnMeteredNetwork() {
- FDroidApp.networkState = FLAG_NET_METERED
- every { preferences.isOnDemandDownloadAllowed } returns false
-
- try {
- AppUpdateWorker.updateAppsNow(context)
- fail()
- } catch (e: NullPointerException) {
- // can't send toast from these tests
- assertTrue(e.message?.contains("toast") == true)
- }
-
- val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_APP_UPDATE).get()
- assertEquals(0, workInfo.size)
- }
-
- @Test
- @Throws(Exception::class)
- fun testPeriodicWork() {
- every { preferences.isAutoDownloadEnabled } returns true
- every { preferences.updateInterval } returns DateUtils.HOUR_IN_MILLIS * 4
- every { preferences.overWifi } returns OVER_NETWORK_ALWAYS
- every { preferences.overData } returns OVER_NETWORK_ALWAYS
-
- AppUpdateWorker.scheduleOrCancel(context)
-
- val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_AUTO_APP_UPDATE).get()
- assertEquals(1, workInfo.size)
- assertEquals(ENQUEUED, workInfo[0].state)
- val id = workInfo[0].id
-
- every { updateManager.updateApps() } returns true
-
- val testDriver = WorkManagerTestInitHelper.getTestDriver(context) ?: fail()
- testDriver.setPeriodDelayMet(id)
- testDriver.setAllConstraintsMet(id)
-
- verify { updateManager.updateApps() }
-
- val workInfo2 = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_AUTO_APP_UPDATE)
- .get()
- assertEquals(1, workInfo2.size)
- assertEquals(ENQUEUED, workInfo2[0].state) // stays enqueued for next time
- }
-
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java b/legacy/src/androidTest/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java
deleted file mode 100644
index 4e0a03ae6..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package org.fdroid.fdroid.work;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.app.Instrumentation;
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.work.OneTimeWorkRequest;
-import androidx.work.WorkInfo;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import org.apache.commons.io.FileUtils;
-import org.fdroid.fdroid.compat.FileCompatTest;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-
-/**
- * This test cannot run on Robolectric unfortunately since it does not support
- * getting the timestamps from the files completely.
- *
- * This is marked with {@link LargeTest} because it always fails on the emulator
- * tests on GitLab CI. That excludes it from the test run there.
- */
-@LargeTest
-public class CleanCacheWorkerTest {
- public static final String TAG = "CleanCacheWorkerEmulatorTest";
-
- @Rule
- public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
-
- @Rule
- public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
-
- @Test
- public void testWorkRequest() throws ExecutionException, InterruptedException {
- OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(CleanCacheWorker.class).build();
- workManagerTestRule.workManager.enqueue(request).getResult();
- ListenableFuture workInfo = workManagerTestRule.workManager.getWorkInfoById(request.getId());
- assertEquals(WorkInfo.State.SUCCEEDED, workInfo.get().getState());
- }
-
- @Test
- public void testClearOldFiles() throws IOException, InterruptedException {
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- File tempDir = FileCompatTest.getWriteableDir(instrumentation);
- assertTrue(tempDir.isDirectory());
- assertTrue(tempDir.canWrite());
-
- File dir = new File(tempDir, "F-Droid-test.clearOldFiles");
- FileUtils.deleteQuietly(dir);
- assertTrue(dir.mkdirs());
- assertTrue(dir.isDirectory());
-
- File first = new File(dir, "first");
- first.deleteOnExit();
-
- File second = new File(dir, "second");
- second.deleteOnExit();
-
- assertFalse(first.exists());
- assertFalse(second.exists());
-
- assertTrue(first.createNewFile());
- assertTrue(first.exists());
-
- Thread.sleep(7000);
- assertTrue(second.createNewFile());
- assertTrue(second.exists());
-
- CleanCacheWorker.clearOldFiles(dir, 3000); // check all in dir
- assertFalse(first.exists());
- assertTrue(second.exists());
-
- Thread.sleep(7000);
- CleanCacheWorker.clearOldFiles(second, 3000); // check just second file
- assertFalse(first.exists());
- assertFalse(second.exists());
-
- // make sure it doesn't freak out on a non-existent file
- File nonexistent = new File(tempDir, "nonexistent");
- CleanCacheWorker.clearOldFiles(nonexistent, 1);
- CleanCacheWorker.clearOldFiles(null, 1);
- }
-
- /*
- // TODO enable this once getImageCacheDir() can be mocked or provide a writable dir in the test
- @Test
- public void testDeleteOldIcons() throws IOException {
- Context context = InstrumentationRegistry.getInstrumentation().getContext();
- File imageCacheDir = Utils.getImageCacheDir(context);
- imageCacheDir.mkdirs();
- assertTrue(imageCacheDir.isDirectory());
- File oldIcon = new File(imageCacheDir, "old.png");
- assertTrue(oldIcon.createNewFile());
- Assume.assumeTrue("test environment must be able to set LastModified time",
- oldIcon.setLastModified(System.currentTimeMillis() - (DateUtils.DAY_IN_MILLIS * 370)));
- File currentIcon = new File(imageCacheDir, "current.png");
- assertTrue(currentIcon.createNewFile());
- CleanCacheWorker.deleteOldIcons(context);
- assertTrue(currentIcon.exists());
- assertFalse(oldIcon.exists());
- }
- */
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/work/FDroidMetricsWorkerTest.java b/legacy/src/androidTest/java/org/fdroid/fdroid/work/FDroidMetricsWorkerTest.java
deleted file mode 100644
index 19f6a8c46..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/work/FDroidMetricsWorkerTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2021 Hans-Christoph Steiner
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.work;
-
-import static org.junit.Assert.assertEquals;
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.work.OneTimeWorkRequest;
-import androidx.work.WorkInfo;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-
-/**
- * This actually runs {@link FDroidMetricsWorker} on a device/emulator and
- * submits a report to https://metrics.cleaninsights.org
- *
- * This is marked with {@link LargeTest} to exclude it from running on GitLab CI
- * because it always fails on the emulator tests there. Also, it actually submits
- * a report.
- */
-@LargeTest
-public class FDroidMetricsWorkerTest {
- public static final String TAG = "FDroidMetricsWorkerTest";
-
- @Rule
- public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
-
- @Rule
- public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
-
- /**
- * A test for easy manual testing.
- */
- @Ignore
- @Test
- public void testGenerateReport() throws IOException {
- String json = FDroidMetricsWorker.generateReport(
- InstrumentationRegistry.getInstrumentation().getTargetContext());
- System.out.println(json);
- }
-
- @Test
- public void testWorkRequest() throws ExecutionException, InterruptedException {
- OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(FDroidMetricsWorker.class).build();
- workManagerTestRule.workManager.enqueue(request).getResult();
- ListenableFuture workInfo = workManagerTestRule.workManager.getWorkInfoById(request.getId());
- assertEquals(WorkInfo.State.SUCCEEDED, workInfo.get().getState());
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/work/WorkManagerTestRule.java b/legacy/src/androidTest/java/org/fdroid/fdroid/work/WorkManagerTestRule.java
deleted file mode 100644
index c6a03d1b6..000000000
--- a/legacy/src/androidTest/java/org/fdroid/fdroid/work/WorkManagerTestRule.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.fdroid.fdroid.work;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.work.Configuration;
-import androidx.work.WorkManager;
-import androidx.work.testing.SynchronousExecutor;
-import androidx.work.testing.WorkManagerTestInitHelper;
-
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
-public class WorkManagerTestRule extends TestWatcher {
- WorkManager workManager;
-
- @Override
- protected void starting(Description description) {
- final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- Context targetContext = instrumentation.getTargetContext();
- Configuration configuration = new Configuration.Builder()
- .setMinimumLoggingLevel(Log.DEBUG)
- .setExecutor(new SynchronousExecutor())
- .build();
-
- WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration);
- workManager = WorkManager.getInstance(targetContext);
- }
-}
diff --git a/legacy/src/androidTest/java/org/fdroid/repo/RepoManagerAddAllIntegrationTest.kt b/legacy/src/androidTest/java/org/fdroid/repo/RepoManagerAddAllIntegrationTest.kt
deleted file mode 100644
index 15adaec8c..000000000
--- a/legacy/src/androidTest/java/org/fdroid/repo/RepoManagerAddAllIntegrationTest.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-package org.fdroid.repo
-
-import android.app.Application
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import app.cash.turbine.TurbineTestContext
-import app.cash.turbine.test
-import kotlinx.coroutines.runBlocking
-import org.fdroid.fdroid.data.DBHelper
-import org.fdroid.fdroid.net.DownloaderFactory
-import org.fdroid.index.RepoManager
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.TemporaryFolder
-import org.junit.runner.RunWith
-import org.slf4j.LoggerFactory.getLogger
-import kotlin.test.assertIs
-import kotlin.time.Duration.Companion.seconds
-
-@RunWith(AndroidJUnit4::class)
-internal class RepoManagerAddAllIntegrationTest {
-
- @get:Rule
- var folder: TemporaryFolder = TemporaryFolder()
-
- private val repos = listOf(
- "https://raw.githubusercontent.com/2br-2b/Fdroid-repo/master/fdroid/repo",
- "https://anonymousmessenger.ly/fdroid/repo",
- "https://fdroid.beocode.eu/fdroid/repo",
- "https://mobileapp.bitwarden.com/fdroid/repo",
- "https://briarproject.org/fdroid/repo",
- "https://fdroid.bromite.org/fdroid/repo",
- "https://fdroid.gitlab.io/ccc/fdroid/repo",
- "https://www.collaboraoffice.com/downloads/fdroid/repo",
- "https://bubu1.eu/cctg/fdroid/repo",
- "https://static.cryptomator.org/android/fdroid/repo",
- "https://lucaapp.gitlab.io/fdroid-repository/fdroid/repo",
- "https://divestos.org/apks/official/fdroid/repo",
- "https://divestos.org/apks/unofficial/fdroid/repo",
- "https://raw.githubusercontent.com/efreak/auto-daily-fdroid/main/fdroid/repo",
- "https://bubu1.eu/fdroidclassic/fdroid/repo",
- "https://f5a.typed.icu/fdroid/repo",
- "https://fdroid.fedilab.app/repo",
- "https://raw.githubusercontent.com/Tobi823/ffupdaterrepo/master/fdroid/repo",
- "https://rfc2822.gitlab.io/fdroid-firefox/fdroid/repo",
- "https://raw.githubusercontent.com/Five-Prayers/fdroid-repo-stable/main/fdroid/repo",
- "https://codeberg.org/florian-obernberger/fdroid-repo/raw/branch/main/repo",
- "https://fdroid.frostnerd.com/fdroid/repo",
- "https://pili.qi0.de/fdroid/repo",
- "https://gitjournal.io/fdroid/repo",
- "https://guardianproject.info/fdroid/repo",
- "https://s3.amazonaws.com/guardianproject/fdroid/repo",
- "https://guardianproject.info/fdroid/repo",
- "https://f-droid.i2p.io/repo",
- "https://iitc.app/fdroid/repo",
- "https://jhass.github.io/insporation/fdroid/repo",
- "https://raw.githubusercontent.com/iodeOS/fdroid/master/fdroid/repo",
- "https://apt.izzysoft.de/fdroid/repo",
- "https://android.izzysoft.de/repo",
- "https://jak-linux.org/fdroid/repo",
- "https://julianfairfax.gitlab.io/fdroid-repo/fdroid/repo",
- "https://kaffeemitkoffein.de/fdroid/repo",
- "https://store.nethunter.com/repo",
- "https://cdn.kde.org/android/stable-releases/fdroid/repo",
- "https://repo.kuschku.de/fdroid/repo",
- "https://fdroid.libretro.com/repo",
- "https://fdroid.ltheinrich.de/fdroid/repo",
- "https://ltt.rs/fdroid/repo",
- "https://pili.qi0.de/fdroid/repo",
- "https://fdroid.metatransapps.com/fdroid/repo",
- "https://microg.org/fdroid/repo",
- "https://fdroid.mm20.de/repo",
- "https://repo.mobilsicher.de/fdroid/repo",
- "https://molly.im/fdroid/repo",
- "https://molly.im/fdroid/foss/fdroid/repo",
- "https://f-droid.monerujo.io/fdroid/repo",
- "https://releases.nailyk.fr/repo",
- "https://nanolx.org/fdroid/repo",
- "https://www.nanolx.org/fdroid/repo",
- "https://repo.netsyms.com/fdroid/repo",
- "https://archive.newpipe.net/fdroid/repo",
- "https://repo.nononsenseapps.com/fdroid/repo",
- "https://fdroid.novy.software/repo",
- "https://raw.githubusercontent.com/nucleus-ffm/Nucleus-F-Droid-Repo/master/fdroid/repo",
- "https://obfusk.ch/fdroid/repo",
- "https://ouchadam.github.io/fdroid-repository/repo",
- "https://fdroid.partidopirata.com.ar/fdroid/repo",
- "https://thecapslock.gitlab.io/fdroid-patched-apps/fdroid/repo",
- "https://fdroid.i2pd.xyz/fdroid/repo",
- "https://fdroid.rami.io/fdroid/repo",
- "https://thedoc.eu.org/fdroid/repo",
- "https://repo.samourai.io/fdroid/repo",
- "https://fdroid.a3.pm/seabear/repo",
- "https://raw.githubusercontent.com/jackbonadies/seekerandroid/fdroid/fdroid/repo",
- "https://fdroid.getsession.org/fdroid/repo",
- "https://raw.githubusercontent.com/simlar/simlar-fdroid-repo/master/fdroid/repo",
- "https://s2.spiritcroc.de/fdroid/repo",
- "https://haagch.frickel.club/files/fdroid/repo",
- "https://submarine.strangled.net/fdroid/repo",
- "https://service.tagesschau.de/app/repo",
- "https://fdroid-repo.calyxinstitute.org/fdroid/repo",
- "https://releases.threema.ch/fdroid/repo",
- "https://raw.githubusercontent.com/chrisgch/tca/master/fdroid/repo",
- "https://fdroid.twinhelix.com/fdroid/repo",
- "https://secfirst.org/fdroid/repo",
- "https://fdroid.videlibri.de/repo",
- "https://guardianproject-wind.s3.amazonaws.com/fdroid/repo",
- "https://raw.githubusercontent.com/xarantolus/fdroid/main/fdroid/repo",
- "https://zimbelstern.eu/fdroid/repo",
- )
-
- private val log = getLogger(this::class.java.simpleName)
- private val context = ApplicationProvider.getApplicationContext()
- private val db = DBHelper.getDb(context) // real DB
- private val httpManager = DownloaderFactory.HTTP_MANAGER
- private val downloaderFactory = DownloaderFactory.INSTANCE
-
- private val repoManager = RepoManager(context, db, downloaderFactory, httpManager)
-
- @Before
- fun optIn() {
- // Careful! This will add lots of repos to your live DB
- assumeTrue(false) // don't run integration tests with real repos all the time
- }
-
- @Test
- fun addAllTheThings() = runBlocking {
- repos.forEach { addRepo(it) }
- }
-
- private suspend fun addRepo(url: String) {
- log.info("Fetching $url")
- repoManager.fetchRepositoryPreview(url = url, proxy = null)
- repoManager.addRepoState.test(timeout = 15.seconds) {
- val fetchState = awaitFinalFetchState()
- if (fetchState is Fetching && fetchState.fetchResult != null) {
- repoManager.addFetchedRepository()
- val item = awaitItem()
- if (item is Adding) {
- // await final state
- assertIs(awaitItem())
- } else {
- // was already final state
- assertIs(item)
- }
- log.info(" Added")
- } else if (fetchState is AddRepoError) {
- log.error(" $fetchState $url")
- }
- repoManager.abortAddingRepository()
- assertIs(awaitItem())
- cancelAndIgnoreRemainingEvents()
- }
- log.info("End $url")
- }
-
- private suspend fun TurbineTestContext.awaitFinalFetchState(): AddRepoState {
- var item = awaitItem()
- log.info(" $item")
- while (item is None || (item is Fetching && !item.done)) {
- item = awaitItem()
- log.info(" $item")
- }
- log.info(" final: $item")
- return item
- }
-}
diff --git a/legacy/src/androidTest/proguard-rules.pro b/legacy/src/androidTest/proguard-rules.pro
deleted file mode 100644
index ce7d3875f..000000000
--- a/legacy/src/androidTest/proguard-rules.pro
+++ /dev/null
@@ -1,35 +0,0 @@
--dontoptimize
--dontwarn
--dontobfuscate
-
--dontwarn android.test.**
--dontwarn android.support.test.**
--dontnote junit.framework.**
--dontnote junit.runner.**
-
-# Uncomment this if you use Mockito
-#-dontwarn org.mockito.**
-
--keep class org.hamcrest.** { *; }
--dontwarn org.hamcrest.**
-
--keep class org.junit.** { *; }
--dontwarn org.junit.**
-
--keep class junit.** { *; }
--dontwarn junit.**
-
--keep class kotlin.reflect.** { *; }
--keep class io.mockk.** { *; }
--keep class kotlin.io.** { *; }
--keep class kotlin.collections.** { *; }
--keep class java.util.concurrent.Executor { *; }
-
--keep class androidx.arch.core.executor.ArchTaskExecutor {*;}
-
--keep class org.fdroid.download.Mirror {
- public static *** fromStrings(***);
-}
-
-# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
--keep class androidx.work.** { *; }
diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/MainActivityEspressoTestFull.java b/legacy/src/androidTestFull/java/org/fdroid/fdroid/MainActivityEspressoTestFull.java
deleted file mode 100644
index 56ab7e5f4..000000000
--- a/legacy/src/androidTestFull/java/org/fdroid/fdroid/MainActivityEspressoTestFull.java
+++ /dev/null
@@ -1,303 +0,0 @@
-package org.fdroid.fdroid;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.action.ViewActions.swipeDown;
-import static androidx.test.espresso.action.ViewActions.swipeLeft;
-import static androidx.test.espresso.action.ViewActions.swipeRight;
-import static androidx.test.espresso.action.ViewActions.swipeUp;
-import static androidx.test.espresso.action.ViewActions.typeText;
-import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.Matchers.not;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import android.Manifest;
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.core.content.ContextCompat;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.IdlingPolicies;
-import androidx.test.espresso.ViewInteraction;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.rule.GrantPermissionRule;
-import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
-import androidx.test.uiautomator.UiSelector;
-
-import org.fdroid.fdroid.views.StatusBanner;
-import org.fdroid.fdroid.views.main.MainActivity;
-import org.hamcrest.Matchers;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class MainActivityEspressoTestFull {
- public static final String TAG = "MainActivityEspressoTestFull";
-
- /**
- * Emulators older than {@code android-25} seem to fail at running Espresso tests.
- *
- * ARM emulators are too slow to run these tests in a useful way. The sad
- * thing is that it would probably work if Android didn't put up the ANR
- * "Process system isn't responding" on boot each time. There seems to be no
- * way to increase the ANR timeout.
- */
- private static boolean canRunEspresso() {
- if (Build.VERSION.SDK_INT < 25
- || Build.SUPPORTED_ABIS[0].startsWith("arm") && isEmulator()) {
- Log.e(TAG, "SKIPPING TEST: ARM emulators are too slow to run these tests in a useful way");
- return false;
- }
- return true;
- }
-
- @BeforeClass
- public static void classSetUp() {
- IdlingPolicies.setIdlingResourceTimeout(10, TimeUnit.MINUTES);
- IdlingPolicies.setMasterPolicyTimeout(10, TimeUnit.MINUTES);
- if (!canRunEspresso()) {
- return;
- }
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- try {
- UiDevice.getInstance(instrumentation)
- .executeShellCommand("pm grant "
- + instrumentation.getTargetContext().getPackageName()
- + " android.permission.SET_ANIMATION_SCALE");
- } catch (IOException e) {
- e.printStackTrace();
- }
- SystemAnimations.disableAll(ApplicationProvider.getApplicationContext());
-
- // dismiss the ANR or any other system dialogs that might be there
- UiObject button = new UiObject(new UiSelector().text("Wait").enabled(true));
- try {
- button.click();
- } catch (UiObjectNotFoundException e) {
- Log.d(TAG, e.getLocalizedMessage());
- }
- new UiWatchers().registerAnrAndCrashWatchers();
-
- Context context = instrumentation.getTargetContext();
- ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
- ActivityManager activityManager = ContextCompat.getSystemService(context, ActivityManager.class);
- activityManager.getMemoryInfo(mi);
- long percentAvail = mi.availMem / mi.totalMem;
- Log.i(TAG, "RAM: " + mi.availMem + " / " + mi.totalMem + " = " + percentAvail);
- }
-
- @AfterClass
- public static void classTearDown() {
- SystemAnimations.enableAll(ApplicationProvider.getApplicationContext());
- }
-
- public static boolean isEmulator() {
- return Build.FINGERPRINT.startsWith("generic")
- || Build.FINGERPRINT.startsWith("unknown")
- || Build.MODEL.contains("google_sdk")
- || Build.MODEL.contains("Emulator")
- || Build.MODEL.contains("Android SDK built for x86")
- || Build.MANUFACTURER.contains("Genymotion")
- || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
- || "google_sdk".equals(Build.PRODUCT);
- }
-
- @Before
- public void setUp() {
- assumeTrue(canRunEspresso());
- }
-
- /**
- * Placate {@link android.os.StrictMode}
- *
- * @see Run finalizers before counting for StrictMode
- */
- @After
- public void tearDown() {
- System.gc();
- System.runFinalization();
- System.gc();
- }
-
- @Rule
- public ActivityTestRule activityTestRule =
- new ActivityTestRule<>(MainActivity.class);
-
- @Rule
- public GrantPermissionRule accessCoarseLocationPermissionRule = GrantPermissionRule.grant(
- Manifest.permission.ACCESS_COARSE_LOCATION);
-
- @Rule
- public GrantPermissionRule readExternalStoragePermissionRule = GrantPermissionRule.grant(
- Manifest.permission.READ_EXTERNAL_STORAGE);
-
- @Test
- public void bottomNavFlavorCheck() {
- onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
- onView(withText(R.string.menu_settings)).check(matches(isDisplayed()));
- onView(withText("THIS SHOULD NOT SHOW UP ANYWHERE!!!")).check(doesNotExist());
-
- assertTrue(BuildConfig.FLAVOR.startsWith("full") || BuildConfig.FLAVOR.startsWith("basic"));
-
- if (BuildConfig.FLAVOR.startsWith("basic")) {
- onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
- onView(withText(R.string.main_menu__categories)).check(doesNotExist());
- onView(withText(R.string.main_menu__swap_nearby)).check(doesNotExist());
- }
-
- if (BuildConfig.FLAVOR.startsWith("full")) {
- onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
- onView(withText(R.string.main_menu__categories)).check(matches(isDisplayed()));
- onView(withText(R.string.main_menu__swap_nearby)).check(matches(isDisplayed()));
- }
- }
-
- @LargeTest
- @Test
- public void showSettings() {
- ViewInteraction settingsBottonNavButton = onView(
- allOf(withText(R.string.menu_settings), isDisplayed()));
- settingsBottonNavButton.perform(click());
- onView(withText(R.string.preference_manage_installed_apps)).check(matches(isDisplayed()));
- if (BuildConfig.FLAVOR.startsWith("basic") && BuildConfig.APPLICATION_ID.endsWith(".debug")) {
- // TODO fix me by sorting out the flavor applicationId for debug builds in app/build.gradle
- Log.i(TAG, "Skipping the remainder of showSettings test because it just crashes on basic .debug builds");
- return;
- }
- ViewInteraction manageInstalledAppsButton = onView(
- allOf(withText(R.string.preference_manage_installed_apps), isDisplayed()));
- manageInstalledAppsButton.perform(click());
- onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
- onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
-
- onView(withText(R.string.menu_manage)).perform(click());
- onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
-
- manageInstalledAppsButton.perform(click());
- onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
- onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
-
- onView(withText(R.string.menu_manage)).perform(click());
- onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
-
- onView(withText(R.string.about_title)).perform(click());
- onView(withId(R.id.version)).check(matches(isDisplayed()));
- onView(withId(R.id.ok_button)).perform(click());
-
- onView(withId(android.R.id.list_container)).perform(swipeUp()).perform(swipeUp()).perform(swipeUp());
- }
-
- @LargeTest
- @Test
- public void showUpdates() {
- ViewInteraction updatesBottonNavButton = onView(allOf(withText(R.string.main_menu__updates), isDisplayed()));
- updatesBottonNavButton.perform(click());
- onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
- }
-
- @LargeTest
- @Test
- public void startSwap() {
- if (!BuildConfig.FLAVOR.startsWith("full")) {
- return;
- }
- ViewInteraction nearbyBottonNavButton = onView(
- allOf(withText(R.string.main_menu__swap_nearby), isDisplayed()));
- nearbyBottonNavButton.perform(click());
- ViewInteraction findPeopleButton = onView(
- allOf(withId(R.id.find_people_button), withText(R.string.nearby_splash__find_people_button),
- isDisplayed()));
- findPeopleButton.perform(click());
- onView(withText(R.string.swap_send_fdroid)).check(matches(isDisplayed()));
- }
-
- @LargeTest
- @Test
- public void showCategories() {
- if (!BuildConfig.FLAVOR.startsWith("full")) {
- return;
- }
- onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
- onView(allOf(withText(R.string.main_menu__categories), isDisplayed())).perform(click());
- onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
- .perform(swipeDown())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeDown())
- .perform(swipeDown())
- .perform(swipeRight())
- .perform(swipeLeft())
- .perform(swipeLeft())
- .perform(swipeLeft())
- .perform(swipeLeft())
- .perform(click());
- }
-
- @LargeTest
- @Test
- public void showLatest() {
- if (!BuildConfig.FLAVOR.startsWith("full")) {
- return;
- }
- onView(Matchers.instanceOf(StatusBanner.class)).check(matches(not(isDisplayed())));
- onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
- onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
- onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
- .perform(swipeDown())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeUp())
- .perform(swipeDown())
- .perform(swipeUp())
- .perform(swipeDown())
- .perform(swipeDown())
- .perform(swipeDown())
- .perform(swipeDown())
- .perform(click());
- }
-
- @LargeTest
- @Test
- public void showSearch() {
- onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
- onView(withId(R.id.fab_search)).check(doesNotExist());
- if (!BuildConfig.FLAVOR.startsWith("full")) {
- return;
- }
- onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
- onView(allOf(withId(R.id.fab_search), isDisplayed())).perform(click());
- onView(withId(R.id.sort)).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.search), isDisplayed()))
- .perform(click())
- .perform(typeText("test"));
- onView(allOf(withId(R.id.sort), isDisplayed())).perform(click());
- }
-}
diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/BonjourManagerTest.java b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/BonjourManagerTest.java
deleted file mode 100644
index 918e7f3c7..000000000
--- a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/BonjourManagerTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package org.fdroid.fdroid.nearby;
-
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.fdroid.fdroid.FDroidApp;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import javax.jmdns.ServiceEvent;
-import javax.jmdns.ServiceListener;
-
-@RunWith(AndroidJUnit4.class)
-public class BonjourManagerTest {
-
- private static final String NAME = "Robolectric-test";
- private static final String LOCALHOST = "localhost";
- private static final int PORT = 8888;
-
- @Test
- public void testStartStop() throws InterruptedException {
- Context context = ApplicationProvider.getApplicationContext();
-
- FDroidApp.ipAddressString = LOCALHOST;
- FDroidApp.port = PORT;
-
- final CountDownLatch addedLatch = new CountDownLatch(1);
- final CountDownLatch resolvedLatch = new CountDownLatch(1);
- final CountDownLatch removedLatch = new CountDownLatch(1);
- BonjourManager.start(context, NAME, false,
- new ServiceListener() {
- @Override
- public void serviceAdded(ServiceEvent serviceEvent) {
- System.out.println("Service added: " + serviceEvent.getInfo());
- if (NAME.equals(serviceEvent.getName())) {
- addedLatch.countDown();
- }
- }
-
- @Override
- public void serviceRemoved(ServiceEvent serviceEvent) {
- System.out.println("Service removed: " + serviceEvent.getInfo());
- removedLatch.countDown();
- }
-
- @Override
- public void serviceResolved(ServiceEvent serviceEvent) {
- System.out.println("Service resolved: " + serviceEvent.getInfo());
- if (NAME.equals(serviceEvent.getName())) {
- resolvedLatch.countDown();
- }
- }
- }, getBlankServiceListener());
- BonjourManager.setVisible(context, true);
- assertTrue(addedLatch.await(30, TimeUnit.SECONDS));
- assertTrue(resolvedLatch.await(30, TimeUnit.SECONDS));
- BonjourManager.setVisible(context, false);
- assertTrue(removedLatch.await(30, TimeUnit.SECONDS));
- BonjourManager.stop(context);
- }
-
- @Test
- public void testRestart() throws InterruptedException {
- Context context = ApplicationProvider.getApplicationContext();
-
- FDroidApp.ipAddressString = LOCALHOST;
- FDroidApp.port = PORT;
-
- BonjourManager.start(context, NAME, false, getBlankServiceListener(), getBlankServiceListener());
-
- final CountDownLatch addedLatch = new CountDownLatch(1);
- final CountDownLatch resolvedLatch = new CountDownLatch(1);
- final CountDownLatch removedLatch = new CountDownLatch(1);
- BonjourManager.restart(context, NAME, false,
- new ServiceListener() {
- @Override
- public void serviceAdded(ServiceEvent serviceEvent) {
- System.out.println("Service added: " + serviceEvent.getInfo());
- if (NAME.equals(serviceEvent.getName())) {
- addedLatch.countDown();
- }
- }
-
- @Override
- public void serviceRemoved(ServiceEvent serviceEvent) {
- System.out.println("Service removed: " + serviceEvent.getInfo());
- removedLatch.countDown();
- }
-
- @Override
- public void serviceResolved(ServiceEvent serviceEvent) {
- System.out.println("Service resolved: " + serviceEvent.getInfo());
- if (NAME.equals(serviceEvent.getName())) {
- resolvedLatch.countDown();
- }
- }
- }, getBlankServiceListener());
- BonjourManager.setVisible(context, true);
- assertTrue(addedLatch.await(30, TimeUnit.SECONDS));
- assertTrue(resolvedLatch.await(30, TimeUnit.SECONDS));
- BonjourManager.setVisible(context, false);
- assertTrue(removedLatch.await(30, TimeUnit.SECONDS));
- BonjourManager.stop(context);
- }
-
- private ServiceListener getBlankServiceListener() {
- return new ServiceListener() {
- @Override
- public void serviceAdded(ServiceEvent serviceEvent) {
- }
-
- @Override
- public void serviceRemoved(ServiceEvent serviceEvent) {
- }
-
- @Override
- public void serviceResolved(ServiceEvent serviceEvent) {
- }
- };
- }
-}
diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt
deleted file mode 100644
index a185ed728..000000000
--- a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.fdroid.fdroid.nearby
-
-import java.io.File
-import java.io.InputStream
-
-object CopyUtils {
-
- @JvmStatic
- fun copyInputStreamToFile(inputStream: InputStream, file: File) {
- inputStream.use { input -> file.outputStream().use { output -> input.copyTo(output) } }
- }
-}
diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/LocalHTTPDManagerTest.java b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/LocalHTTPDManagerTest.java
deleted file mode 100644
index d3d31747f..000000000
--- a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/LocalHTTPDManagerTest.java
+++ /dev/null
@@ -1,195 +0,0 @@
-package org.fdroid.fdroid.nearby;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.Log;
-
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Netstat;
-import org.fdroid.fdroid.Utils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Test the nearby webserver in the emulator.
- */
-@Ignore // TODO this test has worked in the past, but needs work.
-@RunWith(AndroidJUnit4.class)
-public class LocalHTTPDManagerTest {
- private static final String TAG = "LocalHTTPDManagerTest";
-
- private Context context;
- private LocalBroadcastManager lbm;
-
- private static final String LOCALHOST = "localhost";
- private static final int PORT = 8888;
-
- @Before
- public void setUp() {
- context = ApplicationProvider.getApplicationContext();
- lbm = LocalBroadcastManager.getInstance(context);
-
- FDroidApp.ipAddressString = LOCALHOST;
- FDroidApp.port = PORT;
-
- for (Netstat.Connection connection : Netstat.getConnections()) { // NOPMD
- Log.i("LocalHTTPDManagerTest", "connection: " + connection.toString());
- }
- assertFalse(Utils.isServerSocketInUse(PORT));
- LocalHTTPDManager.stop(context);
-
- for (Netstat.Connection connection : Netstat.getConnections()) { // NOPMD
- Log.i("LocalHTTPDManagerTest", "connection: " + connection.toString());
- }
- }
-
- @After
- public void tearDown() {
- lbm.unregisterReceiver(startedReceiver);
- lbm.unregisterReceiver(stoppedReceiver);
- lbm.unregisterReceiver(errorReceiver);
- }
-
- @Ignore
- @Test
- public void testStartStop() throws InterruptedException {
- Log.i(TAG, "testStartStop");
-
- final CountDownLatch startLatch = new CountDownLatch(1);
- BroadcastReceiver latchReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- startLatch.countDown();
- }
- };
- lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
- lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
- lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
- LocalHTTPDManager.start(context, false);
- assertTrue(startLatch.await(30, TimeUnit.SECONDS));
- assertTrue(Utils.isServerSocketInUse(PORT));
- assertTrue(Utils.canConnectToSocket(LOCALHOST, PORT));
- lbm.unregisterReceiver(latchReceiver);
- lbm.unregisterReceiver(stoppedReceiver);
- lbm.unregisterReceiver(errorReceiver);
-
- final CountDownLatch stopLatch = new CountDownLatch(1);
- latchReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- stopLatch.countDown();
- }
- };
- lbm.registerReceiver(startedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
- lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
- lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
- LocalHTTPDManager.stop(context);
- assertTrue(stopLatch.await(30, TimeUnit.SECONDS));
- assertFalse(Utils.isServerSocketInUse(PORT));
- assertFalse(Utils.canConnectToSocket(LOCALHOST, PORT)); // if this is flaky, just remove it
- lbm.unregisterReceiver(latchReceiver);
- }
-
- @Test
- public void testError() throws InterruptedException, IOException {
- Log.i("LocalHTTPDManagerTest", "testError");
- ServerSocket blockerSocket = new ServerSocket(PORT);
-
- final CountDownLatch latch = new CountDownLatch(1);
- BroadcastReceiver latchReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- latch.countDown();
- }
- };
- lbm.registerReceiver(startedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
- lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
- lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
- LocalHTTPDManager.start(context, false);
- assertTrue(latch.await(30, TimeUnit.SECONDS));
- assertTrue(Utils.isServerSocketInUse(PORT));
- assertNotEquals(PORT, FDroidApp.port);
- assertFalse(Utils.isServerSocketInUse(FDroidApp.port));
- lbm.unregisterReceiver(latchReceiver);
- blockerSocket.close();
- }
-
- @Test
- public void testRestart() throws InterruptedException, IOException {
- Log.i("LocalHTTPDManagerTest", "testRestart");
- assertFalse(Utils.isServerSocketInUse(PORT));
- final CountDownLatch startLatch = new CountDownLatch(1);
- BroadcastReceiver latchReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- startLatch.countDown();
- }
- };
- lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
- lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
- lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
- LocalHTTPDManager.start(context, false);
- assertTrue(startLatch.await(30, TimeUnit.SECONDS));
- assertTrue(Utils.isServerSocketInUse(PORT));
- lbm.unregisterReceiver(latchReceiver);
- lbm.unregisterReceiver(stoppedReceiver);
-
- final CountDownLatch restartLatch = new CountDownLatch(1);
- latchReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- restartLatch.countDown();
- }
- };
- lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
- LocalHTTPDManager.restart(context, false);
- assertTrue(restartLatch.await(30, TimeUnit.SECONDS));
- lbm.unregisterReceiver(latchReceiver);
- }
-
- private final BroadcastReceiver startedReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String message = intent.getStringExtra(Intent.EXTRA_TEXT);
- Log.i(TAG, "startedReceiver: " + message);
- fail();
- }
- };
-
- private final BroadcastReceiver stoppedReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String message = intent.getStringExtra(Intent.EXTRA_TEXT);
- Log.i(TAG, "stoppedReceiver: " + message);
- fail();
- }
- };
-
- private final BroadcastReceiver errorReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String message = intent.getStringExtra(Intent.EXTRA_TEXT);
- Log.i(TAG, "errorReceiver: " + message);
- fail();
- }
- };
-}
diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java
deleted file mode 100644
index 93c29bfe6..000000000
--- a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package org.fdroid.fdroid.nearby;
-
-import static org.fdroid.fdroid.nearby.CopyUtils.copyInputStreamToFile;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.MediaStore;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-public class PublicSourceDirProviderTest {
- public static final String TAG = "DataApkProviderTest";
-
- Context context;
- List packageInfoList;
-
- @Before
- public void setUp() {
- context = InstrumentationRegistry.getInstrumentation().getTargetContext();
- packageInfoList = null;
- }
-
- /**
- * Test whether reading installed APKs via our custom {@link android.content.ContentProvider}
- * works. This skips system apps just to make the test easier to manage. It also only
- * copies max 3 apps so it doesn't take a long time to run.
- */
- @Test
- public void testCopyFromGetUri() throws IOException {
- int copyTotal = 3;
- PackageManager pm = context.getPackageManager();
- List packageInfoList = pm.getInstalledPackages(0);
- for (PackageInfo packageInfo : packageInfoList) {
- File apk = new File(packageInfo.applicationInfo.publicSourceDir);
- if (apk.getCanonicalPath().startsWith("/system")) {
- continue;
- }
- Uri uri = PublicSourceDirProvider.getUri(context, packageInfo.packageName);
- InputStream is = null;
- File f = null;
- is = context.getContentResolver().openInputStream(uri);
- f = File.createTempFile("received", ".apk");
- assertNotNull(is);
- copyInputStreamToFile(is, f);
- assertTrue("dest file " + f + " should exist", f.exists());
- assertEquals(f + " should equal " + apk, apk.length(), f.length());
- f.delete();
-
- copyTotal--;
- if (copyTotal < 0) {
- break;
- }
- }
- }
-
- /**
- * Test whether querying the custom {@link android.content.ContentProvider}
- * for installed APKs returns the right kind of data.
- */
- @Test
- public void testQuery() throws IOException {
- PackageManager pm = context.getPackageManager();
- List packageInfoList = pm.getInstalledPackages(0);
- for (PackageInfo packageInfo : packageInfoList) {
- File apk = new File(packageInfo.applicationInfo.publicSourceDir);
- if (apk.getCanonicalPath().startsWith("/system")) {
- continue;
- }
- Uri uri = PublicSourceDirProvider.getUri(context, packageInfo.packageName);
- Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
- assertNotNull(cursor);
-
- cursor.moveToFirst();
- while (!cursor.isAfterLast()) {
- assertNotNull(cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)));
- cursor.moveToNext();
- }
- cursor.close();
- }
- }
-}
diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java b/legacy/src/androidTestFull/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java
deleted file mode 100644
index 6e4879fb8..000000000
--- a/legacy/src/androidTestFull/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-package org.fdroid.fdroid.updater;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Looper;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.fdroid.fdroid.BuildConfig;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Hasher;
-import org.fdroid.fdroid.Preferences;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.nearby.LocalHTTPD;
-import org.fdroid.fdroid.nearby.LocalRepoKeyStore;
-import org.fdroid.fdroid.nearby.LocalRepoManager;
-import org.fdroid.fdroid.nearby.LocalRepoService;
-import org.fdroid.fdroid.nearby.SwapService;
-import org.fdroid.index.v1.IndexV1;
-import org.fdroid.index.v1.PackageV1;
-import org.junit.Test;
-
-import java.io.File;
-import java.security.cert.Certificate;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@LargeTest
-public class SwapRepoEmulatorTest {
- public static final String TAG = "SwapRepoEmulatorTest";
-
- /**
- * @see org.fdroid.fdroid.nearby.WifiStateChangeService.WifiInfoThread#run()
- */
- @Test
- public void testSwap() throws Exception {
- Looper.prepare();
- LocalHTTPD localHttpd = null;
- try {
- Log.i(TAG, "REPO: " + FDroidApp.repo);
- final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
- Preferences.setupForTests(context);
-
- FDroidApp.initWifiSettings();
- assertNull(FDroidApp.repo);
-
- final CountDownLatch latch = new CountDownLatch(1);
- new Thread() {
- @Override
- public void run() {
- while (FDroidApp.repo == null) {
- try {
- String address = FDroidApp.repo == null ? null : FDroidApp.repo.getAddress();
- Log.i(TAG, "Waiting for IP address... " + address);
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- // ignored
- }
- }
- latch.countDown();
- }
- }.start();
- latch.await(10, TimeUnit.MINUTES);
- assertNotNull(FDroidApp.repo.getAddress());
-
- LocalRepoService.runProcess(context, new String[]{context.getPackageName()});
- Log.i(TAG, "REPO: " + FDroidApp.repo);
- File indexJarFile = LocalRepoManager.get(context).getIndexJar();
- assertTrue(indexJarFile.isFile());
-
- localHttpd = new LocalHTTPD(
- context,
- FDroidApp.ipAddressString,
- FDroidApp.port,
- LocalRepoManager.get(context).getWebRoot(),
- false);
- localHttpd.start();
- Thread.sleep(100); // give the server some tine to start.
- assertTrue(localHttpd.isAlive());
-
- LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
- Certificate localCert = localRepoKeyStore.getCertificate();
- String fingerprint = Utils.calcFingerprint(localCert).toLowerCase(Locale.ROOT);
- String signingCert = Hasher.hex(localCert);
- assertFalse(TextUtils.isEmpty(signingCert));
- assertFalse(TextUtils.isEmpty(fingerprint));
-
- assertTrue(Utils.isPortInUse(FDroidApp.ipAddressString, FDroidApp.port));
- Thread.sleep(100);
-
- File swapJarFile = File.createTempFile("swap", "", context.getCacheDir());
- IndexV1 indexV1 = SwapService.getVerifiedRepoIndex(FDroidApp.repo, fingerprint, swapJarFile);
- assertEquals(1, indexV1.getApps().size());
- assertEquals(context.getPackageName(), indexV1.getApps().get(0).getPackageName());
- long firstTimestamp = indexV1.getRepo().getTimestamp();
-
- assertEquals(1, indexV1.getPackages().size());
- List apks = indexV1.getPackages().get(context.getPackageName());
- assertNotNull(apks);
- assertEquals(1, apks.size());
- for (PackageV1 apk : apks) {
- Log.i(TAG, "Apk: " + apk);
- assertNotNull(apk.getVersionCode());
- long versionCode = apk.getVersionCode();
- assertEquals(context.getPackageName(), apk.getPackageName());
- assertEquals(BuildConfig.VERSION_NAME, apk.getVersionName());
- assertEquals(BuildConfig.VERSION_CODE, versionCode);
- }
-
- Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
- mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- List resolveInfoList = context.getPackageManager().queryIntentActivities(mainIntent, 0);
- HashSet packageNames = new HashSet<>();
- for (ResolveInfo resolveInfo : resolveInfoList) {
- if (!isSystemPackage(resolveInfo)) {
- Log.i(TAG, "resolveInfo: " + resolveInfo);
- packageNames.add(resolveInfo.activityInfo.packageName);
- }
- }
- LocalRepoService.runProcess(context, packageNames.toArray(new String[0]));
-
- swapJarFile = File.createTempFile("swap", "", context.getCacheDir());
- indexV1 = SwapService.getVerifiedRepoIndex(FDroidApp.repo, fingerprint, swapJarFile);
- assertTrue(firstTimestamp < indexV1.getRepo().getTimestamp());
- for (String packageName : packageNames) {
- assertNotNull(indexV1.getPackages().get(packageName));
- }
- } finally {
- if (localHttpd != null) {
- localHttpd.stop();
- }
- }
- assertFalse(localHttpd.isAlive());
- }
-
- private boolean isSystemPackage(ResolveInfo resolveInfo) {
- return (resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/nearby/BluetoothClient.java b/legacy/src/basic/java/org/fdroid/fdroid/nearby/BluetoothClient.java
deleted file mode 100644
index 70fd65be9..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/nearby/BluetoothClient.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2018 Senecto Limited
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.nearby;
-
-/**
- * Dummy version for basic app flavor.
- */
-
-public class BluetoothClient {
-
- public BluetoothClient(String ignored) {
- }
-
- public BluetoothConnection openConnection() {
- return null;
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/nearby/LocalRepoManager.java b/legacy/src/basic/java/org/fdroid/fdroid/nearby/LocalRepoManager.java
deleted file mode 100644
index b1cb50b71..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/nearby/LocalRepoManager.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package org.fdroid.fdroid.nearby;
-
-public class LocalRepoManager {
- public static final String[] WEB_ROOT_ASSET_FILES = {};
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/nearby/SDCardScannerService.java b/legacy/src/basic/java/org/fdroid/fdroid/nearby/SDCardScannerService.java
deleted file mode 100644
index c1d7d5ba1..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/nearby/SDCardScannerService.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2018 Hans-Christoph Steiner
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.nearby;
-
-import android.content.Context;
-
-/**
- * Dummy version for basic app flavor.
- */
-public class SDCardScannerService {
- public static void scan(Context context) {
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/nearby/SwapService.java b/legacy/src/basic/java/org/fdroid/fdroid/nearby/SwapService.java
deleted file mode 100644
index 35b645e8a..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/nearby/SwapService.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2018 Hans-Christoph Steiner
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.nearby;
-
-import android.content.Context;
-
-/**
- * Dummy version for basic app flavor.
- */
-public class SwapService {
- public static void start(Context context) {
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/legacy/src/basic/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java
deleted file mode 100644
index dc9993e55..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2018 Senecto Limited
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.nearby;
-
-import android.content.Context;
-import android.net.Uri;
-
-/**
- * Dummy version for basic app flavor.
- */
-public class SwapWorkflowActivity {
-
- public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap";
-
- public static void requestSwap(Context context, Uri uri) {
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java b/legacy/src/basic/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java
deleted file mode 100644
index 497073038..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2018 Senecto Limited
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.nearby;
-
-import android.content.Intent;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-/**
- * Dummy version for basic app flavor.
- */
-public class TreeUriScannerIntentService {
- public static void onActivityResult(AppCompatActivity activity, Intent intent) {
- throw new IllegalStateException("unimplemented");
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java b/legacy/src/basic/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java
deleted file mode 100644
index 40506dcdd..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2018 Senecto Limited
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.nearby;
-
-import android.content.Context;
-import android.content.Intent;
-
-import androidx.annotation.Nullable;
-
-/**
- * Dummy version for basic app flavor.
- */
-public class WifiStateChangeService {
-
- public static void registerReceiver(Context context) {
- }
-
- public static void start(Context context, @Nullable Intent intent) {
- }
-
- public class WifiInfoThread extends Thread {
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/panic/HidingManager.java b/legacy/src/basic/java/org/fdroid/fdroid/panic/HidingManager.java
deleted file mode 100644
index 76b55de66..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/panic/HidingManager.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2018 Senecto Limited
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.panic;
-
-import android.content.Context;
-
-/**
- * Dummy version for basic app flavor.
- */
-public class HidingManager {
-
- public static boolean isHidden(Context context) {
- return false;
- }
-
- public static void showHideDialog(final Context context) {
- throw new IllegalStateException("unimplemented");
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/views/main/LatestLayoutPolicy.java b/legacy/src/basic/java/org/fdroid/fdroid/views/main/LatestLayoutPolicy.java
deleted file mode 100644
index b2fa28ee6..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/views/main/LatestLayoutPolicy.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.fdroid.fdroid.views.main;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.fdroid.fdroid.R;
-
-class LatestLayoutPolicy {
- private final Context context;
-
- LatestLayoutPolicy(Context context) {
- this.context = context.getApplicationContext();
- }
-
- RecyclerView.ItemDecoration getItemDecoration() {
- return new RecyclerView.ItemDecoration() {
- @Override
- public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
- @NonNull RecyclerView.State state) {
- super.getItemOffsets(outRect, view, parent, state);
- Resources resources = context.getResources();
- int padding = (int) resources.getDimension(R.dimen.latest__padding__app_card__normal);
- outRect.set(padding, padding, padding, 0);
- }
- };
- }
-
- /** @noinspection unused*/
- int getItemViewType(int position) {
- return R.id.latest_regular_list;
- }
-
- /** @noinspection unused*/
- int getSpanSize(int position) {
- return 2;
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/views/main/MainViewController.java b/legacy/src/basic/java/org/fdroid/fdroid/views/main/MainViewController.java
deleted file mode 100644
index b9c8dd18b..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/views/main/MainViewController.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2018 Senecto Limited
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.views.main;
-
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.views.PreferencesFragment;
-import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
-
-/**
- * Decides which view on the main screen to attach to a given {@link FrameLayout}. This class
- * doesn't know which view it will be rendering at the time it is constructed. Rather, at some
- * point in the future the {@link MainViewAdapter} will have information about which view we
- * are required to render, and will invoke the relevant "bind*()" method on this class.
- */
-class MainViewController extends RecyclerView.ViewHolder {
-
- private final AppCompatActivity activity;
- private final FrameLayout frame;
-
- @Nullable
- private UpdatesViewBinder updatesView = null;
-
- MainViewController(AppCompatActivity activity, FrameLayout frame) {
- super(frame);
- this.activity = activity;
- this.frame = frame;
- }
-
- /**
- * @see LatestViewBinder
- */
- void bindLatestView() {
- new LatestViewBinder(activity, frame);
- }
-
- /**
- * @see UpdatesViewBinder
- */
- void bindUpdates() {
- if (updatesView == null) {
- updatesView = new UpdatesViewBinder(activity, frame);
- }
-
- updatesView.bind();
- }
-
- void unbindUpdates() {
- if (updatesView != null) {
- updatesView.unbind();
- }
- }
-
- /**
- * @see CategoriesViewBinder
- */
- void bindCategoriesView() {
- new CategoriesViewBinder(activity, frame);
- }
-
- void bindSwapView() {
- throw new IllegalStateException("unimplemented");
- }
-
- /**
- * Attaches a {@link PreferencesFragment} to the view. Everything else is managed by the
- * fragment itself, so no further work needs to be done by this view binder.
- *
- * Note: It is tricky to attach a {@link Fragment} to a view from this view holder. This is due
- * to the way in which the {@link RecyclerView} will reuse existing views and ask us to
- * put a settings fragment in there at arbitrary times. Usually it won't be the same view we
- * attached the fragment to last time, which causes weirdness. The solution is to use code from
- * the com.lsjwzh.widget.recyclerviewpager.FragmentStatePagerAdapter which manages this.
- * The code has been ported to {@link SettingsView}.
- *
- * @see SettingsView
- */
- void bindSettingsView() {
- activity.getLayoutInflater().inflate(R.layout.main_tab_settings, frame, true);
- }
-}
diff --git a/legacy/src/basic/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java b/legacy/src/basic/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java
deleted file mode 100644
index 8446e2255..000000000
--- a/legacy/src/basic/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.fdroid.fdroid.views.main;
-
-import android.content.Context;
-
-class NearbyViewBinder {
- static void updateUsbOtg(Context context) {
- throw new IllegalStateException("unimplemented");
- }
- static void updateExternalStorageViews(Context context) {
- }
-}
diff --git a/legacy/src/basic/res/drawable-anydpi-v26/ic_launcher_foreground.xml b/legacy/src/basic/res/drawable-anydpi-v26/ic_launcher_foreground.xml
deleted file mode 100644
index 06e9368a8..000000000
--- a/legacy/src/basic/res/drawable-anydpi-v26/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/legacy/src/basic/res/drawable-hdpi/ic_launcher.png b/legacy/src/basic/res/drawable-hdpi/ic_launcher.png
deleted file mode 100644
index df5c1af7d..000000000
Binary files a/legacy/src/basic/res/drawable-hdpi/ic_launcher.png and /dev/null differ
diff --git a/legacy/src/basic/res/drawable-ldpi/ic_launcher.png b/legacy/src/basic/res/drawable-ldpi/ic_launcher.png
deleted file mode 100644
index f86138bfb..000000000
Binary files a/legacy/src/basic/res/drawable-ldpi/ic_launcher.png and /dev/null differ
diff --git a/legacy/src/basic/res/drawable-mdpi/ic_launcher.png b/legacy/src/basic/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index 097c10a45..000000000
Binary files a/legacy/src/basic/res/drawable-mdpi/ic_launcher.png and /dev/null differ
diff --git a/legacy/src/basic/res/drawable-xhdpi/ic_launcher.png b/legacy/src/basic/res/drawable-xhdpi/ic_launcher.png
deleted file mode 100644
index ba65e7d87..000000000
Binary files a/legacy/src/basic/res/drawable-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/legacy/src/basic/res/drawable-xxhdpi/ic_launcher.png b/legacy/src/basic/res/drawable-xxhdpi/ic_launcher.png
deleted file mode 100644
index a0b5fbc81..000000000
Binary files a/legacy/src/basic/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/legacy/src/basic/res/drawable-xxxhdpi/ic_launcher.png b/legacy/src/basic/res/drawable-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 712af58d2..000000000
Binary files a/legacy/src/basic/res/drawable-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/legacy/src/basic/res/drawable/ic_launcher_monochrome.xml b/legacy/src/basic/res/drawable/ic_launcher_monochrome.xml
deleted file mode 100644
index 63d62f05b..000000000
--- a/legacy/src/basic/res/drawable/ic_launcher_monochrome.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/legacy/src/basic/res/values/attrs.xml b/legacy/src/basic/res/values/attrs.xml
deleted file mode 100644
index ffc7c9504..000000000
--- a/legacy/src/basic/res/values/attrs.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- false
-
diff --git a/legacy/src/basic/res/values/strings.xml b/legacy/src/basic/res/values/strings.xml
deleted file mode 100644
index 52aeffdac..000000000
--- a/legacy/src/basic/res/values/strings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- @string/app_name_basic
- F-Droid Basic Debug
- @string/about_title_basic
-
diff --git a/legacy/src/debug/AndroidManifest.xml b/legacy/src/debug/AndroidManifest.xml
deleted file mode 100644
index 7387a300a..000000000
--- a/legacy/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/legacy/src/full/AndroidManifest.xml b/legacy/src/full/AndroidManifest.xml
deleted file mode 100644
index 53afeb558..000000000
--- a/legacy/src/full/AndroidManifest.xml
+++ /dev/null
@@ -1,192 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/legacy/src/full/java/cc/mvdan/accesspoint/WifiApControl.java b/legacy/src/full/java/cc/mvdan/accesspoint/WifiApControl.java
deleted file mode 100644
index b77fbced7..000000000
--- a/legacy/src/full/java/cc/mvdan/accesspoint/WifiApControl.java
+++ /dev/null
@@ -1,417 +0,0 @@
-/**
- * Copyright 2015 Daniel Martí
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package cc.mvdan.accesspoint;
-
-import android.content.Context;
-import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiManager;
-import android.provider.Settings;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import org.fdroid.fdroid.BuildConfig;
-
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.regex.Pattern;
-
-/**
- * WifiApControl provides control over Wi-Fi APs using the singleton pattern.
- * Even though isSupported should be reliable, the underlying hidden APIs that
- * are obtained via reflection to provide the main features may not work as
- * expected.
- *
- * TODO Note that this project is **abandoned** since its method doesn't work on Android
- * 7.1 or later. Have a look at these newer alternatives that have been tested to
- * work on Android 8.0:
- *
- * @see shinilms/direct-net-share
- * @see geekywoman/direct-net-share
- * @see aegis1980/WifiHotSpot
- */
-final public class WifiApControl {
-
- private static final String TAG = "WifiApControl";
-
- private static Method getWifiApConfigurationMethod;
- private static Method getWifiApStateMethod;
- private static Method isWifiApEnabledMethod;
- private static Method setWifiApEnabledMethod;
-
- public static final int WIFI_AP_STATE_DISABLING = 10;
- public static final int WIFI_AP_STATE_DISABLED = 11;
- public static final int WIFI_AP_STATE_ENABLING = 12;
- public static final int WIFI_AP_STATE_ENABLED = 13;
- public static final int WIFI_AP_STATE_FAILED = 14;
-
- public static final int STATE_DISABLING = WIFI_AP_STATE_DISABLING;
- public static final int STATE_DISABLED = WIFI_AP_STATE_DISABLED;
- public static final int STATE_ENABLING = WIFI_AP_STATE_ENABLING;
- public static final int STATE_ENABLED = WIFI_AP_STATE_ENABLED;
- public static final int STATE_FAILED = WIFI_AP_STATE_FAILED;
-
- private static boolean isSoftwareSupported() {
- return (getWifiApStateMethod != null
- && isWifiApEnabledMethod != null
- && setWifiApEnabledMethod != null
- && getWifiApConfigurationMethod != null);
- }
-
- private static boolean isHardwareSupported() {
- // TODO: implement via native code
- return true;
- }
-
- // isSupported reports whether Wi-Fi APs are supported by this device.
- public static boolean isSupported() {
- return isSoftwareSupported() && isHardwareSupported();
- }
-
- private static final String FALLBACK_DEVICE = "wlan0";
-
- private final WifiManager wm;
- private final String deviceName;
-
- private static WifiApControl instance = null;
-
- private WifiApControl(Context context) {
- wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
- deviceName = getDeviceName(wm);
- }
-
- // getInstance is a standard singleton instance getter, constructing
- // the actual class when first called.
- @Nullable
- public static WifiApControl getInstance(Context context) {
- if (instance == null) {
- if (!Settings.System.canWrite(context)) {
- Log.e(TAG, "6.0 or later, but haven't been granted WRITE_SETTINGS!");
- return null;
- }
- try {
- for (Method method : WifiManager.class.getDeclaredMethods()) {
- switch (method.getName()) {
- case "getWifiApConfiguration":
- getWifiApConfigurationMethod = method;
- break;
- case "getWifiApState":
- getWifiApStateMethod = method;
- break;
- case "isWifiApEnabled":
- isWifiApEnabledMethod = method;
- break;
- case "setWifiApEnabled":
- setWifiApEnabledMethod = method;
- break;
- }
- }
- instance = new WifiApControl(context);
- instance.isEnabled(); // make sure this instance works
- } catch (Throwable e) {
- if (BuildConfig.DEBUG) {
- throw e;
- }
- Log.e(TAG, "WifiManager failed to init", e);
- return null;
- }
- }
- return instance;
- }
-
- private static String getDeviceName(WifiManager wifiManager) {
- Log.w(TAG, "6.0 or later, unaccessible MAC - falling back to the default device name: " + FALLBACK_DEVICE);
- return FALLBACK_DEVICE;
- }
-
- private static Object invokeQuietly(Method method, Object receiver, Object... args) {
- try {
- return method.invoke(receiver, args);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
- Log.e(TAG, "", e);
- }
- return null;
- }
-
- // isWifiApEnabled returns whether the Wi-Fi AP is currently enabled.
- // If an error occurred invoking the method via reflection, false is
- // returned.
- public boolean isWifiApEnabled() {
- Object result = invokeQuietly(isWifiApEnabledMethod, wm);
- if (result == null) {
- return false;
- }
- return (Boolean) result;
- }
-
- // isEnabled is a commodity function alias for isWifiApEnabled.
- public boolean isEnabled() {
- return isWifiApEnabled();
- }
-
- // newStateNumber adapts the state constants to the current values in
- // the SDK. They were changed on 4.0 to have higher integer values.
- public static int newStateNumber(int state) {
- if (state < 10) {
- return state + 10;
- }
- return state;
- }
-
- // getWifiApState returns the current Wi-Fi AP state.
- // If an error occurred invoking the method via reflection, -1 is
- // returned.
- public int getWifiApState() {
- Object result = invokeQuietly(getWifiApStateMethod, wm);
- if (result == null) {
- return -1;
- }
- return newStateNumber((Integer) result);
- }
-
- // getState is a commodity function alias for getWifiApState.
- public int getState() {
- return getWifiApState();
- }
-
- // getWifiApConfiguration returns the current Wi-Fi AP configuration.
- // If an error occurred invoking the method via reflection, null is
- // returned.
- public WifiConfiguration getWifiApConfiguration() {
- Object result = invokeQuietly(getWifiApConfigurationMethod, wm);
- if (result == null) {
- return null;
- }
- return (WifiConfiguration) result;
- }
-
- // getConfiguration is a commodity function alias for
- // getWifiApConfiguration.
- public WifiConfiguration getConfiguration() {
- return getWifiApConfiguration();
- }
-
- // setWifiApEnabled starts a Wi-Fi AP with the specified
- // configuration. If one is already running, start using the new
- // configuration. You should call WifiManager.setWifiEnabled(false)
- // yourself before calling this method.
- // If an error occurred invoking the method via reflection, false is
- // returned.
- public boolean setWifiApEnabled(WifiConfiguration config, boolean enabled) {
- Object result = invokeQuietly(setWifiApEnabledMethod, wm, config, enabled);
- if (result == null) {
- return false;
- }
- return (Boolean) result;
- }
-
- // setEnabled is a commodity function alias for setWifiApEnabled.
- public boolean setEnabled(WifiConfiguration config, boolean enabled) {
- return setWifiApEnabled(config, enabled);
- }
-
- // enable starts the currently configured Wi-Fi AP.
- public boolean enable() {
- return setEnabled(getConfiguration(), true);
- }
-
- // disable stops any currently running Wi-Fi AP.
- public boolean disable() {
- return setEnabled(null, false);
- }
-
- // getInet6Address returns the IPv6 address that the device has in its
- // own Wi-Fi AP local network. Will return null if no Wi-Fi AP is
- // currently enabled.
- public Inet6Address getInet6Address() {
- if (!isEnabled()) {
- return null;
- }
- return getInetAddress(Inet6Address.class);
- }
-
- // getInet4Address returns the IPv4 address that the device has in its
- // own Wi-Fi AP local network. Will return null if no Wi-Fi AP is
- // currently enabled.
- public Inet4Address getInet4Address() {
- if (!isEnabled()) {
- return null;
- }
- return getInetAddress(Inet4Address.class);
- }
-
-
- private T getInetAddress(Class addressType) {
- try {
- Enumeration ifaces = NetworkInterface.getNetworkInterfaces();
- while (ifaces.hasMoreElements()) {
- NetworkInterface iface = ifaces.nextElement();
-
- if (!iface.getName().equals(deviceName)) {
- continue;
- }
-
- Enumeration addrs = iface.getInetAddresses();
- while (addrs.hasMoreElements()) {
- InetAddress addr = addrs.nextElement();
-
- if (addressType.isInstance(addr)) {
- return addressType.cast(addr);
- }
- }
- }
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- return null;
- }
-
- // Client describes a Wi-Fi AP device connected to the network.
- public static class Client {
-
- // ipAddr is the raw string of the IP Address client
- public String ipAddr;
-
- // hwAddr is the raw string of the MAC of the client
- public String hwAddr;
-
- public Client(String ipAddr, String hwAddr) {
- this.ipAddr = ipAddr;
- this.hwAddr = hwAddr;
- }
- }
-
- // getClients returns a list of all clients connected to the network.
- // Since the information is pulled from ARP, which is cached for up to
- // five minutes, this method may yield clients that disconnected
- // minutes ago.
- public List getClients() {
- if (!isEnabled()) {
- return null;
- }
- List result = new ArrayList<>();
-
- // Basic sanity checks
- Pattern macPattern = Pattern.compile("..:..:..:..:..:..");
-
- BufferedReader br = null;
- try {
- br = new BufferedReader(new FileReader("/proc/net/arp"));
- String line;
- while ((line = br.readLine()) != null) {
- String[] parts = line.split(" +");
- if (parts.length < 6) {
- continue;
- }
-
- String ipAddr = parts[0];
- String hwAddr = parts[3];
- String device = parts[5];
-
- if (!device.equals(deviceName)) {
- continue;
- }
-
- if (!macPattern.matcher(parts[3]).find()) {
- continue;
- }
-
- result.add(new Client(ipAddr, hwAddr));
- }
- } catch (IOException e) {
- Log.e(TAG, "", e);
- } finally {
- try {
- if (br != null) {
- br.close();
- }
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- }
-
- return result;
- }
-
- // ReachableClientListener is an interface to collect the results
- // provided by getReachableClients via callbacks.
- public interface ReachableClientListener {
-
- // onReachableClient is called each time a reachable client is
- // found.
- void onReachableClient(Client c);
-
- // onComplete is called when we are done looking for reachable
- // clients
- void onComplete();
- }
-
- // getReachableClients fetches the clients connected to the network
- // much like getClients, but only those which are reachable. Since
- // checking for reachability requires network I/O, the reachable
- // clients are returned via callbacks. All the clients are returned
- // like in getClients so that the callback returns a subset of the
- // same objects.
- public List getReachableClients(final int timeout,
- final ReachableClientListener listener) {
- List clients = getClients();
- if (clients == null) {
- return null;
- }
- final CountDownLatch latch = new CountDownLatch(clients.size());
- ExecutorService es = Executors.newCachedThreadPool();
- for (final Client c : clients) {
- es.submit(new Runnable() {
- public void run() {
- try {
- InetAddress ip = InetAddress.getByName(c.ipAddr);
- if (ip.isReachable(timeout)) {
- listener.onReachableClient(c);
- }
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- latch.countDown();
- }
- });
- }
- new Thread() {
- public void run() {
- try {
- latch.await();
- } catch (InterruptedException e) {
- Log.e(TAG, "", e);
- }
- listener.onComplete();
- }
- }.start();
- return clients;
- }
-}
diff --git a/legacy/src/full/java/javax/jmdns/impl/FDroidServiceInfo.java b/legacy/src/full/java/javax/jmdns/impl/FDroidServiceInfo.java
deleted file mode 100644
index 37eee53f1..000000000
--- a/legacy/src/full/java/javax/jmdns/impl/FDroidServiceInfo.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package javax.jmdns.impl;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.UnknownHostException;
-
-import javax.jmdns.ServiceInfo;
-import javax.jmdns.impl.util.ByteWrangler;
-
-/**
- * The ServiceInfo class needs to be serialized in order to be sent as an Android broadcast.
- * In order to make it Parcelable (or Serializable for that matter), there are some package-scope
- * methods which needed to be used. Thus, this class is in the javax.jmdns.impl package so that
- * it can access those methods. This is as an alternative to modifying the source code of JmDNS.
- */
-public class FDroidServiceInfo extends ServiceInfoImpl implements Parcelable {
-
- public FDroidServiceInfo(ServiceInfo info) {
- super(info);
- }
-
- /**
- * Return the fingerprint of the signing key, or {@code null} if it is not set.
- */
- public String getFingerprint() {
- // getPropertyString() will return "true" if the value is a zero-length byte array
- // so we just do a custom version using getPropertyBytes()
- byte[] data = getPropertyBytes("fingerprint");
- if (data == null || data.length == 0) {
- return null;
- }
- String fingerprint = ByteWrangler.readUTF(data, 0, data.length);
- if (TextUtils.isEmpty(fingerprint)) {
- return null;
- }
- return fingerprint;
- }
-
- public String getRepoAddress() {
- return getURL(); // Automatically appends the "path" property if present, so no need to do it ourselves.
- }
-
- private static byte[] readBytes(Parcel in) {
- byte[] bytes = new byte[in.readInt()];
- in.readByteArray(bytes);
- return bytes;
- }
-
- public FDroidServiceInfo(Parcel in) {
- super(
- in.readString(),
- in.readString(),
- in.readString(),
- in.readInt(),
- in.readInt(),
- in.readInt(),
- in.readByte() != 0,
- readBytes(in));
-
- int addressCount = in.readInt();
- for (int i = 0; i < addressCount; i++) {
- try {
- addAddress((Inet4Address) Inet4Address.getByAddress(readBytes(in)));
- } catch (UnknownHostException e) {
- e.printStackTrace();
- }
- }
-
- addressCount = in.readInt();
- for (int i = 0; i < addressCount; i++) {
- try {
- addAddress((Inet6Address) Inet6Address.getByAddress(readBytes(in)));
- } catch (UnknownHostException e) {
- e.printStackTrace();
- }
- }
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(getType());
- dest.writeString(getName());
- dest.writeString(getSubtype());
- dest.writeInt(getPort());
- dest.writeInt(getWeight());
- dest.writeInt(getPriority());
- dest.writeByte(isPersistent() ? (byte) 1 : (byte) 0);
- dest.writeInt(getTextBytes().length);
- dest.writeByteArray(getTextBytes());
- dest.writeInt(getInet4Addresses().length);
- for (int i = 0; i < getInet4Addresses().length; i++) {
- Inet4Address address = getInet4Addresses()[i];
- dest.writeInt(address.getAddress().length);
- dest.writeByteArray(address.getAddress());
- }
- dest.writeInt(getInet6Addresses().length);
- for (int i = 0; i < getInet6Addresses().length; i++) {
- Inet6Address address = getInet6Addresses()[i];
- dest.writeInt(address.getAddress().length);
- dest.writeByteArray(address.getAddress());
- }
- }
-
- public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
- public FDroidServiceInfo createFromParcel(Parcel source) {
- return new FDroidServiceInfo(source);
- }
-
- public FDroidServiceInfo[] newArray(int size) {
- return new FDroidServiceInfo[size];
- }
- };
-}
diff --git a/legacy/src/full/java/kellinwood/logging/AbstractLogger.java b/legacy/src/full/java/kellinwood/logging/AbstractLogger.java
deleted file mode 100644
index c2c3e4e0c..000000000
--- a/legacy/src/full/java/kellinwood/logging/AbstractLogger.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.logging;
-
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-public abstract class AbstractLogger implements LoggerInterface {
-
- protected String category;
-
- SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.ENGLISH);
-
- public AbstractLogger(String category) {
- this.category = category;
- }
-
- protected String format(String level, String message) {
- return String.format("%s %s %s: %s\n", dateFormat.format(new Date()), level, category, message);
- }
-
- protected abstract void write(String level, String message, Throwable t);
-
- protected void writeFixNullMessage(String level, String message, Throwable t) {
- if (message == null) {
- if (t != null) message = t.getClass().getName();
- else message = "null";
- }
- write(level, message, t);
- }
-
- public void debug(String message, Throwable t) {
- writeFixNullMessage(DEBUG, message, t);
- }
-
- public void debug(String message) {
- writeFixNullMessage(DEBUG, message, null);
- }
-
- public void error(String message, Throwable t) {
- writeFixNullMessage(ERROR, message, t);
- }
-
- public void error(String message) {
- writeFixNullMessage(ERROR, message, null);
- }
-
- public void info(String message, Throwable t) {
- writeFixNullMessage(INFO, message, t);
- }
-
- public void info(String message) {
- writeFixNullMessage(INFO, message, null);
- }
-
- public void warning(String message, Throwable t) {
- writeFixNullMessage(WARNING, message, t);
- }
-
- public void warning(String message) {
- writeFixNullMessage(WARNING, message, null);
- }
-
- public boolean isDebugEnabled() {
- return true;
- }
-
- public boolean isErrorEnabled() {
- return true;
- }
-
- public boolean isInfoEnabled() {
- return true;
- }
-
- public boolean isWarningEnabled() {
- return true;
- }
-}
diff --git a/legacy/src/full/java/kellinwood/logging/ConsoleLoggerFactory.java b/legacy/src/full/java/kellinwood/logging/ConsoleLoggerFactory.java
deleted file mode 100644
index 1d4b31aa2..000000000
--- a/legacy/src/full/java/kellinwood/logging/ConsoleLoggerFactory.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.logging;
-
-public class ConsoleLoggerFactory implements LoggerFactory {
-
- public LoggerInterface getLogger(String category) {
- return new StreamLogger(category, System.out);
- }
-}
diff --git a/legacy/src/full/java/kellinwood/logging/LoggerFactory.java b/legacy/src/full/java/kellinwood/logging/LoggerFactory.java
deleted file mode 100644
index fe15cab94..000000000
--- a/legacy/src/full/java/kellinwood/logging/LoggerFactory.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.logging;
-
-public interface LoggerFactory {
-
- public LoggerInterface getLogger(String category);
-}
diff --git a/legacy/src/full/java/kellinwood/logging/LoggerInterface.java b/legacy/src/full/java/kellinwood/logging/LoggerInterface.java
deleted file mode 100644
index 0c7d86f60..000000000
--- a/legacy/src/full/java/kellinwood/logging/LoggerInterface.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.logging;
-
-public interface LoggerInterface {
-
- public static final String ERROR = "ERROR";
- public static final String WARNING = "WARNING";
- public static final String INFO = "INFO";
- public static final String DEBUG = "DEBUG";
-
- public boolean isErrorEnabled();
-
- public void error(String message);
-
- public void error(String message, Throwable t);
-
- public boolean isWarningEnabled();
-
- public void warning(String message);
-
- public void warning(String message, Throwable t);
-
- public boolean isInfoEnabled();
-
- public void info(String message);
-
- public void info(String message, Throwable t);
-
- public boolean isDebugEnabled();
-
- public void debug(String message);
-
- public void debug(String message, Throwable t);
-}
diff --git a/legacy/src/full/java/kellinwood/logging/LoggerManager.java b/legacy/src/full/java/kellinwood/logging/LoggerManager.java
deleted file mode 100644
index c71987cde..000000000
--- a/legacy/src/full/java/kellinwood/logging/LoggerManager.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.logging;
-
-import java.util.Map;
-import java.util.TreeMap;
-
-public class LoggerManager {
-
- static LoggerFactory factory = new NullLoggerFactory();
-
- static Map loggers = new TreeMap();
-
- public static void setLoggerFactory(LoggerFactory f) {
- factory = f;
- }
-
- public static LoggerInterface getLogger(String category) {
-
- LoggerInterface logger = loggers.get(category);
- if (logger == null) {
- logger = factory.getLogger(category);
- loggers.put(category, logger);
- }
- return logger;
- }
-}
diff --git a/legacy/src/full/java/kellinwood/logging/NullLoggerFactory.java b/legacy/src/full/java/kellinwood/logging/NullLoggerFactory.java
deleted file mode 100644
index d056a402a..000000000
--- a/legacy/src/full/java/kellinwood/logging/NullLoggerFactory.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.logging;
-
-public class NullLoggerFactory implements LoggerFactory {
-
- static LoggerInterface logger = new LoggerInterface() {
-
- public void debug(String message) {
- }
-
- public void debug(String message, Throwable t) {
- }
-
- public void error(String message) {
- }
-
- public void error(String message, Throwable t) {
- }
-
- public void info(String message) {
- }
-
- public void info(String message, Throwable t) {
- }
-
- public boolean isDebugEnabled() {
- return false;
- }
-
- public boolean isErrorEnabled() {
- return false;
- }
-
- public boolean isInfoEnabled() {
- return false;
- }
-
- public boolean isWarningEnabled() {
- return false;
- }
-
- public void warning(String message) {
- }
-
- public void warning(String message, Throwable t) {
- }
-
- };
-
-
- public LoggerInterface getLogger(String category) {
- return logger;
- }
-}
diff --git a/legacy/src/full/java/kellinwood/logging/StreamLogger.java b/legacy/src/full/java/kellinwood/logging/StreamLogger.java
deleted file mode 100644
index e5c02257f..000000000
--- a/legacy/src/full/java/kellinwood/logging/StreamLogger.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.logging;
-
-import java.io.PrintStream;
-
-public class StreamLogger extends AbstractLogger {
-
- PrintStream out;
-
- public StreamLogger(String category, PrintStream out) {
- super(category);
- this.out = out;
- }
-
- @Override
- protected void write(String level, String message, Throwable t) {
- out.print(format(level, message));
- if (t != null) t.printStackTrace(out);
- }
-}
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/AutoKeyException.java b/legacy/src/full/java/kellinwood/security/zipsigner/AutoKeyException.java
deleted file mode 100644
index b3107843e..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/AutoKeyException.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package kellinwood.security.zipsigner;
-
-public class AutoKeyException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- public AutoKeyException(String message) {
- super(message);
- }
-
- public AutoKeyException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/DefaultResourceAdapter.java b/legacy/src/full/java/kellinwood/security/zipsigner/DefaultResourceAdapter.java
deleted file mode 100644
index aa77dc6f4..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/DefaultResourceAdapter.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package kellinwood.security.zipsigner;
-
-import java.util.Locale;
-
-/**
- * Default resource adapter.
- */
-public class DefaultResourceAdapter implements ResourceAdapter {
-
- @Override
- public String getString(Item item, Object... args) {
-
- switch (item) {
- case INPUT_SAME_AS_OUTPUT_ERROR:
- return "Input and output files are the same. Specify a different name for the output.";
- case AUTO_KEY_SELECTION_ERROR:
- return "Unable to auto-select key for signing " + args[0];
- case LOADING_CERTIFICATE_AND_KEY:
- return "Loading certificate and private key";
- case PARSING_CENTRAL_DIRECTORY:
- return "Parsing the input's central directory";
- case GENERATING_MANIFEST:
- return "Generating manifest";
- case GENERATING_SIGNATURE_FILE:
- return "Generating signature file";
- case GENERATING_SIGNATURE_BLOCK:
- return "Generating signature block file";
- case COPYING_ZIP_ENTRY:
- return String.format(Locale.ENGLISH, "Copying zip entry %d of %d", args[0], args[1]);
- default:
- throw new IllegalArgumentException("Unknown item " + item);
- }
- }
-}
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/HexDumpEncoder.java b/legacy/src/full/java/kellinwood/security/zipsigner/HexDumpEncoder.java
deleted file mode 100644
index 79e5495ae..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/HexDumpEncoder.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.security.zipsigner;
-
-import org.bouncycastle.util.encoders.HexEncoder;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
-/**
- * Produces the classic hex dump with an address column, hex data
- * section (16 bytes per row) and right-column printable character display.
- */
-public class HexDumpEncoder {
-
- static HexEncoder encoder = new HexEncoder();
-
- public static String encode(byte[] data) {
-
- try {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- encoder.encode(data, 0, data.length, baos);
- byte[] hex = baos.toByteArray();
-
- StringBuilder hexDumpOut = new StringBuilder();
- for (int i = 0; i < hex.length; i += 32) {
-
- int max = Math.min(i + 32, hex.length);
-
- StringBuilder hexOut = new StringBuilder();
- StringBuilder chrOut = new StringBuilder();
-
- hexOut.append(String.format("%08x: ", (i / 2)));
-
- for (int j = i; j < max; j += 2) {
- hexOut.append(Character.valueOf((char) hex[j]));
- hexOut.append(Character.valueOf((char) hex[j + 1]));
- if ((j + 2) % 4 == 0) hexOut.append(' ');
-
- int dataChar = data[j / 2];
- if (dataChar >= 32 && dataChar < 127) {
- chrOut.append(Character.valueOf((char) dataChar));
- } else chrOut.append('.');
- }
-
- hexDumpOut.append(hexOut.toString());
- for (int k = hexOut.length(); k < 50; k++) hexDumpOut.append(' ');
- hexDumpOut.append(" ");
- hexDumpOut.append(chrOut);
- hexDumpOut.append("\n");
- }
-
- return hexDumpOut.toString();
- } catch (IOException x) {
- throw new IllegalStateException(x.getClass().getName() + ": " + x.getMessage());
- }
- }
-}
\ No newline at end of file
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/KeySet.java b/legacy/src/full/java/kellinwood/security/zipsigner/KeySet.java
deleted file mode 100644
index cfdeb180f..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/KeySet.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.security.zipsigner;
-
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-
-public class KeySet {
-
- String name;
-
- // certificate
- X509Certificate publicKey = null;
-
- // private key
- PrivateKey privateKey = null;
-
- // signature block template
- byte[] sigBlockTemplate = null;
-
- String signatureAlgorithm = "SHA1withRSA";
-
- public KeySet() {
- }
-
- public KeySet(String name, X509Certificate publicKey, PrivateKey privateKey, byte[] sigBlockTemplate) {
- this.name = name;
- this.publicKey = publicKey;
- this.privateKey = privateKey;
- this.sigBlockTemplate = sigBlockTemplate;
- }
-
- public KeySet(String name, X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, byte[] sigBlockTemplate) {
- this.name = name;
- this.publicKey = publicKey;
- this.privateKey = privateKey;
- if (signatureAlgorithm != null) this.signatureAlgorithm = signatureAlgorithm;
- this.sigBlockTemplate = sigBlockTemplate;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public X509Certificate getPublicKey() {
- return publicKey;
- }
-
- public void setPublicKey(X509Certificate publicKey) {
- this.publicKey = publicKey;
- }
-
- public PrivateKey getPrivateKey() {
- return privateKey;
- }
-
- public void setPrivateKey(PrivateKey privateKey) {
- this.privateKey = privateKey;
- }
-
- public byte[] getSigBlockTemplate() {
- return sigBlockTemplate;
- }
-
- public void setSigBlockTemplate(byte[] sigBlockTemplate) {
- this.sigBlockTemplate = sigBlockTemplate;
- }
-
- public String getSignatureAlgorithm() {
- return signatureAlgorithm;
- }
-
- public void setSignatureAlgorithm(String signatureAlgorithm) {
- if (signatureAlgorithm == null) signatureAlgorithm = "SHA1withRSA";
- else this.signatureAlgorithm = signatureAlgorithm;
- }
-}
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/ProgressEvent.java b/legacy/src/full/java/kellinwood/security/zipsigner/ProgressEvent.java
deleted file mode 100644
index eb29ec3e9..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/ProgressEvent.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.security.zipsigner;
-
-public class ProgressEvent {
-
- public static final int PRORITY_NORMAL = 0;
- public static final int PRORITY_IMPORTANT = 1;
-
- private String message;
- private int percentDone;
- private int priority;
-
- public String getMessage() {
- return message;
- }
-
- public void setMessage(String message) {
- this.message = message;
- }
-
- public int getPercentDone() {
- return percentDone;
- }
-
- public void setPercentDone(int percentDone) {
- this.percentDone = percentDone;
- }
-
- public int getPriority() {
- return priority;
- }
-
- public void setPriority(int priority) {
- this.priority = priority;
- }
-}
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/ProgressHelper.java b/legacy/src/full/java/kellinwood/security/zipsigner/ProgressHelper.java
deleted file mode 100644
index 41b09f264..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/ProgressHelper.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.security.zipsigner;
-
-import java.util.ArrayList;
-
-public class ProgressHelper {
-
- private int progressTotalItems = 0;
- private int progressCurrentItem = 0;
- private ProgressEvent progressEvent = new ProgressEvent();
-
- public void initProgress() {
- progressTotalItems = 10000;
- progressCurrentItem = 0;
- }
-
- public int getProgressTotalItems() {
- return progressTotalItems;
- }
-
- public void setProgressTotalItems(int progressTotalItems) {
- this.progressTotalItems = progressTotalItems;
- }
-
- public int getProgressCurrentItem() {
- return progressCurrentItem;
- }
-
- public void setProgressCurrentItem(int progressCurrentItem) {
- this.progressCurrentItem = progressCurrentItem;
- }
-
- public void progress(int priority, String message) {
-
- progressCurrentItem += 1;
-
- int percentDone;
- if (progressTotalItems == 0) percentDone = 0;
- else percentDone = (100 * progressCurrentItem) / progressTotalItems;
-
- // Notify listeners here
- for (ProgressListener listener : listeners) {
- progressEvent.setMessage(message);
- progressEvent.setPercentDone(percentDone);
- progressEvent.setPriority(priority);
- listener.onProgress(progressEvent);
- }
- }
-
- private ArrayList listeners = new ArrayList();
-
- @SuppressWarnings("unchecked")
- public synchronized void addProgressListener(ProgressListener l) {
- ArrayList list = (ArrayList) listeners.clone();
- list.add(l);
- listeners = list;
- }
-
- @SuppressWarnings("unchecked")
- public synchronized void removeProgressListener(ProgressListener l) {
- ArrayList list = (ArrayList) listeners.clone();
- list.remove(l);
- listeners = list;
- }
-}
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/ProgressListener.java b/legacy/src/full/java/kellinwood/security/zipsigner/ProgressListener.java
deleted file mode 100644
index 3cb62060a..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/ProgressListener.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kellinwood.security.zipsigner;
-
-public interface ProgressListener {
-
- /**
- * Called to notify the listener that progress has been made during
- * the zip signing operation.
- */
- public void onProgress(ProgressEvent event);
-}
\ No newline at end of file
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/ResourceAdapter.java b/legacy/src/full/java/kellinwood/security/zipsigner/ResourceAdapter.java
deleted file mode 100644
index 993749ed3..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/ResourceAdapter.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package kellinwood.security.zipsigner;
-
-/**
- * Interface to obtain internationalized strings for the progress events.
- */
-public interface ResourceAdapter {
-
- public enum Item {
- INPUT_SAME_AS_OUTPUT_ERROR,
- AUTO_KEY_SELECTION_ERROR,
- LOADING_CERTIFICATE_AND_KEY,
- PARSING_CENTRAL_DIRECTORY,
- GENERATING_MANIFEST,
- GENERATING_SIGNATURE_FILE,
- GENERATING_SIGNATURE_BLOCK,
- COPYING_ZIP_ENTRY
- }
-
- public String getString(Item item, Object... args);
-}
diff --git a/legacy/src/full/java/kellinwood/security/zipsigner/ZipSigner.java b/legacy/src/full/java/kellinwood/security/zipsigner/ZipSigner.java
deleted file mode 100644
index 53668efd8..000000000
--- a/legacy/src/full/java/kellinwood/security/zipsigner/ZipSigner.java
+++ /dev/null
@@ -1,780 +0,0 @@
-/*
- * Copyright (C) 2010 Ken Ellinwood
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/* This file is a heavily modified version of com.android.signapk.SignApk.java.
- * The changes include:
- * - addition of the signZip() convenience methods
- * - addition of a progress listener interface
- * - removal of main()
- * - switch to a signature generation method that verifies
- * in Android recovery
- * - eliminated dependency on sun.security and sun.misc APIs by
- * using signature block template files.
- */
-
-package kellinwood.security.zipsigner;
-
-import android.util.Base64;
-
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.lang.reflect.Method;
-import java.net.URL;
-import java.security.DigestOutputStream;
-import java.security.GeneralSecurityException;
-import java.security.Key;
-import java.security.KeyFactory;
-import java.security.KeyStore;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.Provider;
-import java.security.Security;
-import java.security.Signature;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
-import java.security.spec.PKCS8EncodedKeySpec;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Observable;
-import java.util.Observer;
-import java.util.TreeMap;
-import java.util.jar.Attributes;
-import java.util.jar.JarFile;
-import java.util.jar.Manifest;
-import java.util.regex.Pattern;
-
-import javax.crypto.Cipher;
-import javax.crypto.EncryptedPrivateKeyInfo;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-
-import kellinwood.logging.LoggerInterface;
-import kellinwood.logging.LoggerManager;
-import kellinwood.zipio.ZioEntry;
-import kellinwood.zipio.ZipInput;
-import kellinwood.zipio.ZipOutput;
-
-/**
- * This is a modified copy of com.android.signapk.SignApk.java. It provides an
- * API to sign JAR files (including APKs and Zip/OTA updates) in
- * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
- *
- * Please see the README.txt file in the root of this project for usage instructions.
- */
-public class ZipSigner {
-
- private boolean canceled = false;
-
- private ProgressHelper progressHelper = new ProgressHelper();
- private ResourceAdapter resourceAdapter = new DefaultResourceAdapter();
-
- static LoggerInterface log = null;
-
- private static final String CERT_SF_NAME = "META-INF/CERT.SF";
- private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
-
- // Files matching this pattern are not copied to the output.
- private static Pattern stripPattern =
- Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");
-
- Map loadedKeys = new HashMap();
- KeySet keySet = null;
-
- public static LoggerInterface getLogger() {
- if (log == null) log = LoggerManager.getLogger(ZipSigner.class.getName());
- return log;
- }
-
- public static final String MODE_AUTO_TESTKEY = "auto-testkey";
- public static final String MODE_AUTO_NONE = "auto-none";
- public static final String MODE_AUTO = "auto";
- public static final String KEY_NONE = "none";
- public static final String KEY_TESTKEY = "testkey";
-
- // Allowable key modes.
- public static final String[] SUPPORTED_KEY_MODES =
- new String[]{MODE_AUTO_TESTKEY, MODE_AUTO, MODE_AUTO_NONE, "media", "platform", "shared", KEY_TESTKEY, KEY_NONE};
-
- String keymode = KEY_TESTKEY; // backwards compatible with versions that only signed with this key
-
- Map autoKeyDetect = new HashMap();
-
- AutoKeyObservable autoKeyObservable = new AutoKeyObservable();
-
- public ZipSigner() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
- // MD5 of the first 1458 bytes of the signature block generated by the key, mapped to the key name
- autoKeyDetect.put("aa9852bc5a53272ac8031d49b65e4b0e", "media");
- autoKeyDetect.put("e60418c4b638f20d0721e115674ca11f", "platform");
- autoKeyDetect.put("3e24e49741b60c215c010dc6048fca7d", "shared");
- autoKeyDetect.put("dab2cead827ef5313f28e22b6fa8479f", "testkey");
- }
-
- public ResourceAdapter getResourceAdapter() {
- return resourceAdapter;
- }
-
- public void setResourceAdapter(ResourceAdapter resourceAdapter) {
- this.resourceAdapter = resourceAdapter;
- }
-
- // when the key mode is automatic, the observers are called when the key is determined
- public void addAutoKeyObserver(Observer o) {
- autoKeyObservable.addObserver(o);
- }
-
- public String getKeymode() {
- return keymode;
- }
-
- public void setKeymode(String km) throws IOException, GeneralSecurityException {
- if (getLogger().isDebugEnabled()) getLogger().debug("setKeymode: " + km);
- keymode = km;
- if (keymode.startsWith(MODE_AUTO)) {
- keySet = null;
- } else {
- progressHelper.initProgress();
- loadKeys(keymode);
- }
- }
-
- public static String[] getSupportedKeyModes() {
- return SUPPORTED_KEY_MODES;
- }
-
- protected String autoDetectKey(String mode, Map zioEntries)
- throws NoSuchAlgorithmException, IOException {
- boolean debug = getLogger().isDebugEnabled();
-
- if (!mode.startsWith(MODE_AUTO)) return mode;
-
- // Auto-determine which keys to use
- String keyName = null;
- // Start by finding the signature block file in the input.
- for (Map.Entry entry : zioEntries.entrySet()) {
- String entryName = entry.getKey();
- if (entryName.startsWith("META-INF/") && entryName.endsWith(".RSA")) {
-
- // Compute MD5 of the first 1458 bytes, which is the size of our signature block templates --
- // e.g., the portion of the sig block file that is the same for a given certificate.
- MessageDigest md5 = MessageDigest.getInstance("MD5");
- byte[] entryData = entry.getValue().getData();
- if (entryData.length < 1458) break; // sig block too short to be a supported key
- md5.update(entryData, 0, 1458);
- byte[] rawDigest = md5.digest();
-
- // Create the hex representation of the digest value
- StringBuilder builder = new StringBuilder();
- for (byte b : rawDigest) {
- builder.append(String.format("%02x", b));
- }
-
- String md5String = builder.toString();
- // Lookup the key name
- keyName = autoKeyDetect.get(md5String);
-
- if (debug) {
- if (keyName != null) {
- getLogger().debug(String.format("Auto-determined key=%s using md5=%s", keyName, md5String));
- } else {
- getLogger().debug(String.format("Auto key determination failed for md5=%s", md5String));
- }
- }
- if (keyName != null) return keyName;
- }
- }
-
- if (mode.equals(MODE_AUTO_TESTKEY)) {
- // in auto-testkey mode, fallback to the testkey if it couldn't be determined
- if (debug) getLogger().debug("Falling back to key=" + keyName);
- return KEY_TESTKEY;
-
- } else if (mode.equals(MODE_AUTO_NONE)) {
- // in auto-node mode, simply copy the input to the output when the key can't be determined.
- if (debug) getLogger().debug("Unable to determine key, returning: " + KEY_NONE);
- return KEY_NONE;
- }
-
- return null;
- }
-
- public void issueLoadingCertAndKeysProgressEvent() {
- progressHelper.progress(ProgressEvent.PRORITY_IMPORTANT, resourceAdapter.getString(ResourceAdapter.Item.LOADING_CERTIFICATE_AND_KEY));
- }
-
- // Loads one of the built-in keys (media, platform, shared, testkey)
- public void loadKeys(String name)
- throws IOException, GeneralSecurityException {
-
- keySet = loadedKeys.get(name);
- if (keySet != null) return;
-
- keySet = new KeySet();
- keySet.setName(name);
- loadedKeys.put(name, keySet);
-
- if (KEY_NONE.equals(name)) return;
-
- issueLoadingCertAndKeysProgressEvent();
-
- // load the private key
- URL privateKeyUrl = getClass().getResource("/keys/" + name + ".pk8");
- keySet.setPrivateKey(readPrivateKey(privateKeyUrl, null));
-
- // load the certificate
- URL publicKeyUrl = getClass().getResource("/keys/" + name + ".x509.pem");
- keySet.setPublicKey(readPublicKey(publicKeyUrl));
-
- // load the signature block template
- URL sigBlockTemplateUrl = getClass().getResource("/keys/" + name + ".sbt");
- if (sigBlockTemplateUrl != null) {
- keySet.setSigBlockTemplate(readContentAsBytes(sigBlockTemplateUrl));
- }
- }
-
- public void setKeys(String name, X509Certificate publicKey, PrivateKey privateKey, byte[] signatureBlockTemplate) {
- keySet = new KeySet(name, publicKey, privateKey, signatureBlockTemplate);
- }
-
- public void setKeys(String name, X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, byte[] signatureBlockTemplate) {
- keySet = new KeySet(name, publicKey, privateKey, signatureAlgorithm, signatureBlockTemplate);
- }
-
- public KeySet getKeySet() {
- return keySet;
- }
-
- // Allow the operation to be canceled.
- public void cancel() {
- canceled = true;
- }
-
- // Allow the instance to sign again if previously canceled.
- public void resetCanceled() {
- canceled = false;
- }
-
- public boolean isCanceled() {
- return canceled;
- }
-
- @SuppressWarnings("unchecked")
- public void loadProvider(String providerClassName)
- throws ClassNotFoundException, IllegalAccessException, InstantiationException {
- Class providerClass = Class.forName(providerClassName);
- Provider provider = (Provider) providerClass.newInstance();
- Security.insertProviderAt(provider, 1);
- }
-
-
- public X509Certificate readPublicKey(URL publicKeyUrl)
- throws IOException, GeneralSecurityException {
- InputStream input = publicKeyUrl.openStream();
- try {
- CertificateFactory cf = CertificateFactory.getInstance("X.509");
- return (X509Certificate) cf.generateCertificate(input);
- } finally {
- input.close();
- }
- }
-
- /**
- * Decrypt an encrypted PKCS 8 format private key.
- *
- * Based on ghstark's post on Aug 6, 2006 at
- * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
- *
- * @param encryptedPrivateKey The raw data of the private key
- * @param keyPassword the key password
- */
- private KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, String keyPassword)
- throws GeneralSecurityException {
- EncryptedPrivateKeyInfo epkInfo;
- try {
- epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
- } catch (IOException ex) {
- // Probably not an encrypted key.
- return null;
- }
-
- char[] keyPasswd = keyPassword.toCharArray();
-
- SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
- Key key = skFactory.generateSecret(new PBEKeySpec(keyPasswd));
-
- Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
- cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
-
- try {
- return epkInfo.getKeySpec(cipher);
- } catch (InvalidKeySpecException ex) {
- getLogger().error("signapk: Password for private key may be bad.");
- throw ex;
- }
- }
-
- /**
- * Fetch the content at the specified URL and return it as a byte array.
- */
- public byte[] readContentAsBytes(URL contentUrl) throws IOException {
- return readContentAsBytes(contentUrl.openStream());
- }
-
- /**
- * Fetch the content from the given stream and return it as a byte array.
- */
- public byte[] readContentAsBytes(InputStream input) throws IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
-
- byte[] buffer = new byte[2048];
-
- int numRead = input.read(buffer);
- while (numRead != -1) {
- baos.write(buffer, 0, numRead);
- numRead = input.read(buffer);
- }
-
- byte[] bytes = baos.toByteArray();
- return bytes;
- }
-
- /**
- * Read a PKCS 8 format private key.
- */
- public PrivateKey readPrivateKey(URL privateKeyUrl, String keyPassword)
- throws IOException, GeneralSecurityException {
- DataInputStream input = new DataInputStream(privateKeyUrl.openStream());
- try {
- byte[] bytes = readContentAsBytes(input);
-
- KeySpec spec = decryptPrivateKey(bytes, keyPassword);
- if (spec == null) {
- spec = new PKCS8EncodedKeySpec(bytes);
- }
-
- try {
- return KeyFactory.getInstance("RSA").generatePrivate(spec);
- } catch (InvalidKeySpecException ex) {
- return KeyFactory.getInstance("DSA").generatePrivate(spec);
- }
- } finally {
- input.close();
- }
- }
-
- /**
- * Add the SHA1 of every file to the manifest, creating it if necessary.
- */
- private Manifest addDigestsToManifest(Map entries)
- throws IOException, GeneralSecurityException {
- Manifest input = null;
- ZioEntry manifestEntry = entries.get(JarFile.MANIFEST_NAME);
- if (manifestEntry != null) {
- input = new Manifest();
- input.read(manifestEntry.getInputStream());
- }
- Manifest output = new Manifest();
- Attributes main = output.getMainAttributes();
- if (input != null) {
- main.putAll(input.getMainAttributes());
- } else {
- main.putValue("Manifest-Version", "1.0");
- main.putValue("Created-By", "1.0 (Android SignApk)");
- }
-
- // BASE64Encoder base64 = new BASE64Encoder();
- MessageDigest md = MessageDigest.getInstance("SHA1");
- byte[] buffer = new byte[512];
- int num;
-
- // We sort the input entries by name, and add them to the
- // output manifest in sorted order. We expect that the output
- // map will be deterministic.
-
- TreeMap byName = new TreeMap();
- byName.putAll(entries);
-
- boolean debug = getLogger().isDebugEnabled();
- if (debug) getLogger().debug("Manifest entries:");
- for (ZioEntry entry : byName.values()) {
- if (canceled) break;
- String name = entry.getName();
- if (debug) getLogger().debug(name);
- if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
- !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
- (stripPattern == null ||
- !stripPattern.matcher(name).matches())) {
-
- progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_MANIFEST));
- InputStream data = entry.getInputStream();
- while ((num = data.read(buffer)) > 0) {
- md.update(buffer, 0, num);
- }
-
- Attributes attr = null;
- if (input != null) {
- Attributes inAttr = input.getAttributes(name);
- if (inAttr != null) attr = new Attributes(inAttr);
- }
- if (attr == null) attr = new Attributes();
- attr.putValue("SHA1-Digest", Base64.encodeToString(md.digest(), Base64.NO_WRAP));
- output.getEntries().put(name, attr);
- }
- }
-
- return output;
- }
-
-
- /**
- * Write the signature file to the given output stream.
- */
- private void generateSignatureFile(Manifest manifest, OutputStream out)
- throws IOException, GeneralSecurityException {
- out.write(("Signature-Version: 1.0\r\n").getBytes());
- out.write(("Created-By: 1.0 (Android SignApk)\r\n").getBytes());
-
-
- // BASE64Encoder base64 = new BASE64Encoder();
- MessageDigest md = MessageDigest.getInstance("SHA1");
- PrintStream print = new PrintStream(
- new DigestOutputStream(new ByteArrayOutputStream(), md),
- true, "UTF-8");
-
- // Digest of the entire manifest
- manifest.write(print);
- print.flush();
-
- out.write(("SHA1-Digest-Manifest: " + Base64.encodeToString(md.digest(), Base64.NO_WRAP) + "\r\n\r\n").getBytes());
-
- Map entries = manifest.getEntries();
- for (Map.Entry entry : entries.entrySet()) {
- if (canceled) break;
- progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_SIGNATURE_FILE));
- // Digest of the manifest stanza for this entry.
- String nameEntry = "Name: " + entry.getKey() + "\r\n";
- print.print(nameEntry);
- for (Map.Entry