This commit is contained in:
@@ -7,8 +7,8 @@ trim_trailing_whitespace = true
|
||||
|
||||
[*.{kt,kts}]
|
||||
indent_size = 4
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_allow_trailing_comma=true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||
ij_kotlin_name_count_to_use_star_import = 999
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
||||
|
||||
|
||||
2
.github/workflows/release_build.yml
vendored
2
.github/workflows/release_build.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Extract Version Code
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION_CODE=$(grep -oP '(?<=versionCode=)\d+' app/build.gradle) # Adjust path to your build.gradle
|
||||
VERSION_CODE=$(grep -oP '(?<=versionCode=)\d+' app/build.gradle.kts) # Adjust path to your build.gradle
|
||||
echo "::set-output name=version_code::$VERSION_CODE"
|
||||
echo "Version Code: $VERSION_CODE"
|
||||
|
||||
|
||||
25
README.md
25
README.md
@@ -2,17 +2,17 @@
|
||||
|
||||
<img width="" src="metadata/en-US/images/featureGraphic.png" alt="Droid-ify" align="center">
|
||||
|
||||
[](https://github.com/Iamlooker/Droid-ify/stargazers)
|
||||
[](https://github.com/Iamlooker/Droid-ify/blob/master/COPYING)
|
||||
[](https://github.com/Iamlooker/Droid-ify/releases/)
|
||||
[](https://github.com/Iamlooker/Droid-ify/releases/latest)
|
||||
[](https://f-droid.org/packages/com.looker.droidify)
|
||||
[](https://github.com/Iamlooker/Droid-ify/stargazers)
|
||||
[](https://github.com/Iamlooker/Droid-ify/blob/master/COPYING)
|
||||
[](https://github.com/Iamlooker/Droid-ify/releases/)
|
||||
[](https://github.com/Iamlooker/Droid-ify/releases/latest)
|
||||
[](https://f-droid.org/packages/com.looker.droidify)
|
||||
</div>
|
||||
<div align="left">
|
||||
|
||||
## Features
|
||||
|
||||
* Material & Clean design
|
||||
* Clean Material 3 design
|
||||
* Fast repository syncing
|
||||
* Smooth user experience
|
||||
* Feature-rich
|
||||
@@ -21,13 +21,13 @@
|
||||
|
||||
<img src="metadata/en-US/images/phoneScreenshots/1.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/2.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/3.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/4.png" width="25%" />
|
||||
|
||||
## Building and Installing
|
||||
## Building and installing
|
||||
|
||||
1. **Install Android Studio**:
|
||||
- Download and install [Android Studio](https://developer.android.com/studio) on your computer
|
||||
if you haven't already.
|
||||
|
||||
2. **Clone the Repository**:
|
||||
2. **Clone the repository**:
|
||||
- Open Android Studio and select "Project from Version Control."
|
||||
- Paste the link to this repository to clone it to your local machine.
|
||||
|
||||
@@ -39,15 +39,14 @@
|
||||
## TODO
|
||||
|
||||
- [ ] Add support for `index-v2`
|
||||
- [ ] Add detekt code-analysis
|
||||
- [ ] Add GitHub Repo feature
|
||||
- [ ] Add detekt code analysis
|
||||
|
||||
## Contribution
|
||||
## Contributing
|
||||
|
||||
- Pick any issue you would like to resolve
|
||||
- Fork the project
|
||||
- Open a Pull Request
|
||||
- Your PR will undergo review
|
||||
- Open a pull request
|
||||
- Your pull request will undergo review
|
||||
|
||||
## Translations
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.ktlint)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
@@ -12,7 +12,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
val latestVersionName = "0.6.5"
|
||||
val latestVersionName = "0.6.6"
|
||||
namespace = "com.looker.droidify"
|
||||
buildToolsVersion = "35.0.0"
|
||||
compileSdk = 35
|
||||
@@ -20,37 +20,28 @@ android {
|
||||
minSdk = 23
|
||||
targetSdk = 35
|
||||
applicationId = "com.looker.droidify"
|
||||
versionCode = 650
|
||||
versionCode = 660
|
||||
versionName = latestVersionName
|
||||
vectorDrawables.useSupportLibrary = false
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
testInstrumentationRunner = "com.looker.droidify.TestRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
compileOptions.isCoreLibraryDesugaringEnabled = true
|
||||
kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-parameters")
|
||||
androidResources.generateLocaleConfig = true
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
compilerOptions {
|
||||
languageVersion.set(KotlinVersion.KOTLIN_2_2)
|
||||
apiVersion.set(KotlinVersion.KOTLIN_2_2)
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
freeCompilerArgs = listOf(
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-Xcontext-receivers"
|
||||
)
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
sourceSets.forEach { source ->
|
||||
val javaDir = source.java.srcDirs.find { it.name == "java" }
|
||||
source.java {
|
||||
srcDir(File(javaDir?.parentFile, "kotlin"))
|
||||
}
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.generateKotlin", "true")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -64,7 +55,7 @@ android {
|
||||
resValue("string", "application_name", "Droid-ify")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard.pro"
|
||||
"proguard.pro",
|
||||
)
|
||||
}
|
||||
create("alpha") {
|
||||
@@ -73,7 +64,7 @@ android {
|
||||
resValue("string", "application_name", "Droid-ify Alpha")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard.pro"
|
||||
"proguard.pro",
|
||||
)
|
||||
isDebuggable = true
|
||||
isMinifyEnabled = true
|
||||
@@ -82,7 +73,7 @@ android {
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "VERSION_NAME",
|
||||
value = "\"v$latestVersionName\""
|
||||
value = "\"v$latestVersionName\"",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -111,18 +102,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
android.set(true)
|
||||
ignoreFailures.set(true)
|
||||
debug.set(true)
|
||||
reporters {
|
||||
reporter(ReporterType.HTML)
|
||||
}
|
||||
filter {
|
||||
exclude("**/generated/**")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.desugaring)
|
||||
|
||||
@@ -144,19 +123,17 @@ dependencies {
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.datetime)
|
||||
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.coroutines.android)
|
||||
implementation(libs.coroutines.guava)
|
||||
implementation(libs.bundles.coroutines)
|
||||
|
||||
implementation(libs.libsu.core)
|
||||
implementation(libs.shizuku.api)
|
||||
api(libs.shizuku.provider)
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
implementation(libs.jackson.core)
|
||||
implementation(libs.serialization)
|
||||
|
||||
implementation(libs.ktor.core)
|
||||
implementation(libs.ktor.okhttp)
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.bundles.room)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
implementation(libs.work.ktx)
|
||||
|
||||
@@ -169,13 +146,16 @@ dependencies {
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.bundles.test.unit)
|
||||
testRuntimeOnly(libs.junit.platform)
|
||||
androidTestImplementation(platform(libs.junit.bom))
|
||||
androidTestImplementation(libs.hilt.test)
|
||||
androidTestImplementation(libs.room.test)
|
||||
androidTestImplementation(libs.bundles.test.android)
|
||||
kspAndroidTest(libs.hilt.compiler)
|
||||
|
||||
// debugImplementation(libs.leakcanary)
|
||||
}
|
||||
|
||||
// using a task as a preBuild dependency instead of a function that takes some time insures that it runs
|
||||
// in /res are (almost) all languages that have a translated string is saved. this is safer and saves some time
|
||||
task("detectAndroidLocals") {
|
||||
val langsList: MutableSet<String> = HashSet()
|
||||
|
||||
|
||||
1084
app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json
Normal file
1084
app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json
Normal file
File diff suppressed because it is too large
Load Diff
58
app/src/androidTest/assets/additional_repos.xml
Normal file
58
app/src/androidTest/assets/additional_repos.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="default_repos">
|
||||
<!-- SHIFT -->
|
||||
<!-- name -->
|
||||
<item>SHIFT</item>
|
||||
<!-- address -->
|
||||
<item>https://fdroid.shiftphones.com/fdroid/repo</item>
|
||||
<!-- description -->
|
||||
<item>Provides updates and optional apps for your SHIFT device.</item>
|
||||
<!-- version -->
|
||||
<item>20002</item>
|
||||
<!-- enabled -->
|
||||
<item>1</item>
|
||||
<!-- priority (disabled for additional_repos.xml) -->
|
||||
<!--<item>1</item>-->
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
<item>308205ea308203d2a003020102020900e74081628660407c300d06092a864886f70d01010b05003081a13125302306092a864886f70d01090116166664726f696440736869667470686f6e65732e636f6d311f301d060355040313166664726f69642e736869667470686f6e65732e636f6d31143012060355040b130b534849465450484f4e4553310e300c060355040a13055348494654311330110603550407130a46616c6b656e62657267310f300d0603550408130648657373656e310b30090603550406130244453020170d3235303630353039353133345a180f32303532313032313039353133345a3081a13125302306092a864886f70d01090116166664726f696440736869667470686f6e65732e636f6d311f301d060355040313166664726f69642e736869667470686f6e65732e636f6d31143012060355040b130b534849465450484f4e4553310e300c060355040a13055348494654311330110603550407130a46616c6b656e62657267310f300d0603550408130648657373656e310b300906035504061302444530820222300d06092a864886f70d01010105000382020f003082020a0282020100b5042d416fde7b9eb1b4f399030da3bb7f903e29823aca5d193ef4e2bea848375e2ee1e1a599aa7d157292ba09759938d40b38c5fccaa12f6877a8335c3a1646788a4282dddf197f8e264bf1241efb93f12dfdaed175c4df2a4f68701db5518a6a76c7f51815154d6909598a8ba8af731197ee03d79fc3b5bbbb5fb0d0435e1a33caa7a8c9a4cd9d69c625663741b889efb505d634c8b11765bc703831b8db1cec78c7c2ccec83e211565f13a253a2b320a88621c68978cf7530b01a61796cfd56109ffa09525c1dc20dbabc9b77a0b3e547af052c31a7c137500ee82269cef267fd2c87f3c58036f69561eac39e9ba03ab7e0627880212285f165bbfa6ed12997921891387317c9185695e7ee4694955ff76ef17f9ad8dadd60d5fd0f9b43ce6cb6c8484dfea300fdec8c4dc59d07697694590e28f0f4f01e2dd72afdcb1ec64e95ee2712ba0cf0120807bcd307090af5a7423ace98c990d9b3ae132fbc61414334af2d9ceb81aa460a79529fcab2fd5565c4b7027e6697164d07b800a23b202fc23140055fb36090f78dc24a87483e16dd40e7270a47f020035e352d67fc5ddfcfee82de243f5688e268d1450bca1cb6dcbfc0c5f85e675293d8907b96951cf913fef33177248c3442c59e0dfb75803dd55ef750c0ff80997fde5bf36bf9c4cc6d0dc562e5ed91ccd9139f7885cb7984d61ee6e71b5616fb5e8bf89162c98f0203010001a321301f301d0603551d0e041604142e046a0536dcc228ae2c82770be90c751d4f6fc7300d06092a864886f70d01010b050003820201006cd87b00983dad8f298f9bb880ab982bf25cc6c12002cfa6494b76d08fea86a19f035d42b5f3551896531c67df5618e3153e822c218f220e875cac9e89fab080f0a1410a776f745417110275766c494bc598323481595d75dd019e15dcef8a94814155fc76531feaada8f53d364b75f8bb59ad3493b2bb483d037fcbfa136030ee842a6f3babe851224cd5f8e9ada88ad0eb2f894989310f4bc5751bdf39becccb2a261b76c46ebd4a6df533c990b88e65d21d1ac566d8f801b93a97b9b316f6c3bc40442018429290e06056dbe07c49fca4ef614212421040971d65182acc2c49c0cb8fdd533590e61b7889c39464478a4936f914c276114ae4478bcfad8d0ec67b8a05c7ee83e442ba66775a2376c9939ff54e56e6485abefd31ec89c5d3dcf66df26f590c4a369a04fd4400518eae5e7312d5960da573d18d26dc8717e2913b6f2ccd4dc07958b6d40856ba0720727524127e91656b42cd969161c8e07f66896c2f5bf40268b3c6a84a600492b607f09f5bee013532ead0c1d4773aaab361141c0b44ac31fd40368c92828597758575a27d3b079ce98a41b5837f8007d8890354f5549b150a5bf0bce792bda1981daf0bcf90f2754d74100d85e1110fa6749bf43ba36afe33f828a4341ad5c161878da07a6c87479bb183ebbfa83bbe646da17a26b9947ceb2ee3c94114c18b1d76b7236f585031a0d21dd6359493a8d043</item>
|
||||
|
||||
<!-- microG -->
|
||||
<!-- name -->
|
||||
<item>microG F-Droid repo</item>
|
||||
<!-- address -->
|
||||
<item>https://microg.org/fdroid/repo</item>
|
||||
<!-- description -->
|
||||
<item>This is a repository of microG apps to be used with F-Droid. Applications in this repository are signed official binaries built by the microG Team from the corresponding source code.</item>
|
||||
<!-- version -->
|
||||
<item>20002</item>
|
||||
<!-- enabled -->
|
||||
<item>1</item>
|
||||
<!-- priority (disabled for additional_repos.xml) -->
|
||||
<!--<item>1</item>-->
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
<item>308202ed308201d5a003020102020426ffa009300d06092a864886f70d01010b05003027310b300906035504061302444531183016060355040a130f4e4f47415050532050726f6a656374301e170d3132313030363132303533325a170d3337303933303132303533325a3027310b300906035504061302444531183016060355040a130f4e4f47415050532050726f6a65637430820122300d06092a864886f70d01010105000382010f003082010a02820101009a8d2a5336b0eaaad89ce447828c7753b157459b79e3215dc962ca48f58c2cd7650df67d2dd7bda0880c682791f32b35c504e43e77b43c3e4e541f86e35a8293a54fb46e6b16af54d3a4eda458f1a7c8bc1b7479861ca7043337180e40079d9cdccb7e051ada9b6c88c9ec635541e2ebf0842521c3024c826f6fd6db6fd117c74e859d5af4db04448965ab5469b71ce719939a06ef30580f50febf96c474a7d265bb63f86a822ff7b643de6b76e966a18553c2858416cf3309dd24278374bdd82b4404ef6f7f122cec93859351fc6e5ea947e3ceb9d67374fe970e593e5cd05c905e1d24f5a5484f4aadef766e498adf64f7cf04bddd602ae8137b6eea40722d0203010001a321301f301d0603551d0e04160414110b7aa9ebc840b20399f69a431f4dba6ac42a64300d06092a864886f70d01010b0500038201010007c32ad893349cf86952fb5a49cfdc9b13f5e3c800aece77b2e7e0e9c83e34052f140f357ec7e6f4b432dc1ed542218a14835acd2df2deea7efd3fd5e8f1c34e1fb39ec6a427c6e6f4178b609b369040ac1f8844b789f3694dc640de06e44b247afed11637173f36f5886170fafd74954049858c6096308fc93c1bc4dd5685fa7a1f982a422f2a3b36baa8c9500474cf2af91c39cbec1bc898d10194d368aa5e91f1137ec115087c31962d8f76cd120d28c249cf76f4c70f5baa08c70a7234ce4123be080cee789477401965cfe537b924ef36747e8caca62dfefdd1a6288dcb1c4fd2aaa6131a7ad254e9742022cfd597d2ca5c660ce9e41ff537e5a4041e37</item>
|
||||
|
||||
<!-- IzzyOnDroid -->
|
||||
<!-- name -->
|
||||
<item>IzzyOnDroid F-Droid Repo</item>
|
||||
<!-- address -->
|
||||
<item>https://apt.izzysoft.de/fdroid/repo</item>
|
||||
<!-- description -->
|
||||
<item>This is a repository of apps to be used with F-Droid. Applications in this repository are official binaries built by the original application developers, taken from their resp. repositories (mostly Github, GitLab, Codeberg). Updates for the apps are usually fetched daily, and you can expect daily index updates.</item>
|
||||
<!-- version -->
|
||||
<item>20002</item>
|
||||
<!-- enabled -->
|
||||
<item>1</item>
|
||||
<!-- priority (disabled for additional_repos.xml) -->
|
||||
<!--<item>1</item>-->
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
<item>308204e1308202c9a003020102020454c60934300d06092a864886f70d01010b050030213110300e060355040b1307462d44726f6964310d300b060355040313046e65626f301e170d3136303331303230313634325a170d3433303732373230313634325a30213110300e060355040b1307462d44726f6964310d300b060355040313046e65626f30820222300d06092a864886f70d01010105000382020f003082020a0282020100ac59258ca2e9c216af14d58cb53adb13658480aed5ebc1f59bfc474f0f67c0efe9d58304d0cbda2897bd3283e7afe15512f32743ee243f4b9bba5a017806bc5c3441c905df37d00d3cf77b012af33ee4033b7e8d686277043bcb28241a3fe9f6ebfd72f305a928e300edf554ffaa139d85b5c9282aa8f1a82ff74caea2c13006dbeae8aac9ff44fa4c9122808b90c304db8b9e6ddecdbfbf5ce4ed0115cf1ba2bc6a4d6211765553df9b650db69155448aec4b0aaf59d19712aca3010a0d96eb02ed84e90c16162272af32fe909a5acde37d78fba500994f50c1ec5afa528945a7567567560a9fbafbabd68190c5c13f9a53f39a72734bd8de43c06b21a5cecf2747e6a1879352c49ee29fa092c26ca495baac69eddb614941e27b6a27fb3fb74cbdfe5822bfc266130c1f723a7ab91ed3d6c5261d31fc80ab82b7caa2727120522e65863af436a438c05039e1e099faae4d6170baa10fc9bb7bf101e2b4c9769e693eb7e4e3eebd47bfbfe0069c24a8b1ef72d8fe6549202490cff7b0f36c458b8192fe58f984839290d69639abb15fe1ef2925eb491627f2eefbd13225b925a7bbfc0fb4d95a3fb43599c172037e599639b4f86c4eabc173013776a854e146dfacf424cbae4254f9806ecd79d092f5e67a2f00c98ad64c0bfbeaff117fe4c62685e2e75e2ef507325d05f866510c20006a6c01e8e25d75bd42a0d5397b73eb0203010001a321301f301d0603551d0e0416041417f4fd41b0aa3f4fa981423a123f6f6016e3ce80300d06092a864886f70d01010b050003820201008d5d93cbb48fde9df566d75c54a8da2f29e9ae1bac2ed2436a0f165730244ac9e471b473674bc68717c34e30c29ce5ffa027fa12a7eb2f45b036db0cca79238262ba84f6ec8ffddcfe2b398c0a6aa33d117f83996b3bece96b1ea6f8066c395e5021c2b5fe1638c7ac146cda6ef2e4a836bd9c968ed76c51cc0b09caa4b1a79d5d10b3829804db992a70feb9a76535bc04631193abee9c9d7ebfb07ad464542f65744e76d92c5aeb3beb96dbb0b3d746845cbfa2b12c6da31863ea4a0d664dc5974d5b808c1be52a5e595ed181d86feeff4dc82bc8ee3c11ff807a811322931e804df1d90b5b813dd9ce81f3d8dd7d1bb2994901fe1c1004673f53c7b60cdbc2f914ce0718fbfc8e89b443091f71ecb9f169d558c3818bb1db714a47025154eb974600ca54e29933a87a4080910eee05dcc34de7048fa95b1128d8910b18b5957f2e745de00decd2434af455b24aa3e53de889e37919212a6adb3f4088baec6cc9f3e21b812593605fba0394355bd994f21ceaba861aae29244f5113d4291fdddedbef091e63885ebf318c6e12d338fa9555783643a19181c2cc935307fcee5e6dabf8dd6e19a92b29dbc529d3ef170916fb7b2d9dbf95a358ac7c0204b6e6a416b59441c49c41d6f78b1de63eb8b10c516a5952a20eb0c595cfa21530350c5adde74d815918deb870a9e7750fcb4dc50538fd591006434cbbb001cc2ae1fe11</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.looker.droidify
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.looker.droidify.index.OemRepositoryParser
|
||||
import com.looker.droidify.sync.common.assets
|
||||
import org.junit.Before
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@SmallTest
|
||||
class OemRepositoryParserTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseFile() {
|
||||
val stream = assets("additional_repos.xml")
|
||||
val list = OemRepositoryParser.parse(stream)
|
||||
assertEquals(3, list.size)
|
||||
val listOfNames = list.map { it.name }
|
||||
assertContentEquals(listOfNames, listOf("SHIFT", "microG F-Droid repo", "IzzyOnDroid F-Droid Repo"))
|
||||
}
|
||||
}
|
||||
116
app/src/androidTest/kotlin/com/looker/droidify/RoomTesting.kt
Normal file
116
app/src/androidTest/kotlin/com/looker/droidify/RoomTesting.kt
Normal file
@@ -0,0 +1,116 @@
|
||||
package com.looker.droidify
|
||||
|
||||
import android.content.Context
|
||||
import com.looker.droidify.data.local.dao.AppDao
|
||||
import com.looker.droidify.data.local.dao.IndexDao
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.domain.model.VersionInfo
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.sync.FakeDownloader
|
||||
import com.looker.droidify.sync.common.JsonParser
|
||||
import com.looker.droidify.sync.common.downloadIndex
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import javax.inject.Inject
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@HiltAndroidTest
|
||||
class RoomTesting {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var indexDao: IndexDao
|
||||
|
||||
@Inject
|
||||
lateinit var appDao: AppDao
|
||||
|
||||
@Inject
|
||||
@ApplicationContext
|
||||
lateinit var context: Context
|
||||
|
||||
private val defaults = Repository.defaultRepositories
|
||||
private val izzyLegacy = defaults[4]
|
||||
private val fdroidLegacy = defaults[0]
|
||||
|
||||
@Before
|
||||
fun before() = runTest {
|
||||
hiltRule.inject()
|
||||
launch {
|
||||
val izzy = izzyLegacy.toRepo(1)
|
||||
val izzyFile = FakeDownloader.downloadIndex(context, izzy, "i2", "index-v2.json")
|
||||
val izzyIndex =
|
||||
JsonParser.decodeFromString<IndexV2>(izzyFile.readBytes().decodeToString())
|
||||
indexDao.insertIndex(
|
||||
fingerprint = izzy.fingerprint!!,
|
||||
index = izzyIndex,
|
||||
expectedRepoId = izzy.id,
|
||||
)
|
||||
}
|
||||
// launch {
|
||||
// val fdroid = fdroidLegacy.toRepo(2)
|
||||
// val fdroidFile =
|
||||
// FakeDownloader.downloadIndex(context, fdroid, "f2", "fdroid-index-v2.json")
|
||||
// val fdroidIndex =
|
||||
// JsonParser.decodeFromString<IndexV2>(fdroidFile.readBytes().decodeToString())
|
||||
// indexDao.insertIndex(
|
||||
// fingerprint = fdroid.fingerprint!!,
|
||||
// index = fdroidIndex,
|
||||
// expectedRepoId = fdroid.id,
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sortOrderTest() = runTest {
|
||||
val lastUpdatedQuery = appDao.query(sortOrder = SortOrder.UPDATED)
|
||||
var previousUpdated = Long.MAX_VALUE
|
||||
lastUpdatedQuery.forEach {
|
||||
println("Previous: $previousUpdated, Current: ${it.lastUpdated}")
|
||||
assertTrue(it.lastUpdated <= previousUpdated)
|
||||
previousUpdated = it.lastUpdated
|
||||
}
|
||||
|
||||
val addedQuery = appDao.query(sortOrder = SortOrder.ADDED)
|
||||
var previousAdded = Long.MAX_VALUE
|
||||
addedQuery.forEach {
|
||||
println("Previous: $previousAdded, Current: ${it.added}")
|
||||
assertTrue(it.added <= previousAdded)
|
||||
previousAdded = it.added
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun categoryTest() = runTest {
|
||||
val categoryQuery = appDao.query(
|
||||
sortOrder = SortOrder.UPDATED,
|
||||
categoriesToInclude = listOf("Games", "Food"),
|
||||
)
|
||||
val nonCategoryQuery = appDao.query(
|
||||
sortOrder = SortOrder.UPDATED,
|
||||
categoriesToExclude = listOf("Games", "Food"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Repository.toRepo(id: Int) = Repo(
|
||||
id = id,
|
||||
enabled = enabled,
|
||||
address = address,
|
||||
name = name,
|
||||
description = description,
|
||||
fingerprint = Fingerprint(fingerprint),
|
||||
authentication = null,
|
||||
versionInfo = VersionInfo(timestamp, entityTag),
|
||||
mirrors = emptyList(),
|
||||
)
|
||||
16
app/src/androidTest/kotlin/com/looker/droidify/TestRunner.kt
Normal file
16
app/src/androidTest/kotlin/com/looker/droidify/TestRunner.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.looker.droidify
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
|
||||
class TestRunner : AndroidJUnitRunner() {
|
||||
override fun newApplication(
|
||||
cl: ClassLoader,
|
||||
appName: String,
|
||||
context: Context,
|
||||
): Application {
|
||||
return super.newApplication(cl, HiltTestApplication::class.java.getName(), context)
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,18 @@ import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.index.RepositoryUpdater.IndexType
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.sync.FakeDownloader
|
||||
import com.looker.droidify.sync.common.assets
|
||||
import com.looker.droidify.sync.common.benchmark
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -21,7 +27,9 @@ class RepositoryUpdaterTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
Database.init(context)
|
||||
RepositoryUpdater.init(CoroutineScope(Dispatchers.Default), FakeDownloader)
|
||||
repository = Repository(
|
||||
id = 15,
|
||||
address = "https://apt.izzysoft.de/fdroid/repo",
|
||||
@@ -41,13 +49,14 @@ class RepositoryUpdaterTest {
|
||||
|
||||
@Test
|
||||
fun processFile() {
|
||||
testRepetition(1) {
|
||||
val output = benchmark(1) {
|
||||
val createFile = File.createTempFile("index", "entry")
|
||||
val mergerFile = File.createTempFile("index", "merger")
|
||||
val jarStream = context.resources.assets.open("index-v1.jar")
|
||||
jarStream.copyTo(createFile.outputStream())
|
||||
process(createFile, mergerFile)
|
||||
}
|
||||
println(output)
|
||||
}
|
||||
|
||||
private fun process(file: File, merger: File) = measureTimeMillis {
|
||||
@@ -65,28 +74,4 @@ class RepositoryUpdaterTest {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun testRepetition(repetition: Int, block: () -> Long) {
|
||||
val times = (1..repetition).map {
|
||||
System.gc()
|
||||
System.runFinalization()
|
||||
block().toDouble()
|
||||
}
|
||||
val meanAndDeviation = times.culledMeanAndDeviation()
|
||||
println(times)
|
||||
println("${meanAndDeviation.first} ± ${meanAndDeviation.second}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Double>.culledMeanAndDeviation(): Pair<Double, Double> = when {
|
||||
isEmpty() -> Double.NaN to Double.NaN
|
||||
size == 1 || size == 2 -> this.meanAndDeviation()
|
||||
else -> sorted().subList(1, size - 1).meanAndDeviation()
|
||||
}
|
||||
|
||||
private fun List<Double>.meanAndDeviation(): Pair<Double, Double> {
|
||||
val mean = average()
|
||||
return mean to sqrt(fold(0.0) { acc, value -> acc + (value - mean).squared() } / size)
|
||||
}
|
||||
|
||||
private fun Double.squared() = this * this
|
||||
|
||||
@@ -44,7 +44,7 @@ class EntrySyncableTest {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Before
|
||||
fun before() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
dispatcher = StandardTestDispatcher()
|
||||
validator = IndexJarValidator(dispatcher)
|
||||
parser = EntryParser(dispatcher, JsonParser, validator)
|
||||
|
||||
@@ -7,8 +7,8 @@ import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.sync.common.IndexJarValidator
|
||||
import com.looker.droidify.sync.common.Izzy
|
||||
import com.looker.droidify.sync.common.JsonParser
|
||||
import com.looker.droidify.sync.common.downloadIndex
|
||||
import com.looker.droidify.sync.common.benchmark
|
||||
import com.looker.droidify.sync.common.downloadIndex
|
||||
import com.looker.droidify.sync.common.toV2
|
||||
import com.looker.droidify.sync.v1.V1Parser
|
||||
import com.looker.droidify.sync.v1.V1Syncable
|
||||
@@ -17,6 +17,7 @@ import com.looker.droidify.sync.v2.V2Parser
|
||||
import com.looker.droidify.sync.v2.model.FileV2
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||
import com.looker.droidify.sync.v2.model.PackageV2
|
||||
import com.looker.droidify.sync.v2.model.VersionV2
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
@@ -28,6 +29,8 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class V1SyncableTest {
|
||||
@@ -42,7 +45,7 @@ class V1SyncableTest {
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
dispatcher = StandardTestDispatcher()
|
||||
validator = IndexJarValidator(dispatcher)
|
||||
parser = V1Parser(dispatcher, JsonParser, validator)
|
||||
@@ -102,9 +105,38 @@ class V1SyncableTest {
|
||||
testIndexConversion("index-v1.jar", "index-v2-updated.json")
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun v1tov2FDroidRepo() = runTest(dispatcher) {
|
||||
testIndexConversion("fdroid-index-v1.jar", "fdroid-index-v2.json")
|
||||
@Test
|
||||
fun targetPropertyTest() = runTest(dispatcher) {
|
||||
val v2IzzyFile =
|
||||
FakeDownloader.downloadIndex(context, repo, "izzy-v2", "index-v2-updated.json")
|
||||
val v2FdroidFile =
|
||||
FakeDownloader.downloadIndex(context, repo, "fdroid-v2", "fdroid-index-v2.json")
|
||||
val (_, v2Izzy) = v2Parser.parse(v2IzzyFile, repo)
|
||||
val (_, v2Fdroid) = v2Parser.parse(v2FdroidFile, repo)
|
||||
|
||||
val performTest: (PackageV2) -> Unit = { data ->
|
||||
print("lib: ")
|
||||
println(data.metadata.liberapay)
|
||||
print("donate: ")
|
||||
println(data.metadata.donate)
|
||||
print("bit: ")
|
||||
println(data.metadata.bitcoin)
|
||||
print("flattr: ")
|
||||
println(data.metadata.flattrID)
|
||||
print("Open: ")
|
||||
println(data.metadata.openCollective)
|
||||
print("LiteCoin: ")
|
||||
println(data.metadata.litecoin)
|
||||
}
|
||||
|
||||
v2Izzy.packages.forEach { (packageName, data) ->
|
||||
println("Testing on Izzy $packageName")
|
||||
performTest(data)
|
||||
}
|
||||
v2Fdroid.packages.forEach { (packageName, data) ->
|
||||
println("Testing on FDroid $packageName")
|
||||
performTest(data)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun testIndexConversion(
|
||||
@@ -252,6 +284,8 @@ private fun assertVersion(
|
||||
assertNotNull(foundVersion)
|
||||
|
||||
assertEquals(expectedVersion.added, foundVersion.added)
|
||||
assertEquals(expectedVersion.file.sha256, foundVersion.file.sha256)
|
||||
assertEquals(expectedVersion.file.size, foundVersion.file.size)
|
||||
assertEquals(expectedVersion.file.name, foundVersion.file.name)
|
||||
assertEquals(expectedVersion.src?.name, foundVersion.src?.name)
|
||||
|
||||
@@ -261,7 +295,13 @@ private fun assertVersion(
|
||||
assertEquals(expectedMan.versionCode, foundMan.versionCode)
|
||||
assertEquals(expectedMan.versionName, foundMan.versionName)
|
||||
assertEquals(expectedMan.maxSdkVersion, foundMan.maxSdkVersion)
|
||||
assertNotNull(expectedMan.usesSdk)
|
||||
assertNotNull(foundMan.usesSdk)
|
||||
assertEquals(expectedMan.usesSdk, foundMan.usesSdk)
|
||||
assertTrue(expectedMan.usesSdk.minSdkVersion >= 1)
|
||||
assertTrue(expectedMan.usesSdk.targetSdkVersion >= 1)
|
||||
assertTrue(foundMan.usesSdk.minSdkVersion >= 1)
|
||||
assertTrue(foundMan.usesSdk.targetSdkVersion >= 1)
|
||||
|
||||
assertContentEquals(
|
||||
expectedMan.features.sortedBy { it.name },
|
||||
|
||||
@@ -8,11 +8,6 @@ internal inline fun benchmark(
|
||||
extraMessage: String? = null,
|
||||
block: () -> Long,
|
||||
): String {
|
||||
if (extraMessage != null) {
|
||||
println("=".repeat(50))
|
||||
println(extraMessage)
|
||||
println("=".repeat(50))
|
||||
}
|
||||
val times = DoubleArray(repetition)
|
||||
repeat(repetition) { iteration ->
|
||||
System.gc()
|
||||
@@ -20,11 +15,19 @@ internal inline fun benchmark(
|
||||
times[iteration] = block().toDouble()
|
||||
}
|
||||
val meanAndDeviation = times.culledMeanAndDeviation()
|
||||
return buildString {
|
||||
return buildString(200) {
|
||||
append("=".repeat(50))
|
||||
append("\n")
|
||||
if (extraMessage != null) {
|
||||
append(extraMessage)
|
||||
append("\n")
|
||||
append("=".repeat(50))
|
||||
append("\n")
|
||||
}
|
||||
if (times.size > 1) {
|
||||
append(times.joinToString(" | "))
|
||||
append("\n")
|
||||
}
|
||||
append("${meanAndDeviation.first} ms ± ${meanAndDeviation.second.toFloat()} ms")
|
||||
append("\n")
|
||||
append("=".repeat(50))
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.domain.model.VersionInfo
|
||||
|
||||
val Izzy = Repo(
|
||||
id = 1L,
|
||||
id = 1,
|
||||
enabled = true,
|
||||
address = "https://apt.izzysoft.de/fdroid/repo",
|
||||
name = "IzzyOnDroid F-Droid Repo",
|
||||
@@ -15,6 +15,4 @@ val Izzy = Repo(
|
||||
authentication = Authentication("", ""),
|
||||
versionInfo = VersionInfo(0L, null),
|
||||
mirrors = emptyList(),
|
||||
antiFeatures = emptyList(),
|
||||
categories = emptyList(),
|
||||
)
|
||||
|
||||
1
app/src/debug/assets/izzy_index_v2.json
Normal file
1
app/src/debug/assets/izzy_index_v2.json
Normal file
File diff suppressed because one or more lines are too long
@@ -97,7 +97,7 @@
|
||||
<data android:pathPattern="/.*/packages/.*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
@@ -80,7 +80,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
|
||||
// if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
|
||||
|
||||
val databaseUpdated = Database.init(this)
|
||||
ProductPreferences.init(this, appScope)
|
||||
@@ -107,7 +107,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
},
|
||||
)
|
||||
val installedItems =
|
||||
packageManager.getInstalledPackagesCompat()
|
||||
@@ -200,7 +200,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
periodMillis = period,
|
||||
networkType = syncConditions.toJobNetworkType(),
|
||||
isCharging = syncConditions.pluggedIn,
|
||||
isBatteryLow = syncConditions.batteryNotLow
|
||||
isBatteryLow = syncConditions.batteryNotLow,
|
||||
)
|
||||
jobScheduler?.schedule(job)
|
||||
}
|
||||
@@ -212,10 +212,13 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = ""))
|
||||
}
|
||||
}
|
||||
Connection(SyncService::class.java, onBind = { connection, binder ->
|
||||
Connection(
|
||||
SyncService::class.java,
|
||||
onBind = { connection, binder ->
|
||||
binder.sync(SyncService.SyncRequest.FORCE)
|
||||
connection.unbind(this)
|
||||
}).bind(this)
|
||||
},
|
||||
).bind(this)
|
||||
}
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
@@ -256,12 +259,12 @@ fun strictThreadPolicy() {
|
||||
.detectNetwork()
|
||||
.detectUnbufferedIo()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,13 +14,6 @@ import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.looker.droidify.utility.common.DeeplinkType
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.deeplinkType
|
||||
import com.looker.droidify.utility.common.extension.homeAsUp
|
||||
import com.looker.droidify.utility.common.extension.inputManager
|
||||
import com.looker.droidify.utility.common.getInstallPackageName
|
||||
import com.looker.droidify.utility.common.requestNotificationPermission
|
||||
import com.looker.droidify.database.CursorOwner
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.extension.getThemeRes
|
||||
@@ -34,6 +27,13 @@ import com.looker.droidify.ui.repository.RepositoriesFragment
|
||||
import com.looker.droidify.ui.repository.RepositoryFragment
|
||||
import com.looker.droidify.ui.settings.SettingsFragment
|
||||
import com.looker.droidify.ui.tabsFragment.TabsFragment
|
||||
import com.looker.droidify.utility.common.DeeplinkType
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.deeplinkType
|
||||
import com.looker.droidify.utility.common.extension.homeAsUp
|
||||
import com.looker.droidify.utility.common.extension.inputManager
|
||||
import com.looker.droidify.utility.common.getInstallPackageName
|
||||
import com.looker.droidify.utility.common.requestNotificationPermission
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -64,7 +64,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
@Parcelize
|
||||
private class FragmentStackItem(
|
||||
val className: String, val arguments: Bundle?, val savedState: Fragment.SavedState?
|
||||
val className: String, val arguments: Bundle?, val savedState: Fragment.SavedState?,
|
||||
) : Parcelable
|
||||
|
||||
lateinit var cursorOwner: CursorOwner
|
||||
@@ -87,24 +87,25 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun collectChange() {
|
||||
val hiltEntryPoint = EntryPointAccessors.fromApplication(
|
||||
this, CustomUserRepositoryInjector::class.java
|
||||
)
|
||||
val hiltEntryPoint =
|
||||
EntryPointAccessors.fromApplication(this, CustomUserRepositoryInjector::class.java)
|
||||
val newSettings = hiltEntryPoint.settingsRepository().get { theme to dynamicTheme }
|
||||
runBlocking {
|
||||
val theme = newSettings.first()
|
||||
setTheme(
|
||||
resources.configuration.getThemeRes(
|
||||
theme = theme.first, dynamicTheme = theme.second
|
||||
)
|
||||
theme = theme.first,
|
||||
dynamicTheme = theme.second,
|
||||
),
|
||||
)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
newSettings.drop(1).collect { themeAndDynamic ->
|
||||
setTheme(
|
||||
resources.configuration.getThemeRes(
|
||||
theme = themeAndDynamic.first, dynamicTheme = themeAndDynamic.second
|
||||
)
|
||||
theme = themeAndDynamic.first,
|
||||
dynamicTheme = themeAndDynamic.second,
|
||||
),
|
||||
)
|
||||
recreate()
|
||||
}
|
||||
@@ -116,9 +117,11 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
val rootView = FrameLayout(this).apply { id = R.id.main_content }
|
||||
addContentView(
|
||||
rootView, ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
rootView,
|
||||
ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
),
|
||||
)
|
||||
|
||||
requestNotificationPermission(request = notificationPermission::launch)
|
||||
@@ -188,7 +191,7 @@ class MainActivity : AppCompatActivity() {
|
||||
if (open != null) {
|
||||
setCustomAnimations(
|
||||
if (open) R.animator.slide_in else 0,
|
||||
if (open) R.animator.slide_in_keep else R.animator.slide_out
|
||||
if (open) R.animator.slide_in_keep else R.animator.slide_out,
|
||||
)
|
||||
}
|
||||
setReorderingAllowed(true)
|
||||
@@ -202,8 +205,8 @@ class MainActivity : AppCompatActivity() {
|
||||
FragmentStackItem(
|
||||
it::class.java.name,
|
||||
it.arguments,
|
||||
supportFragmentManager.saveFragmentInstanceState(it)
|
||||
)
|
||||
supportFragmentManager.saveFragmentInstanceState(it),
|
||||
),
|
||||
)
|
||||
}
|
||||
replaceFragment(fragment, true)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package com.looker.droidify.data.encryption
|
||||
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
private const val KEY_SIZE = 256
|
||||
private const val IV_SIZE = 16
|
||||
private const val ALGORITHM = "AES"
|
||||
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||
|
||||
@JvmInline
|
||||
value class Key(val secretKey: ByteArray) {
|
||||
|
||||
val spec: SecretKeySpec
|
||||
get() = SecretKeySpec(secretKey, ALGORITHM)
|
||||
|
||||
fun encrypt(input: String): Pair<Encrypted, ByteArray> {
|
||||
val iv = generateIV()
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, spec, ivSpec)
|
||||
val encrypted = cipher.doFinal(input.toByteArray())
|
||||
return Encrypted(Base64.encode(encrypted)) to iv
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Before encrypting we convert it to a base64 string
|
||||
* */
|
||||
@JvmInline
|
||||
value class Encrypted(val value: String) {
|
||||
fun decrypt(key: Key, iv: ByteArray): String {
|
||||
val iv = IvParameterSpec(iv)
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.DECRYPT_MODE, key.spec, iv)
|
||||
val decrypted = cipher.doFinal(Base64.decode(value))
|
||||
return String(decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateSecretKey(): ByteArray {
|
||||
return with(KeyGenerator.getInstance(ALGORITHM)) {
|
||||
init(KEY_SIZE)
|
||||
generateKey().encoded
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateIV(): ByteArray {
|
||||
val iv = ByteArray(IV_SIZE)
|
||||
val secureRandom = SecureRandom()
|
||||
secureRandom.nextBytes(iv)
|
||||
return iv
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.looker.droidify.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.BuiltInTypeConverters
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.looker.droidify.data.local.converters.Converters
|
||||
import com.looker.droidify.data.local.converters.PermissionConverter
|
||||
import com.looker.droidify.data.local.dao.AppDao
|
||||
import com.looker.droidify.data.local.dao.AuthDao
|
||||
import com.looker.droidify.data.local.dao.IndexDao
|
||||
import com.looker.droidify.data.local.dao.RepoDao
|
||||
import com.looker.droidify.data.local.model.AntiFeatureAppRelation
|
||||
import com.looker.droidify.data.local.model.AntiFeatureEntity
|
||||
import com.looker.droidify.data.local.model.AntiFeatureRepoRelation
|
||||
import com.looker.droidify.data.local.model.AppEntity
|
||||
import com.looker.droidify.data.local.model.AuthenticationEntity
|
||||
import com.looker.droidify.data.local.model.AuthorEntity
|
||||
import com.looker.droidify.data.local.model.CategoryAppRelation
|
||||
import com.looker.droidify.data.local.model.CategoryEntity
|
||||
import com.looker.droidify.data.local.model.CategoryRepoRelation
|
||||
import com.looker.droidify.data.local.model.DonateEntity
|
||||
import com.looker.droidify.data.local.model.GraphicEntity
|
||||
import com.looker.droidify.data.local.model.InstalledEntity
|
||||
import com.looker.droidify.data.local.model.LinksEntity
|
||||
import com.looker.droidify.data.local.model.MirrorEntity
|
||||
import com.looker.droidify.data.local.model.RepoEntity
|
||||
import com.looker.droidify.data.local.model.ScreenshotEntity
|
||||
import com.looker.droidify.data.local.model.VersionEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
AntiFeatureEntity::class,
|
||||
AntiFeatureAppRelation::class,
|
||||
AntiFeatureRepoRelation::class,
|
||||
AuthenticationEntity::class,
|
||||
AuthorEntity::class,
|
||||
AppEntity::class,
|
||||
CategoryEntity::class,
|
||||
CategoryAppRelation::class,
|
||||
CategoryRepoRelation::class,
|
||||
DonateEntity::class,
|
||||
GraphicEntity::class,
|
||||
InstalledEntity::class,
|
||||
LinksEntity::class,
|
||||
MirrorEntity::class,
|
||||
RepoEntity::class,
|
||||
ScreenshotEntity::class,
|
||||
VersionEntity::class,
|
||||
],
|
||||
version = 1,
|
||||
)
|
||||
@TypeConverters(
|
||||
PermissionConverter::class,
|
||||
Converters::class,
|
||||
builtInTypeConverters = BuiltInTypeConverters(),
|
||||
)
|
||||
abstract class DroidifyDatabase : RoomDatabase() {
|
||||
abstract fun appDao(): AppDao
|
||||
abstract fun repoDao(): RepoDao
|
||||
abstract fun authDao(): AuthDao
|
||||
abstract fun indexDao(): IndexDao
|
||||
}
|
||||
|
||||
fun droidifyDatabase(context: Context): DroidifyDatabase = Room
|
||||
.databaseBuilder(
|
||||
context = context,
|
||||
klass = DroidifyDatabase::class.java,
|
||||
name = "droidify_room",
|
||||
)
|
||||
.addCallback(
|
||||
object : RoomDatabase.Callback() {
|
||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||
super.onOpen(db)
|
||||
db.query("PRAGMA synchronous = OFF")
|
||||
db.query("PRAGMA journal_mode = WAL")
|
||||
}
|
||||
},
|
||||
)
|
||||
.build()
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.looker.droidify.data.local.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.looker.droidify.sync.common.JsonParser
|
||||
import com.looker.droidify.sync.v2.model.FileV2
|
||||
import com.looker.droidify.sync.v2.model.LocalizedIcon
|
||||
import com.looker.droidify.sync.v2.model.LocalizedString
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
private val localizedStringSerializer =
|
||||
MapSerializer(String.serializer(), String.serializer())
|
||||
private val stringListSerializer = ListSerializer(String.serializer())
|
||||
private val localizedIconSerializer =
|
||||
MapSerializer(String.serializer(), FileV2.serializer())
|
||||
private val mapOfLocalizedStringsSerializer =
|
||||
MapSerializer(String.serializer(), localizedStringSerializer)
|
||||
|
||||
object Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun fromLocalizedString(value: LocalizedString): String {
|
||||
return JsonParser.encodeToString(localizedStringSerializer, value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toLocalizedString(value: String): LocalizedString {
|
||||
return JsonParser.decodeFromString(localizedStringSerializer, value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromLocalizedIcon(value: LocalizedIcon?): String? {
|
||||
return value?.let { JsonParser.encodeToString(localizedIconSerializer, it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toLocalizedIcon(value: String?): LocalizedIcon? {
|
||||
return value?.let { JsonParser.decodeFromString(localizedIconSerializer, it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromLocalizedList(value: Map<String, LocalizedString>): String {
|
||||
return JsonParser.encodeToString(mapOfLocalizedStringsSerializer, value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toLocalizedList(value: String): Map<String, LocalizedString> {
|
||||
return JsonParser.decodeFromString(mapOfLocalizedStringsSerializer, value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromStringList(value: List<String>): String {
|
||||
return JsonParser.encodeToString(stringListSerializer, value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toStringList(value: String): List<String> {
|
||||
return JsonParser.decodeFromString(stringListSerializer, value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.looker.droidify.data.local.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.looker.droidify.sync.common.JsonParser
|
||||
import com.looker.droidify.sync.v2.model.PermissionV2
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
|
||||
private val permissionListSerializer = ListSerializer(PermissionV2.serializer())
|
||||
|
||||
object PermissionConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun fromPermissionV2List(value: List<PermissionV2>): String {
|
||||
return JsonParser.encodeToString(permissionListSerializer, value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toPermissionV2List(value: String): List<PermissionV2> {
|
||||
return JsonParser.decodeFromString(permissionListSerializer, value)
|
||||
}
|
||||
}
|
||||
189
app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt
Normal file
189
app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt
Normal file
@@ -0,0 +1,189 @@
|
||||
package com.looker.droidify.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.RawQuery
|
||||
import androidx.room.Transaction
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import com.looker.droidify.data.local.model.AntiFeatureAppRelation
|
||||
import com.looker.droidify.data.local.model.AppEntity
|
||||
import com.looker.droidify.data.local.model.AppEntityRelations
|
||||
import com.looker.droidify.data.local.model.CategoryAppRelation
|
||||
import com.looker.droidify.data.local.model.VersionEntity
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.sync.v2.model.DefaultName
|
||||
import com.looker.droidify.sync.v2.model.Tag
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface AppDao {
|
||||
|
||||
@RawQuery(
|
||||
observedEntities = [
|
||||
AppEntity::class,
|
||||
VersionEntity::class,
|
||||
CategoryAppRelation::class,
|
||||
AntiFeatureAppRelation::class,
|
||||
],
|
||||
)
|
||||
fun _rawStreamAppEntities(query: SimpleSQLiteQuery): Flow<List<AppEntity>>
|
||||
|
||||
@RawQuery
|
||||
suspend fun _rawQueryAppEntities(query: SimpleSQLiteQuery): List<AppEntity>
|
||||
|
||||
fun stream(
|
||||
sortOrder: SortOrder,
|
||||
searchQuery: String? = null,
|
||||
repoId: Int? = null,
|
||||
categoriesToInclude: List<DefaultName>? = null,
|
||||
categoriesToExclude: List<DefaultName>? = null,
|
||||
antiFeaturesToInclude: List<Tag>? = null,
|
||||
antiFeaturesToExclude: List<Tag>? = null,
|
||||
): Flow<List<AppEntity>> = _rawStreamAppEntities(
|
||||
searchQuery(
|
||||
sortOrder = sortOrder,
|
||||
searchQuery = searchQuery,
|
||||
repoId = repoId,
|
||||
categoriesToInclude = categoriesToInclude,
|
||||
categoriesToExclude = categoriesToExclude,
|
||||
antiFeaturesToInclude = antiFeaturesToInclude,
|
||||
antiFeaturesToExclude = antiFeaturesToExclude,
|
||||
),
|
||||
)
|
||||
|
||||
suspend fun query(
|
||||
sortOrder: SortOrder,
|
||||
searchQuery: String? = null,
|
||||
repoId: Int? = null,
|
||||
categoriesToInclude: List<DefaultName>? = null,
|
||||
categoriesToExclude: List<DefaultName>? = null,
|
||||
antiFeaturesToInclude: List<Tag>? = null,
|
||||
antiFeaturesToExclude: List<Tag>? = null,
|
||||
): List<AppEntity> = _rawQueryAppEntities(
|
||||
searchQuery(
|
||||
sortOrder = sortOrder,
|
||||
searchQuery = searchQuery,
|
||||
repoId = repoId,
|
||||
categoriesToInclude = categoriesToInclude,
|
||||
categoriesToExclude = categoriesToExclude,
|
||||
antiFeaturesToInclude = antiFeaturesToInclude,
|
||||
antiFeaturesToExclude = antiFeaturesToExclude,
|
||||
),
|
||||
)
|
||||
|
||||
private fun searchQuery(
|
||||
sortOrder: SortOrder,
|
||||
searchQuery: String?,
|
||||
repoId: Int?,
|
||||
categoriesToInclude: List<DefaultName>?,
|
||||
categoriesToExclude: List<DefaultName>?,
|
||||
antiFeaturesToInclude: List<Tag>?,
|
||||
antiFeaturesToExclude: List<Tag>?,
|
||||
): SimpleSQLiteQuery {
|
||||
val args = arrayListOf<Any?>()
|
||||
|
||||
val query = buildString(1024) {
|
||||
append("SELECT DISTINCT app.* FROM app")
|
||||
append(" LEFT JOIN version ON app.id = version.appId")
|
||||
append(" LEFT JOIN category_app_relation ON app.id = category_app_relation.id")
|
||||
append(" LEFT JOIN anti_features_app_relation ON app.id = anti_features_app_relation.appId")
|
||||
append(" WHERE 1")
|
||||
|
||||
if (repoId != null) {
|
||||
append(" AND app.repoId = ?")
|
||||
args.add(repoId)
|
||||
}
|
||||
|
||||
if (categoriesToInclude != null) {
|
||||
append(" AND category_app_relation.defaultName IN (")
|
||||
append(categoriesToInclude.joinToString(", ") { "?" })
|
||||
append(")")
|
||||
args.addAll(categoriesToInclude)
|
||||
}
|
||||
|
||||
if (categoriesToExclude != null) {
|
||||
append(" AND category_app_relation.defaultName NOT IN (")
|
||||
append(categoriesToExclude.joinToString(", ") { "?" })
|
||||
append(")")
|
||||
args.addAll(categoriesToExclude)
|
||||
}
|
||||
|
||||
if (antiFeaturesToInclude != null) {
|
||||
append(" AND anti_features_app_relation.tag IN (")
|
||||
append(antiFeaturesToInclude.joinToString(", ") { "?" })
|
||||
append(")")
|
||||
args.addAll(antiFeaturesToInclude)
|
||||
}
|
||||
|
||||
if (antiFeaturesToExclude != null) {
|
||||
append(" AND anti_features_app_relation.tag NOT IN (")
|
||||
append(antiFeaturesToExclude.joinToString(", ") { "?" })
|
||||
append(")")
|
||||
args.addAll(antiFeaturesToExclude)
|
||||
}
|
||||
|
||||
if (searchQuery != null) {
|
||||
val searchPattern = "%${searchQuery}%"
|
||||
append(
|
||||
"""
|
||||
AND (
|
||||
app.name LIKE ?
|
||||
OR app.summary LIKE ?
|
||||
OR app.packageName LIKE ?
|
||||
OR app.description LIKE ?
|
||||
)""",
|
||||
)
|
||||
args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern))
|
||||
}
|
||||
|
||||
append(" ORDER BY ")
|
||||
|
||||
// Weighting: name > summary > packageName > description
|
||||
if (searchQuery != null) {
|
||||
val searchPattern = "%${searchQuery}%"
|
||||
append("(CASE WHEN app.name LIKE ? THEN 4 ELSE 0 END) + ")
|
||||
append("(CASE WHEN app.summary LIKE ? THEN 3 ELSE 0 END) + ")
|
||||
append("(CASE WHEN app.packageName LIKE ? THEN 2 ELSE 0 END) + ")
|
||||
append("(CASE WHEN app.description LIKE ? THEN 1 ELSE 0 END) DESC, ")
|
||||
args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern))
|
||||
}
|
||||
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED -> append("app.lastUpdated DESC, ")
|
||||
SortOrder.ADDED -> append("app.added DESC, ")
|
||||
SortOrder.SIZE -> append("version.apk_size DESC, ")
|
||||
SortOrder.NAME -> Unit
|
||||
}
|
||||
append("app.name COLLATE LOCALIZED ASC")
|
||||
}
|
||||
|
||||
return SimpleSQLiteQuery(query, args.toTypedArray())
|
||||
}
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT app.*
|
||||
FROM app
|
||||
LEFT JOIN installed
|
||||
ON app.packageName = installed.packageName
|
||||
LEFT JOIN version
|
||||
ON version.appId = app.id
|
||||
WHERE installed.packageName IS NOT NULL
|
||||
ORDER BY
|
||||
CASE WHEN version.versionCode > installed.versionCode THEN 1 ELSE 2 END,
|
||||
app.lastUpdated DESC,
|
||||
app.name COLLATE LOCALIZED ASC
|
||||
""",
|
||||
)
|
||||
fun installedStream(): Flow<List<AppEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM app WHERE packageName = :packageName")
|
||||
fun queryAppEntity(packageName: String): Flow<List<AppEntityRelations>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM app")
|
||||
suspend fun count(): Int
|
||||
|
||||
@Query("DELETE FROM app WHERE id = :id")
|
||||
suspend fun delete(id: Int)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.looker.droidify.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.looker.droidify.data.local.model.AuthenticationEntity
|
||||
|
||||
@Dao
|
||||
interface AuthDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(authentication: AuthenticationEntity)
|
||||
|
||||
@Query("SELECT * FROM authentication WHERE repoId = :repoId")
|
||||
suspend fun getAuthentication(repoId: Int): AuthenticationEntity?
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.looker.droidify.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import com.looker.droidify.data.local.model.AntiFeatureAppRelation
|
||||
import com.looker.droidify.data.local.model.AntiFeatureEntity
|
||||
import com.looker.droidify.data.local.model.AntiFeatureRepoRelation
|
||||
import com.looker.droidify.data.local.model.AppEntity
|
||||
import com.looker.droidify.data.local.model.AuthorEntity
|
||||
import com.looker.droidify.data.local.model.CategoryAppRelation
|
||||
import com.looker.droidify.data.local.model.CategoryEntity
|
||||
import com.looker.droidify.data.local.model.CategoryRepoRelation
|
||||
import com.looker.droidify.data.local.model.DonateEntity
|
||||
import com.looker.droidify.data.local.model.GraphicEntity
|
||||
import com.looker.droidify.data.local.model.LinksEntity
|
||||
import com.looker.droidify.data.local.model.MirrorEntity
|
||||
import com.looker.droidify.data.local.model.RepoEntity
|
||||
import com.looker.droidify.data.local.model.ScreenshotEntity
|
||||
import com.looker.droidify.data.local.model.VersionEntity
|
||||
import com.looker.droidify.data.local.model.antiFeatureEntity
|
||||
import com.looker.droidify.data.local.model.appEntity
|
||||
import com.looker.droidify.data.local.model.authorEntity
|
||||
import com.looker.droidify.data.local.model.categoryEntity
|
||||
import com.looker.droidify.data.local.model.donateEntity
|
||||
import com.looker.droidify.data.local.model.linkEntity
|
||||
import com.looker.droidify.data.local.model.localizedGraphics
|
||||
import com.looker.droidify.data.local.model.localizedScreenshots
|
||||
import com.looker.droidify.data.local.model.mirrorEntity
|
||||
import com.looker.droidify.data.local.model.repoEntity
|
||||
import com.looker.droidify.data.local.model.versionEntities
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
|
||||
@Dao
|
||||
interface IndexDao {
|
||||
|
||||
@Transaction
|
||||
suspend fun insertIndex(
|
||||
fingerprint: Fingerprint,
|
||||
index: IndexV2,
|
||||
expectedRepoId: Int = 0,
|
||||
) {
|
||||
val repoId = upsertRepo(
|
||||
index.repo.repoEntity(
|
||||
id = expectedRepoId,
|
||||
fingerprint = fingerprint,
|
||||
),
|
||||
)
|
||||
val antiFeatures = index.repo.antiFeatures.flatMap { (tag, feature) ->
|
||||
feature.antiFeatureEntity(tag)
|
||||
}
|
||||
val categories = index.repo.categories.flatMap { (defaultName, category) ->
|
||||
category.categoryEntity(defaultName)
|
||||
}
|
||||
val antiFeatureRepoRelations = antiFeatures.map { AntiFeatureRepoRelation(repoId, it.tag) }
|
||||
val categoryRepoRelations = categories.map { CategoryRepoRelation(repoId, it.defaultName) }
|
||||
val mirrors = index.repo.mirrors.map { it.mirrorEntity(repoId) }
|
||||
insertAntiFeatures(antiFeatures)
|
||||
insertAntiFeatureRepoRelation(antiFeatureRepoRelations)
|
||||
insertCategories(categories)
|
||||
insertCategoryRepoRelation(categoryRepoRelations)
|
||||
insertMirror(mirrors)
|
||||
index.packages.forEach { (packageName, packages) ->
|
||||
val metadata = packages.metadata
|
||||
val author = metadata.authorEntity()
|
||||
val authorId = upsertAuthor(author)
|
||||
val appId = appIdByPackageName(repoId, packageName) ?: insertApp(
|
||||
appEntity = metadata.appEntity(
|
||||
packageName = packageName,
|
||||
repoId = repoId,
|
||||
authorId = authorId,
|
||||
),
|
||||
).toInt()
|
||||
val versions = packages.versionEntities(appId)
|
||||
insertVersions(versions.keys.toList())
|
||||
insertAntiFeatureAppRelation(versions.values.flatten())
|
||||
val appCategories = packages.metadata.categories.map { CategoryAppRelation(appId, it) }
|
||||
insertCategoryAppRelation(appCategories)
|
||||
metadata.linkEntity(appId)?.let { insertLink(it) }
|
||||
metadata.screenshots?.localizedScreenshots(appId)?.let { insertScreenshots(it) }
|
||||
metadata.localizedGraphics(appId)?.let { insertGraphics(it) }
|
||||
metadata.donateEntity(appId)?.let { insertDonate(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertRepo(repoEntity: RepoEntity): Long
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun updateRepo(repoEntity: RepoEntity): Int
|
||||
|
||||
@Transaction
|
||||
suspend fun upsertRepo(repoEntity: RepoEntity): Int {
|
||||
val id = insertRepo(repoEntity)
|
||||
return if (id == -1L) {
|
||||
updateRepo(repoEntity)
|
||||
} else {
|
||||
id.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertMirror(mirrors: List<MirrorEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAntiFeatures(antiFeatures: List<AntiFeatureEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertCategories(categories: List<CategoryEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertAntiFeatureRepoRelation(crossRef: List<AntiFeatureRepoRelation>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertCategoryRepoRelation(crossRef: List<CategoryRepoRelation>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertApp(appEntity: AppEntity): Long
|
||||
|
||||
@Query("SELECT id FROM app WHERE packageName = :packageName AND repoId = :repoId LIMIT 1")
|
||||
suspend fun appIdByPackageName(repoId: Int, packageName: String): Int?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertAuthor(authorEntity: AuthorEntity): Long
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT id FROM author
|
||||
WHERE
|
||||
(:email IS NULL AND email IS NULL OR email = :email) AND
|
||||
(:name IS NULL AND name IS NULL OR name = :name COLLATE NOCASE) AND
|
||||
(:website IS NULL AND website IS NULL OR website = :website COLLATE NOCASE)
|
||||
LIMIT 1
|
||||
""",
|
||||
)
|
||||
suspend fun authorId(
|
||||
email: String?,
|
||||
name: String?,
|
||||
website: String?,
|
||||
): Int?
|
||||
|
||||
@Transaction
|
||||
suspend fun upsertAuthor(authorEntity: AuthorEntity): Int {
|
||||
val id = insertAuthor(authorEntity)
|
||||
return if (id == -1L) {
|
||||
authorId(
|
||||
email = authorEntity.email,
|
||||
name = authorEntity.name,
|
||||
website = authorEntity.website,
|
||||
)!!.toInt()
|
||||
} else {
|
||||
id.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertScreenshots(screenshotEntity: List<ScreenshotEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertLink(linksEntity: LinksEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertGraphics(graphicEntity: List<GraphicEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertDonate(donateEntity: List<DonateEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertVersions(versions: List<VersionEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertCategoryAppRelation(crossRef: List<CategoryAppRelation>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertAntiFeatureAppRelation(crossRef: List<AntiFeatureAppRelation>)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.looker.droidify.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.looker.droidify.data.local.model.RepoEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RepoDao {
|
||||
|
||||
@Query("SELECT * FROM repository")
|
||||
fun stream(): Flow<List<RepoEntity>>
|
||||
|
||||
@Query("SELECT * FROM repository WHERE id = :repoId")
|
||||
fun repo(repoId: Int): Flow<RepoEntity>
|
||||
|
||||
@Query("DELETE FROM repository WHERE id = :id")
|
||||
suspend fun delete(id: Int)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import com.looker.droidify.sync.v2.model.AntiFeatureReason
|
||||
import com.looker.droidify.sync.v2.model.AntiFeatureV2
|
||||
import com.looker.droidify.sync.v2.model.Tag
|
||||
|
||||
@Entity(
|
||||
tableName = "anti_feature",
|
||||
primaryKeys = ["tag", "locale"],
|
||||
)
|
||||
data class AntiFeatureEntity(
|
||||
val icon: String?,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val locale: String,
|
||||
val tag: Tag,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "anti_feature_repo_relation",
|
||||
primaryKeys = ["id", "tag"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = RepoEntity::class,
|
||||
childColumns = ["id"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class AntiFeatureRepoRelation(
|
||||
@ColumnInfo("id")
|
||||
val repoId: Int,
|
||||
val tag: Tag,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "anti_features_app_relation",
|
||||
primaryKeys = ["tag", "appId", "versionCode"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = AppEntity::class,
|
||||
childColumns = ["appId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class AntiFeatureAppRelation(
|
||||
val tag: Tag,
|
||||
val reason: AntiFeatureReason,
|
||||
val appId: Int,
|
||||
val versionCode: Long,
|
||||
)
|
||||
|
||||
fun AntiFeatureV2.antiFeatureEntity(
|
||||
tag: Tag,
|
||||
): List<AntiFeatureEntity> {
|
||||
return name.map { (locale, localizedName) ->
|
||||
AntiFeatureEntity(
|
||||
icon = icon[locale]?.name,
|
||||
name = localizedName,
|
||||
description = description[locale],
|
||||
tag = tag,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.Index
|
||||
import androidx.room.Junction
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
import com.looker.droidify.sync.v2.model.LocalizedIcon
|
||||
import com.looker.droidify.sync.v2.model.LocalizedString
|
||||
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||
|
||||
@Entity(
|
||||
tableName = "app",
|
||||
indices = [
|
||||
Index("authorId"),
|
||||
Index("repoId"),
|
||||
Index("packageName"),
|
||||
Index("packageName", "repoId", unique = true),
|
||||
],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = RepoEntity::class,
|
||||
childColumns = ["repoId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
ForeignKey(
|
||||
entity = AuthorEntity::class,
|
||||
childColumns = ["authorId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class AppEntity(
|
||||
val added: Long,
|
||||
val lastUpdated: Long,
|
||||
val license: String?,
|
||||
val name: LocalizedString,
|
||||
val icon: LocalizedIcon?,
|
||||
val preferredSigner: String?,
|
||||
val summary: LocalizedString?,
|
||||
val description: LocalizedString?,
|
||||
val packageName: String,
|
||||
val authorId: Int,
|
||||
val repoId: Int,
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int = 0,
|
||||
)
|
||||
|
||||
data class AppEntityRelations(
|
||||
@Embedded val app: AppEntity,
|
||||
@Relation(
|
||||
parentColumn = "authorId",
|
||||
entityColumn = "id",
|
||||
)
|
||||
val author: AuthorEntity,
|
||||
@Relation(
|
||||
parentColumn = "id",
|
||||
entityColumn = "appId",
|
||||
)
|
||||
val links: LinksEntity?,
|
||||
@Relation(
|
||||
parentColumn = "id",
|
||||
entityColumn = "defaultName",
|
||||
associateBy = Junction(CategoryAppRelation::class),
|
||||
)
|
||||
val categories: List<CategoryEntity>,
|
||||
@Relation(
|
||||
parentColumn = "id",
|
||||
entityColumn = "appId",
|
||||
)
|
||||
val graphics: List<GraphicEntity>?,
|
||||
@Relation(
|
||||
parentColumn = "id",
|
||||
entityColumn = "appId",
|
||||
)
|
||||
val screenshots: List<ScreenshotEntity>?,
|
||||
@Relation(
|
||||
parentColumn = "id",
|
||||
entityColumn = "appId",
|
||||
)
|
||||
val versions: List<VersionEntity>?,
|
||||
@Relation(
|
||||
parentColumn = "packageName",
|
||||
entityColumn = "packageName",
|
||||
)
|
||||
val installed: InstalledEntity?,
|
||||
)
|
||||
|
||||
fun MetadataV2.appEntity(
|
||||
packageName: String,
|
||||
repoId: Int,
|
||||
authorId: Int,
|
||||
) = AppEntity(
|
||||
added = added,
|
||||
lastUpdated = lastUpdated,
|
||||
license = license,
|
||||
name = name,
|
||||
icon = icon,
|
||||
preferredSigner = preferredSigner,
|
||||
summary = summary,
|
||||
description = description,
|
||||
packageName = packageName,
|
||||
authorId = authorId,
|
||||
repoId = repoId,
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.PrimaryKey
|
||||
import com.looker.droidify.data.encryption.Encrypted
|
||||
|
||||
@Entity(
|
||||
tableName = "authentication",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = RepoEntity::class,
|
||||
childColumns = ["repoId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class AuthenticationEntity(
|
||||
val password: Encrypted,
|
||||
val username: String,
|
||||
val initializationVector: String,
|
||||
@PrimaryKey
|
||||
val repoId: Int,
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.looker.droidify.domain.model.Author
|
||||
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||
|
||||
@Entity(
|
||||
tableName = "author",
|
||||
indices = [Index("email", "name", "website", unique = true)],
|
||||
)
|
||||
data class AuthorEntity(
|
||||
val email: String?,
|
||||
val name: String?,
|
||||
val website: String?,
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int = 0,
|
||||
)
|
||||
|
||||
fun MetadataV2.authorEntity() = AuthorEntity(
|
||||
email = authorEmail,
|
||||
name = authorName,
|
||||
website = authorWebSite,
|
||||
)
|
||||
|
||||
fun AuthorEntity.toAuthor() = Author(
|
||||
email = email,
|
||||
name = name,
|
||||
phone = null,
|
||||
web = website,
|
||||
id = id,
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import com.looker.droidify.sync.v2.model.CategoryV2
|
||||
import com.looker.droidify.sync.v2.model.DefaultName
|
||||
|
||||
@Entity(
|
||||
tableName = "category",
|
||||
primaryKeys = ["defaultName", "locale"],
|
||||
indices = [Index("defaultName")]
|
||||
)
|
||||
data class CategoryEntity(
|
||||
val icon: String?,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val locale: String,
|
||||
val defaultName: DefaultName,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "category_repo_relation",
|
||||
primaryKeys = ["id", "defaultName"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = RepoEntity::class,
|
||||
childColumns = ["id"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class CategoryRepoRelation(
|
||||
@ColumnInfo("id")
|
||||
val repoId: Int,
|
||||
val defaultName: DefaultName,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "category_app_relation",
|
||||
primaryKeys = ["id", "defaultName"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = AppEntity::class,
|
||||
childColumns = ["id"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class CategoryAppRelation(
|
||||
@ColumnInfo("id")
|
||||
val appId: Int,
|
||||
val defaultName: DefaultName,
|
||||
)
|
||||
|
||||
fun CategoryV2.categoryEntity(
|
||||
defaultName: DefaultName,
|
||||
): List<CategoryEntity> {
|
||||
return name.map { (locale, localizedName) ->
|
||||
CategoryEntity(
|
||||
icon = icon[locale]?.name,
|
||||
name = localizedName,
|
||||
description = description[locale],
|
||||
defaultName = defaultName,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.Index
|
||||
import com.looker.droidify.domain.model.Donation
|
||||
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||
|
||||
@Entity(
|
||||
tableName = "donate",
|
||||
primaryKeys = ["type", "appId"],
|
||||
indices = [Index("appId")],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = AppEntity::class,
|
||||
childColumns = ["appId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class DonateEntity(
|
||||
@param:DonationType
|
||||
val type: Int,
|
||||
val value: String,
|
||||
val appId: Int,
|
||||
)
|
||||
|
||||
fun MetadataV2.donateEntity(appId: Int): List<DonateEntity>? {
|
||||
return buildList {
|
||||
if (bitcoin != null) {
|
||||
add(DonateEntity(BITCOIN_ADD, bitcoin, appId))
|
||||
}
|
||||
if (litecoin != null) {
|
||||
add(DonateEntity(LITECOIN_ADD, litecoin, appId))
|
||||
}
|
||||
if (liberapay != null) {
|
||||
add(DonateEntity(LIBERAPAY_ID, liberapay, appId))
|
||||
}
|
||||
if (openCollective != null) {
|
||||
add(DonateEntity(OPEN_COLLECTIVE_ID, openCollective, appId))
|
||||
}
|
||||
if (flattrID != null) {
|
||||
add(DonateEntity(FLATTR_ID, flattrID, appId))
|
||||
}
|
||||
if (!donate.isNullOrEmpty()) {
|
||||
add(DonateEntity(REGULAR, donate.joinToString(STRING_LIST_SEPARATOR), appId))
|
||||
}
|
||||
}.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun List<DonateEntity>.toDonation(): Donation {
|
||||
var bitcoinAddress: String? = null
|
||||
var litecoinAddress: String? = null
|
||||
var liberapayId: String? = null
|
||||
var openCollectiveId: String? = null
|
||||
var flattrId: String? = null
|
||||
var regular: List<String>? = null
|
||||
for (entity in this) {
|
||||
when (entity.type) {
|
||||
BITCOIN_ADD -> bitcoinAddress = entity.value
|
||||
FLATTR_ID -> flattrId = entity.value
|
||||
LIBERAPAY_ID -> liberapayId = entity.value
|
||||
LITECOIN_ADD -> litecoinAddress = entity.value
|
||||
OPEN_COLLECTIVE_ID -> openCollectiveId = entity.value
|
||||
REGULAR -> regular = entity.value.split(STRING_LIST_SEPARATOR)
|
||||
}
|
||||
}
|
||||
|
||||
return Donation(
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
litecoinAddress = litecoinAddress,
|
||||
liberapayId = liberapayId,
|
||||
openCollectiveId = openCollectiveId,
|
||||
flattrId = flattrId,
|
||||
regularUrl = regular,
|
||||
)
|
||||
}
|
||||
|
||||
private const val STRING_LIST_SEPARATOR = "&^%#@!"
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@IntDef(
|
||||
BITCOIN_ADD,
|
||||
LITECOIN_ADD,
|
||||
LIBERAPAY_ID,
|
||||
OPEN_COLLECTIVE_ID,
|
||||
FLATTR_ID,
|
||||
REGULAR,
|
||||
)
|
||||
private annotation class DonationType
|
||||
|
||||
private const val BITCOIN_ADD = 0
|
||||
private const val LITECOIN_ADD = 1
|
||||
private const val LIBERAPAY_ID = 2
|
||||
private const val OPEN_COLLECTIVE_ID = 3
|
||||
private const val FLATTR_ID = 4
|
||||
private const val REGULAR = 5
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.Index
|
||||
import com.looker.droidify.domain.model.Graphics
|
||||
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||
|
||||
@Entity(
|
||||
tableName = "graphic",
|
||||
primaryKeys = ["type", "locale", "appId"],
|
||||
indices = [Index("appId", "locale")],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = AppEntity::class,
|
||||
childColumns = ["appId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class GraphicEntity(
|
||||
@param:GraphicType
|
||||
val type: Int,
|
||||
val url: String,
|
||||
val locale: String,
|
||||
val appId: Int,
|
||||
)
|
||||
|
||||
fun MetadataV2.localizedGraphics(appId: Int): List<GraphicEntity>? {
|
||||
return buildList {
|
||||
promoGraphic?.forEach { (locale, value) ->
|
||||
add(GraphicEntity(PROMO_GRAPHIC, value.name, locale, appId))
|
||||
}
|
||||
featureGraphic?.forEach { (locale, value) ->
|
||||
add(GraphicEntity(FEATURE_GRAPHIC, value.name, locale, appId))
|
||||
}
|
||||
tvBanner?.forEach { (locale, value) ->
|
||||
add(GraphicEntity(TV_BANNER, value.name, locale, appId))
|
||||
}
|
||||
video?.forEach { (locale, value) ->
|
||||
add(GraphicEntity(VIDEO, value, locale, appId))
|
||||
}
|
||||
}.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun List<GraphicEntity>.toGraphics(): Graphics {
|
||||
var featureGraphic: String? = null
|
||||
var promoGraphic: String? = null
|
||||
var tvBanner: String? = null
|
||||
var video: String? = null
|
||||
|
||||
for (entity in this) {
|
||||
when (entity.type) {
|
||||
FEATURE_GRAPHIC -> featureGraphic = entity.url
|
||||
PROMO_GRAPHIC -> promoGraphic = entity.url
|
||||
TV_BANNER -> tvBanner = entity.url
|
||||
VIDEO -> video = entity.url
|
||||
}
|
||||
}
|
||||
return Graphics(
|
||||
featureGraphic = featureGraphic,
|
||||
promoGraphic = promoGraphic,
|
||||
tvBanner = tvBanner,
|
||||
video = video,
|
||||
)
|
||||
}
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@IntDef(
|
||||
VIDEO,
|
||||
TV_BANNER,
|
||||
PROMO_GRAPHIC,
|
||||
FEATURE_GRAPHIC,
|
||||
)
|
||||
annotation class GraphicType
|
||||
|
||||
private const val VIDEO = 0
|
||||
private const val TV_BANNER = 1
|
||||
private const val PROMO_GRAPHIC = 2
|
||||
private const val FEATURE_GRAPHIC = 3
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity("installed")
|
||||
data class InstalledEntity(
|
||||
val versionCode: String,
|
||||
val versionName: String,
|
||||
val signature: String,
|
||||
@PrimaryKey
|
||||
val packageName: String,
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.PrimaryKey
|
||||
import com.looker.droidify.domain.model.Links
|
||||
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||
|
||||
@Entity(
|
||||
tableName = "link",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = AppEntity::class,
|
||||
childColumns = ["appId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class LinksEntity(
|
||||
val changelog: String?,
|
||||
val issueTracker: String?,
|
||||
val translation: String?,
|
||||
val sourceCode: String?,
|
||||
val webSite: String?,
|
||||
@PrimaryKey
|
||||
val appId: Int,
|
||||
)
|
||||
|
||||
private fun MetadataV2.isLinkNull(): Boolean {
|
||||
return changelog == null &&
|
||||
issueTracker == null &&
|
||||
translation == null &&
|
||||
sourceCode == null &&
|
||||
webSite == null
|
||||
}
|
||||
|
||||
fun MetadataV2.linkEntity(appId: Int) = if (!isLinkNull()) {
|
||||
LinksEntity(
|
||||
appId = appId,
|
||||
changelog = changelog,
|
||||
issueTracker = issueTracker,
|
||||
translation = translation,
|
||||
sourceCode = sourceCode,
|
||||
webSite = webSite,
|
||||
)
|
||||
} else null
|
||||
|
||||
fun LinksEntity.toLinks() = Links(
|
||||
changelog = changelog,
|
||||
issueTracker = issueTracker,
|
||||
translation = translation,
|
||||
sourceCode = sourceCode,
|
||||
webSite = webSite,
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.looker.droidify.sync.v2.model.MirrorV2
|
||||
|
||||
@Entity(
|
||||
tableName = "mirror",
|
||||
indices = [Index("repoId")],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = RepoEntity::class,
|
||||
childColumns = ["repoId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
]
|
||||
)
|
||||
data class MirrorEntity(
|
||||
val url: String,
|
||||
val countryCode: String?,
|
||||
val isPrimary: Boolean,
|
||||
val repoId: Int,
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int = 0,
|
||||
)
|
||||
|
||||
fun MirrorV2.mirrorEntity(repoId: Int) = MirrorEntity(
|
||||
url = url,
|
||||
countryCode = countryCode,
|
||||
isPrimary = isPrimary == true,
|
||||
repoId = repoId,
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.looker.droidify.domain.model.Authentication
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.domain.model.VersionInfo
|
||||
import com.looker.droidify.sync.v2.model.LocalizedIcon
|
||||
import com.looker.droidify.sync.v2.model.LocalizedString
|
||||
import com.looker.droidify.sync.v2.model.RepoV2
|
||||
import com.looker.droidify.sync.v2.model.localizedValue
|
||||
|
||||
@Entity(tableName = "repository")
|
||||
data class RepoEntity(
|
||||
val icon: LocalizedIcon?,
|
||||
val address: String,
|
||||
val name: LocalizedString,
|
||||
val description: LocalizedString,
|
||||
val fingerprint: Fingerprint,
|
||||
val timestamp: Long,
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int = 0,
|
||||
)
|
||||
|
||||
fun RepoV2.repoEntity(
|
||||
id: Int,
|
||||
fingerprint: Fingerprint,
|
||||
) = RepoEntity(
|
||||
id = id,
|
||||
icon = icon,
|
||||
address = address,
|
||||
name = name,
|
||||
description = description,
|
||||
timestamp = timestamp,
|
||||
fingerprint = fingerprint,
|
||||
)
|
||||
|
||||
fun RepoEntity.toRepo(
|
||||
locale: String,
|
||||
mirrors: List<String>,
|
||||
enabled: Boolean,
|
||||
authentication: Authentication? = null,
|
||||
) = Repo(
|
||||
name = name.localizedValue(locale) ?: "Unknown",
|
||||
description = description.localizedValue(locale) ?: "Unknown",
|
||||
fingerprint = fingerprint,
|
||||
authentication = authentication,
|
||||
enabled = enabled,
|
||||
address = address,
|
||||
versionInfo = VersionInfo(timestamp = timestamp, etag = null),
|
||||
mirrors = mirrors,
|
||||
id = id,
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.Index
|
||||
import com.looker.droidify.domain.model.Screenshots
|
||||
import com.looker.droidify.sync.v2.model.LocalizedFiles
|
||||
import com.looker.droidify.sync.v2.model.ScreenshotsV2
|
||||
|
||||
@Entity(
|
||||
tableName = "screenshot",
|
||||
primaryKeys = ["path", "type", "locale", "appId"],
|
||||
indices = [Index("appId", "locale")],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = AppEntity::class,
|
||||
childColumns = ["appId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class ScreenshotEntity(
|
||||
val path: String,
|
||||
@param:ScreenshotType
|
||||
val type: Int,
|
||||
val locale: String,
|
||||
val appId: Int,
|
||||
)
|
||||
|
||||
fun ScreenshotsV2.localizedScreenshots(appId: Int): List<ScreenshotEntity> {
|
||||
if (isNull) return emptyList()
|
||||
val screenshots = mutableListOf<ScreenshotEntity>()
|
||||
|
||||
val screenshotIterator: (Int, LocalizedFiles?) -> Unit = { type, localizedFiles ->
|
||||
localizedFiles?.forEach { (locale, files) ->
|
||||
for ((path, _, _) in files) {
|
||||
screenshots.add(
|
||||
ScreenshotEntity(
|
||||
locale = locale,
|
||||
appId = appId,
|
||||
type = type,
|
||||
path = path,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
screenshotIterator(PHONE, phone)
|
||||
screenshotIterator(SEVEN_INCH, sevenInch)
|
||||
screenshotIterator(TEN_INCH, tenInch)
|
||||
screenshotIterator(WEAR, wear)
|
||||
screenshotIterator(TV, tv)
|
||||
return screenshots
|
||||
}
|
||||
|
||||
fun List<ScreenshotEntity>.toScreenshots(): Screenshots {
|
||||
val phone = mutableListOf<String>()
|
||||
val sevenInch = mutableListOf<String>()
|
||||
val tenInch = mutableListOf<String>()
|
||||
val wear = mutableListOf<String>()
|
||||
val tv = mutableListOf<String>()
|
||||
for (index in this.indices) {
|
||||
val entity = get(index)
|
||||
when (entity.type) {
|
||||
PHONE -> phone.add(entity.path)
|
||||
SEVEN_INCH -> sevenInch.add(entity.path)
|
||||
TEN_INCH -> tenInch.add(entity.path)
|
||||
TV -> tv.add(entity.path)
|
||||
WEAR -> wear.add(entity.path)
|
||||
}
|
||||
}
|
||||
|
||||
return Screenshots(
|
||||
phone = phone,
|
||||
sevenInch = sevenInch,
|
||||
tenInch = tenInch,
|
||||
wear = wear,
|
||||
tv = tv,
|
||||
)
|
||||
}
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@IntDef(
|
||||
PHONE,
|
||||
SEVEN_INCH,
|
||||
TEN_INCH,
|
||||
WEAR,
|
||||
TV,
|
||||
)
|
||||
private annotation class ScreenshotType
|
||||
|
||||
private const val PHONE = 0
|
||||
private const val SEVEN_INCH = 1
|
||||
private const val TEN_INCH = 2
|
||||
private const val WEAR = 3
|
||||
private const val TV = 4
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.looker.droidify.data.local.model
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.looker.droidify.sync.v2.model.ApkFileV2
|
||||
import com.looker.droidify.sync.v2.model.FileV2
|
||||
import com.looker.droidify.sync.v2.model.LocalizedString
|
||||
import com.looker.droidify.sync.v2.model.PackageV2
|
||||
import com.looker.droidify.sync.v2.model.PermissionV2
|
||||
|
||||
@Entity(
|
||||
tableName = "version",
|
||||
indices = [Index("appId")],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = AppEntity::class,
|
||||
childColumns = ["appId"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class VersionEntity(
|
||||
val added: Long,
|
||||
val whatsNew: LocalizedString,
|
||||
val versionName: String,
|
||||
val versionCode: Long,
|
||||
val maxSdkVersion: Int?,
|
||||
val minSdkVersion: Int,
|
||||
val targetSdkVersion: Int,
|
||||
@Embedded("apk_")
|
||||
val apk: ApkFileV2,
|
||||
@Embedded("src_")
|
||||
val src: FileV2?,
|
||||
val features: List<String>,
|
||||
val nativeCode: List<String>,
|
||||
val permissions: List<PermissionV2>,
|
||||
val permissionsSdk23: List<PermissionV2>,
|
||||
val appId: Int,
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int = 0,
|
||||
)
|
||||
|
||||
fun PackageV2.versionEntities(appId: Int): Map<VersionEntity, List<AntiFeatureAppRelation>> {
|
||||
return versions.map { (_, version) ->
|
||||
VersionEntity(
|
||||
added = version.added,
|
||||
whatsNew = version.whatsNew,
|
||||
versionName = version.manifest.versionName,
|
||||
versionCode = version.manifest.versionCode,
|
||||
maxSdkVersion = version.manifest.maxSdkVersion,
|
||||
minSdkVersion = version.manifest.usesSdk?.minSdkVersion ?: -1,
|
||||
targetSdkVersion = version.manifest.usesSdk?.targetSdkVersion ?: -1,
|
||||
apk = version.file,
|
||||
src = version.src,
|
||||
features = version.manifest.features.map { it.name },
|
||||
nativeCode = version.manifest.nativecode,
|
||||
permissions = version.manifest.usesPermission,
|
||||
permissionsSdk23 = version.manifest.usesPermissionSdk23,
|
||||
appId = appId,
|
||||
) to version.antiFeatures.map { (tag, reason) ->
|
||||
AntiFeatureAppRelation(
|
||||
tag = tag,
|
||||
reason = reason,
|
||||
appId = appId,
|
||||
versionCode = version.manifest.versionCode,
|
||||
)
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
@@ -5,44 +5,45 @@ import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.model.ProductItem
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
sealed class Request {
|
||||
internal abstract val id: Int
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
sealed interface Request {
|
||||
val id: Int
|
||||
|
||||
class Available(
|
||||
val searchQuery: String,
|
||||
val section: ProductItem.Section,
|
||||
val order: SortOrder,
|
||||
) : Request() {
|
||||
override val id: Int
|
||||
get() = 1
|
||||
}
|
||||
override val id: Int = 1,
|
||||
) : Request
|
||||
|
||||
class Installed(
|
||||
val searchQuery: String,
|
||||
val section: ProductItem.Section,
|
||||
val order: SortOrder,
|
||||
) : Request() {
|
||||
override val id: Int
|
||||
get() = 2
|
||||
}
|
||||
override val id: Int = 2,
|
||||
) : Request
|
||||
|
||||
class Updates(
|
||||
val searchQuery: String,
|
||||
val section: ProductItem.Section,
|
||||
val order: SortOrder,
|
||||
val skipSignatureCheck: Boolean,
|
||||
) : Request() {
|
||||
override val id: Int
|
||||
get() = 3
|
||||
}
|
||||
override val id: Int = 3,
|
||||
) : Request
|
||||
|
||||
object Repositories : Request() {
|
||||
override val id: Int
|
||||
get() = 4
|
||||
object Repositories : Request {
|
||||
override val id = 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +57,6 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
val cursor: Cursor?,
|
||||
)
|
||||
|
||||
init {
|
||||
retainInstance = true
|
||||
}
|
||||
|
||||
private val activeRequests = mutableMapOf<Int, ActiveRequest>()
|
||||
|
||||
fun attach(callback: Callback, request: Request) {
|
||||
@@ -93,6 +90,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
|
||||
val request = activeRequests[id]!!.request
|
||||
return QueryLoader(requireContext()) {
|
||||
val settings = runBlocking { settingsRepository.getInitial() }
|
||||
when (request) {
|
||||
is Request.Available ->
|
||||
Database.ProductAdapter
|
||||
@@ -103,6 +101,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it,
|
||||
skipSignatureCheck = settings.ignoreSignature,
|
||||
)
|
||||
|
||||
is Request.Installed ->
|
||||
@@ -114,6 +113,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it,
|
||||
skipSignatureCheck = settings.ignoreSignature,
|
||||
)
|
||||
|
||||
is Request.Updates ->
|
||||
@@ -125,7 +125,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it,
|
||||
skipSignatureCheck = request.skipSignatureCheck,
|
||||
skipSignatureCheck = settings.ignoreSignature,
|
||||
)
|
||||
|
||||
is Request.Repositories -> Database.RepositoryAdapter.query(it)
|
||||
|
||||
@@ -9,7 +9,8 @@ import android.os.CancellationSignal
|
||||
import androidx.core.database.sqlite.transaction
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.database.table.DatabaseHelper
|
||||
import com.looker.droidify.database.table.Table
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.model.InstalledItem
|
||||
import com.looker.droidify.model.Product
|
||||
@@ -20,7 +21,6 @@ import com.looker.droidify.utility.common.extension.asSequence
|
||||
import com.looker.droidify.utility.common.extension.firstOrNull
|
||||
import com.looker.droidify.utility.common.extension.parseDictionary
|
||||
import com.looker.droidify.utility.common.extension.writeDictionary
|
||||
import com.looker.droidify.utility.common.log
|
||||
import com.looker.droidify.utility.serialization.product
|
||||
import com.looker.droidify.utility.serialization.productItem
|
||||
import com.looker.droidify.utility.serialization.repository
|
||||
@@ -44,52 +44,15 @@ import kotlin.collections.set
|
||||
|
||||
object Database {
|
||||
fun init(context: Context): Boolean {
|
||||
val helper = Helper(context)
|
||||
val helper = DatabaseHelper(context)
|
||||
db = helper.writableDatabase
|
||||
if (helper.created) {
|
||||
for (repository in Repository.defaultRepositories.sortedBy { it.name }) {
|
||||
RepositoryAdapter.put(repository)
|
||||
}
|
||||
}
|
||||
RepositoryAdapter.removeDuplicates()
|
||||
return helper.created || helper.updated
|
||||
}
|
||||
|
||||
private lateinit var db: SQLiteDatabase
|
||||
|
||||
private interface Table {
|
||||
val memory: Boolean
|
||||
val innerName: String
|
||||
val createTable: String
|
||||
val createIndex: String?
|
||||
get() = null
|
||||
|
||||
val databasePrefix: String
|
||||
get() = if (memory) "memory." else ""
|
||||
|
||||
val name: String
|
||||
get() = "$databasePrefix$innerName"
|
||||
|
||||
fun formatCreateTable(name: String): String {
|
||||
return buildString(128) {
|
||||
append("CREATE TABLE ")
|
||||
append(name)
|
||||
append(" (")
|
||||
trimAndJoin(createTable)
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
|
||||
val createIndexPairFormatted: Pair<String, String>?
|
||||
get() = createIndex?.let {
|
||||
Pair(
|
||||
"CREATE INDEX ${innerName}_index ON $innerName ($it)",
|
||||
"CREATE INDEX ${name}_index ON $innerName ($it)",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object Schema {
|
||||
object Schema {
|
||||
object Repository : Table {
|
||||
const val ROW_ID = "_id"
|
||||
const val ROW_ENABLED = "enabled"
|
||||
@@ -190,126 +153,6 @@ object Database {
|
||||
}
|
||||
}
|
||||
|
||||
private class Helper(context: Context) : SQLiteOpenHelper(context, "droidify", null, 5) {
|
||||
var created = false
|
||||
private set
|
||||
var updated = false
|
||||
private set
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) = Unit
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) =
|
||||
onVersionChange(db)
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) =
|
||||
onVersionChange(db)
|
||||
|
||||
private fun onVersionChange(db: SQLiteDatabase) {
|
||||
handleTables(db, true, Schema.Product, Schema.Category)
|
||||
addRepos(db, Repository.newlyAdded)
|
||||
this.updated = true
|
||||
}
|
||||
|
||||
override fun onOpen(db: SQLiteDatabase) {
|
||||
val create = handleTables(db, false, Schema.Repository)
|
||||
val updated = handleTables(db, create, Schema.Product, Schema.Category)
|
||||
db.execSQL("ATTACH DATABASE ':memory:' AS memory")
|
||||
handleTables(db, false, Schema.Installed, Schema.Lock)
|
||||
handleIndexes(
|
||||
db,
|
||||
Schema.Repository,
|
||||
Schema.Product,
|
||||
Schema.Category,
|
||||
Schema.Installed,
|
||||
Schema.Lock,
|
||||
)
|
||||
dropOldTables(db, Schema.Repository, Schema.Product, Schema.Category)
|
||||
this.created = this.created || create
|
||||
this.updated = this.updated || create || updated
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTables(db: SQLiteDatabase, recreate: Boolean, vararg tables: Table): Boolean {
|
||||
val shouldRecreate = recreate || tables.any { table ->
|
||||
val sql = db.query(
|
||||
"${table.databasePrefix}sqlite_master",
|
||||
columns = arrayOf("sql"),
|
||||
selection = Pair("type = ? AND name = ?", arrayOf("table", table.innerName)),
|
||||
).use { it.firstOrNull()?.getString(0) }.orEmpty()
|
||||
table.formatCreateTable(table.innerName) != sql
|
||||
}
|
||||
return shouldRecreate && run {
|
||||
val shouldVacuum = tables.map {
|
||||
db.execSQL("DROP TABLE IF EXISTS ${it.name}")
|
||||
db.execSQL(it.formatCreateTable(it.name))
|
||||
!it.memory
|
||||
}
|
||||
if (shouldVacuum.any { it } && !db.inTransaction()) {
|
||||
db.execSQL("VACUUM")
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun addRepos(db: SQLiteDatabase, repos: List<Repository>) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
log("Add Repos: $repos", "RepositoryAdapter")
|
||||
}
|
||||
if (repos.isEmpty()) return
|
||||
db.transaction {
|
||||
repos.forEach {
|
||||
RepositoryAdapter.put(it, database = this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIndexes(db: SQLiteDatabase, vararg tables: Table) {
|
||||
val shouldVacuum = tables.map { table ->
|
||||
val sqls = db.query(
|
||||
"${table.databasePrefix}sqlite_master",
|
||||
columns = arrayOf("name", "sql"),
|
||||
selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", table.innerName)),
|
||||
)
|
||||
.use { cursor ->
|
||||
cursor.asSequence()
|
||||
.mapNotNull { it.getString(1)?.let { sql -> Pair(it.getString(0), sql) } }
|
||||
.toList()
|
||||
}
|
||||
.filter { !it.first.startsWith("sqlite_") }
|
||||
val createIndexes = table.createIndexPairFormatted?.let { listOf(it) }.orEmpty()
|
||||
createIndexes.map { it.first } != sqls.map { it.second } && run {
|
||||
for (name in sqls.map { it.first }) {
|
||||
db.execSQL("DROP INDEX IF EXISTS $name")
|
||||
}
|
||||
for (createIndexPair in createIndexes) {
|
||||
db.execSQL(createIndexPair.second)
|
||||
}
|
||||
!table.memory
|
||||
}
|
||||
}
|
||||
if (shouldVacuum.any { it } && !db.inTransaction()) {
|
||||
db.execSQL("VACUUM")
|
||||
}
|
||||
}
|
||||
|
||||
private fun dropOldTables(db: SQLiteDatabase, vararg neededTables: Table) {
|
||||
val tables = db.query(
|
||||
"sqlite_master",
|
||||
columns = arrayOf("name"),
|
||||
selection = Pair("type = ?", arrayOf("table")),
|
||||
)
|
||||
.use { cursor -> cursor.asSequence().mapNotNull { it.getString(0) }.toList() }
|
||||
.filter { !it.startsWith("sqlite_") && !it.startsWith("android_") }
|
||||
.toSet() - neededTables.mapNotNull { if (it.memory) null else it.name }.toSet()
|
||||
if (tables.isNotEmpty()) {
|
||||
for (table in tables) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $table")
|
||||
}
|
||||
if (!db.inTransaction()) {
|
||||
db.execSQL("VACUUM")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Subject {
|
||||
data object Repositories : Subject()
|
||||
data class Repository(val id: Long) : Subject()
|
||||
@@ -364,7 +207,7 @@ object Database {
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.query(
|
||||
fun SQLiteDatabase.query(
|
||||
table: String,
|
||||
columns: Array<String>? = null,
|
||||
selection: Pair<String, Array<String>>? = null,
|
||||
@@ -607,6 +450,19 @@ object Database {
|
||||
.map { getUpdates(skipSignatureCheck) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
fun getAll(): List<Product> {
|
||||
return db.query(
|
||||
Schema.Product.name,
|
||||
columns = arrayOf(
|
||||
Schema.Product.ROW_REPOSITORY_ID,
|
||||
Schema.Product.ROW_DESCRIPTION,
|
||||
Schema.Product.ROW_DATA,
|
||||
),
|
||||
selection = null,
|
||||
signal = null,
|
||||
).use { it.asSequence().map(::transform).toList() }
|
||||
}
|
||||
|
||||
fun get(packageName: String, signal: CancellationSignal?): List<Product> {
|
||||
return db.query(
|
||||
Schema.Product.name,
|
||||
@@ -719,7 +575,7 @@ object Database {
|
||||
when (order) {
|
||||
SortOrder.UPDATED -> builder += "product.${Schema.Product.ROW_UPDATED} DESC,"
|
||||
SortOrder.ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC,"
|
||||
SortOrder.NAME -> Unit
|
||||
else -> Unit
|
||||
}::class
|
||||
builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC"
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package com.looker.droidify.database.table
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import androidx.core.database.sqlite.transaction
|
||||
import com.looker.droidify.database.Database.RepositoryAdapter
|
||||
import com.looker.droidify.database.Database.Schema
|
||||
import com.looker.droidify.database.Database.jsonParse
|
||||
import com.looker.droidify.database.Database.query
|
||||
import com.looker.droidify.index.OemRepositoryParser
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.utility.common.extension.asSequence
|
||||
import com.looker.droidify.utility.common.extension.firstOrNull
|
||||
import com.looker.droidify.utility.serialization.repository
|
||||
|
||||
private const val DB_LEGACY_NAME = "droidify"
|
||||
|
||||
private const val DB_LEGACY_VERSION = 6
|
||||
|
||||
class DatabaseHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, DB_LEGACY_NAME, null, DB_LEGACY_VERSION) {
|
||||
var created = false
|
||||
private set
|
||||
var updated = false
|
||||
private set
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
// Create all tables
|
||||
db.execSQL(Schema.Repository.formatCreateTable(Schema.Repository.name))
|
||||
db.execSQL(Schema.Product.formatCreateTable(Schema.Product.name))
|
||||
db.execSQL(Schema.Category.formatCreateTable(Schema.Category.name))
|
||||
|
||||
// Add default repositories for new database
|
||||
db.addDefaultRepositories()
|
||||
db.addOemRepositories()
|
||||
this.created = true
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.removeRepositories()
|
||||
db.addNewlyAddedRepositories()
|
||||
db.addOemRepositories()
|
||||
this.updated = true
|
||||
}
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
// Handle database downgrades if needed
|
||||
onUpgrade(db, oldVersion, newVersion)
|
||||
}
|
||||
|
||||
override fun onOpen(db: SQLiteDatabase) {
|
||||
// Handle memory tables and indexes
|
||||
db.execSQL("ATTACH DATABASE ':memory:' AS memory")
|
||||
handleTables(db, Schema.Installed, Schema.Lock)
|
||||
handleIndexes(
|
||||
db,
|
||||
Schema.Repository,
|
||||
Schema.Product,
|
||||
Schema.Category,
|
||||
Schema.Installed,
|
||||
Schema.Lock,
|
||||
)
|
||||
dropOldTables(db, Schema.Repository, Schema.Product, Schema.Category)
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addOemRepositories() {
|
||||
transaction {
|
||||
OemRepositoryParser
|
||||
.getSystemDefaultRepos()
|
||||
?.forEach { repo -> RepositoryAdapter.put(repo, database = this) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addDefaultRepositories() {
|
||||
// Add all default repositories for new database
|
||||
transaction {
|
||||
(Repository.defaultRepositories + Repository.newlyAdded)
|
||||
.sortedBy { it.name }
|
||||
.forEach { repo -> RepositoryAdapter.put(repo, database = this) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addNewlyAddedRepositories() {
|
||||
// Add only newly added repositories, checking for existing ones
|
||||
val existingRepos = query(
|
||||
Schema.Repository.name,
|
||||
columns = arrayOf(Schema.Repository.ROW_DATA),
|
||||
selection = null,
|
||||
signal = null,
|
||||
).use { cursor ->
|
||||
cursor.asSequence().mapNotNull {
|
||||
val dataIndex = it.getColumnIndexOrThrow(Schema.Repository.ROW_DATA)
|
||||
val data = it.getBlob(dataIndex)
|
||||
|
||||
try {
|
||||
data.jsonParse { json -> json.repository() }.address
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
// Only add repositories that don't already exist
|
||||
val reposToAdd = Repository.newlyAdded.filter { repo ->
|
||||
repo.address !in existingRepos
|
||||
}
|
||||
|
||||
if (reposToAdd.isNotEmpty()) {
|
||||
transaction {
|
||||
reposToAdd.forEach { repo ->
|
||||
RepositoryAdapter.put(repo, database = this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.removeRepositories() {
|
||||
// Remove repositories that are in the toRemove list
|
||||
val reposToRemove = Repository.toRemove
|
||||
if (reposToRemove.isEmpty()) return
|
||||
|
||||
// Get all repositories with their IDs and addresses
|
||||
val existingRepos = query(
|
||||
Schema.Repository.name,
|
||||
columns = arrayOf(Schema.Repository.ROW_ID, Schema.Repository.ROW_DATA),
|
||||
selection = null,
|
||||
signal = null,
|
||||
).use { cursor ->
|
||||
cursor.asSequence().mapNotNull {
|
||||
val idIndex = it.getColumnIndexOrThrow(Schema.Repository.ROW_ID)
|
||||
val dataIndex = it.getColumnIndexOrThrow(Schema.Repository.ROW_DATA)
|
||||
val id = it.getLong(idIndex)
|
||||
val data = it.getBlob(dataIndex)
|
||||
|
||||
try {
|
||||
val repo = data.jsonParse { json -> json.repository() }
|
||||
id to repo.address
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
// Find repositories to remove
|
||||
val reposToRemoveIds = existingRepos.filter { (_, address) ->
|
||||
address in reposToRemove
|
||||
}.keys
|
||||
|
||||
if (reposToRemoveIds.isNotEmpty()) {
|
||||
transaction {
|
||||
reposToRemoveIds.forEach { repoId ->
|
||||
// Directly update the database to mark repository as deleted
|
||||
update(
|
||||
Schema.Repository.name,
|
||||
android.content.ContentValues().apply {
|
||||
put(Schema.Repository.ROW_DELETED, 1)
|
||||
},
|
||||
"${Schema.Repository.ROW_ID} = ?",
|
||||
arrayOf(repoId.toString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTables(db: SQLiteDatabase, vararg tables: Table): Boolean {
|
||||
val shouldRecreate = tables.any { table ->
|
||||
val sql = db.query(
|
||||
"${table.databasePrefix}sqlite_master",
|
||||
columns = arrayOf("sql"),
|
||||
selection = Pair("type = ? AND name = ?", arrayOf("table", table.innerName)),
|
||||
).use { it.firstOrNull()?.getString(0) }.orEmpty()
|
||||
table.formatCreateTable(table.innerName) != sql
|
||||
}
|
||||
return shouldRecreate && run {
|
||||
val shouldVacuum = tables.map {
|
||||
db.execSQL("DROP TABLE IF EXISTS ${it.name}")
|
||||
db.execSQL(it.formatCreateTable(it.name))
|
||||
!it.memory
|
||||
}
|
||||
if (shouldVacuum.any { it } && !db.inTransaction()) {
|
||||
db.execSQL("VACUUM")
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIndexes(db: SQLiteDatabase, vararg tables: Table) {
|
||||
val shouldVacuum = tables.map { table ->
|
||||
val sqls = db.query(
|
||||
"${table.databasePrefix}sqlite_master",
|
||||
columns = arrayOf("name", "sql"),
|
||||
selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", table.innerName)),
|
||||
)
|
||||
.use { cursor ->
|
||||
cursor.asSequence()
|
||||
.mapNotNull { it.getString(1)?.let { sql -> Pair(it.getString(0), sql) } }
|
||||
.toList()
|
||||
}
|
||||
.filter { !it.first.startsWith("sqlite_") }
|
||||
val createIndexes = table.createIndexPairFormatted?.let { listOf(it) }.orEmpty()
|
||||
createIndexes.map { it.first } != sqls.map { it.second } && run {
|
||||
for (name in sqls.map { it.first }) {
|
||||
db.execSQL("DROP INDEX IF EXISTS $name")
|
||||
}
|
||||
for (createIndexPair in createIndexes) {
|
||||
db.execSQL(createIndexPair.second)
|
||||
}
|
||||
!table.memory
|
||||
}
|
||||
}
|
||||
if (shouldVacuum.any { it } && !db.inTransaction()) {
|
||||
db.execSQL("VACUUM")
|
||||
}
|
||||
}
|
||||
|
||||
private fun dropOldTables(db: SQLiteDatabase, vararg neededTables: Table) {
|
||||
val tables = db.query(
|
||||
"sqlite_master",
|
||||
columns = arrayOf("name"),
|
||||
selection = Pair("type = ?", arrayOf("table")),
|
||||
)
|
||||
.use { cursor -> cursor.asSequence().mapNotNull { it.getString(0) }.toList() }
|
||||
.filter { !it.startsWith("sqlite_") && !it.startsWith("android_") }
|
||||
.toSet() - neededTables.mapNotNull { if (it.memory) null else it.name }.toSet()
|
||||
if (tables.isNotEmpty()) {
|
||||
for (table in tables) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $table")
|
||||
}
|
||||
if (!db.inTransaction()) {
|
||||
db.execSQL("VACUUM")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.looker.droidify.database.table
|
||||
|
||||
import com.looker.droidify.database.trimAndJoin
|
||||
|
||||
interface Table {
|
||||
val memory: Boolean
|
||||
val innerName: String
|
||||
val createTable: String
|
||||
val createIndex: String?
|
||||
get() = null
|
||||
|
||||
val databasePrefix: String
|
||||
get() = if (memory) "memory." else ""
|
||||
|
||||
val name: String
|
||||
get() = "$databasePrefix$innerName"
|
||||
|
||||
fun formatCreateTable(name: String): String {
|
||||
return buildString(128) {
|
||||
append("CREATE TABLE ")
|
||||
append(name)
|
||||
append(" (")
|
||||
trimAndJoin(createTable)
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
|
||||
val createIndexPairFormatted: Pair<String, String>?
|
||||
get() = createIndex?.let {
|
||||
Pair(
|
||||
"CREATE INDEX ${innerName}_index ON $innerName ($it)",
|
||||
"CREATE INDEX ${name}_index ON $innerName ($it)",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import com.looker.droidify.datastore.model.AutoSync
|
||||
import com.looker.droidify.datastore.model.InstallerType
|
||||
import com.looker.droidify.datastore.model.LegacyInstallerComponent
|
||||
import com.looker.droidify.datastore.model.ProxyPreference
|
||||
import com.looker.droidify.datastore.model.ProxyType
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
@@ -24,11 +25,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.Instant
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
class PreferenceSettingsRepository(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
private val exporter: Exporter<Settings>,
|
||||
@@ -36,7 +39,7 @@ class PreferenceSettingsRepository(
|
||||
override val data: Flow<Settings> = dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
Log.e("TAG", "Error reading preferences.", exception)
|
||||
Log.e("PreferencesSettingsRepository", "Error reading preferences.", exception)
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
@@ -85,6 +88,31 @@ class PreferenceSettingsRepository(
|
||||
override suspend fun setInstallerType(installerType: InstallerType) =
|
||||
INSTALLER_TYPE.update(installerType.name)
|
||||
|
||||
override suspend fun setLegacyInstallerComponent(component: LegacyInstallerComponent?) {
|
||||
when (component) {
|
||||
null -> {
|
||||
LEGACY_INSTALLER_COMPONENT_TYPE.update("")
|
||||
LEGACY_INSTALLER_COMPONENT_CLASS.update("")
|
||||
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("")
|
||||
}
|
||||
is LegacyInstallerComponent.Component -> {
|
||||
LEGACY_INSTALLER_COMPONENT_TYPE.update("component")
|
||||
LEGACY_INSTALLER_COMPONENT_CLASS.update(component.clazz)
|
||||
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update(component.activity)
|
||||
}
|
||||
LegacyInstallerComponent.Unspecified -> {
|
||||
LEGACY_INSTALLER_COMPONENT_TYPE.update("unspecified")
|
||||
LEGACY_INSTALLER_COMPONENT_CLASS.update("")
|
||||
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("")
|
||||
}
|
||||
LegacyInstallerComponent.AlwaysChoose -> {
|
||||
LEGACY_INSTALLER_COMPONENT_TYPE.update("always_choose")
|
||||
LEGACY_INSTALLER_COMPONENT_CLASS.update("")
|
||||
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setAutoUpdate(allow: Boolean) =
|
||||
AUTO_UPDATE.update(allow)
|
||||
|
||||
@@ -125,6 +153,18 @@ class PreferenceSettingsRepository(
|
||||
private fun mapSettings(preferences: Preferences): Settings {
|
||||
val installerType =
|
||||
InstallerType.valueOf(preferences[INSTALLER_TYPE] ?: InstallerType.Default.name)
|
||||
val legacyInstallerComponent = when (preferences[LEGACY_INSTALLER_COMPONENT_TYPE]) {
|
||||
"component" -> {
|
||||
preferences[LEGACY_INSTALLER_COMPONENT_CLASS]?.takeIf { it.isNotBlank() }?.let { cls ->
|
||||
preferences[LEGACY_INSTALLER_COMPONENT_ACTIVITY]?.takeIf { it.isNotBlank() }?.let { act ->
|
||||
LegacyInstallerComponent.Component(cls, act)
|
||||
}
|
||||
}
|
||||
}
|
||||
"unspecified" -> LegacyInstallerComponent.Unspecified
|
||||
"always_choose" -> LegacyInstallerComponent.AlwaysChoose
|
||||
else -> null
|
||||
}
|
||||
|
||||
val language = preferences[LANGUAGE] ?: "system"
|
||||
val incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false
|
||||
@@ -154,6 +194,7 @@ class PreferenceSettingsRepository(
|
||||
theme = theme,
|
||||
dynamicTheme = dynamicTheme,
|
||||
installerType = installerType,
|
||||
legacyInstallerComponent = legacyInstallerComponent,
|
||||
autoUpdate = autoUpdate,
|
||||
autoSync = autoSync,
|
||||
sortOrder = sortOrder,
|
||||
@@ -185,6 +226,9 @@ class PreferenceSettingsRepository(
|
||||
val LAST_CLEAN_UP = longPreferencesKey("key_last_clean_up_time")
|
||||
val FAVOURITE_APPS = stringSetPreferencesKey("key_favourite_apps")
|
||||
val HOME_SCREEN_SWIPING = booleanPreferencesKey("key_home_swiping")
|
||||
val LEGACY_INSTALLER_COMPONENT_CLASS = stringPreferencesKey("key_legacy_installer_component_class")
|
||||
val LEGACY_INSTALLER_COMPONENT_ACTIVITY = stringPreferencesKey("key_legacy_installer_component_activity")
|
||||
val LEGACY_INSTALLER_COMPONENT_TYPE = stringPreferencesKey("key_legacy_installer_component_type")
|
||||
|
||||
// Enums
|
||||
val THEME = stringPreferencesKey("key_theme")
|
||||
@@ -200,6 +244,28 @@ class PreferenceSettingsRepository(
|
||||
set(UNSTABLE_UPDATES, settings.unstableUpdate)
|
||||
set(THEME, settings.theme.name)
|
||||
set(DYNAMIC_THEME, settings.dynamicTheme)
|
||||
when (settings.legacyInstallerComponent) {
|
||||
is LegacyInstallerComponent.Component -> {
|
||||
set(LEGACY_INSTALLER_COMPONENT_TYPE, "component")
|
||||
set(LEGACY_INSTALLER_COMPONENT_CLASS, settings.legacyInstallerComponent.clazz)
|
||||
set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, settings.legacyInstallerComponent.activity)
|
||||
}
|
||||
LegacyInstallerComponent.Unspecified -> {
|
||||
set(LEGACY_INSTALLER_COMPONENT_TYPE, "unspecified")
|
||||
set(LEGACY_INSTALLER_COMPONENT_CLASS, "")
|
||||
set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "")
|
||||
}
|
||||
LegacyInstallerComponent.AlwaysChoose -> {
|
||||
set(LEGACY_INSTALLER_COMPONENT_TYPE, "always_choose")
|
||||
set(LEGACY_INSTALLER_COMPONENT_CLASS, "")
|
||||
set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "")
|
||||
}
|
||||
null -> {
|
||||
set(LEGACY_INSTALLER_COMPONENT_TYPE, "")
|
||||
set(LEGACY_INSTALLER_COMPONENT_CLASS, "")
|
||||
set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "")
|
||||
}
|
||||
}
|
||||
set(INSTALLER_TYPE, settings.installerType.name)
|
||||
set(AUTO_UPDATE, settings.autoUpdate)
|
||||
set(AUTO_SYNC, settings.autoSync.name)
|
||||
|
||||
@@ -3,23 +3,26 @@ package com.looker.droidify.datastore
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.looker.droidify.datastore.model.AutoSync
|
||||
import com.looker.droidify.datastore.model.InstallerType
|
||||
import com.looker.droidify.datastore.model.LegacyInstallerComponent
|
||||
import com.looker.droidify.datastore.model.ProxyPreference
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.datastore.model.Theme
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Serializable
|
||||
@OptIn(ExperimentalTime::class)
|
||||
data class Settings(
|
||||
val language: String = "system",
|
||||
val incompatibleVersions: Boolean = false,
|
||||
@@ -29,6 +32,7 @@ data class Settings(
|
||||
val theme: Theme = Theme.SYSTEM,
|
||||
val dynamicTheme: Boolean = false,
|
||||
val installerType: InstallerType = InstallerType.Default,
|
||||
val legacyInstallerComponent: LegacyInstallerComponent? = null,
|
||||
val autoUpdate: Boolean = false,
|
||||
val autoSync: AutoSync = AutoSync.WIFI_ONLY,
|
||||
val sortOrder: SortOrder = SortOrder.UPDATED,
|
||||
@@ -44,6 +48,7 @@ object SettingsSerializer : Serializer<Settings> {
|
||||
|
||||
private val json = Json { encodeDefaults = true }
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override val defaultValue: Settings = Settings()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): Settings {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.looker.droidify.datastore
|
||||
import android.net.Uri
|
||||
import com.looker.droidify.datastore.model.AutoSync
|
||||
import com.looker.droidify.datastore.model.InstallerType
|
||||
import com.looker.droidify.datastore.model.LegacyInstallerComponent
|
||||
import com.looker.droidify.datastore.model.ProxyType
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.datastore.model.Theme
|
||||
@@ -37,6 +38,8 @@ interface SettingsRepository {
|
||||
|
||||
suspend fun setInstallerType(installerType: InstallerType)
|
||||
|
||||
suspend fun setLegacyInstallerComponent(component: LegacyInstallerComponent?)
|
||||
|
||||
suspend fun setAutoUpdate(allow: Boolean)
|
||||
|
||||
suspend fun setAutoSync(autoSync: AutoSync)
|
||||
|
||||
@@ -92,7 +92,7 @@ fun Context?.sortOrderName(sortOrder: SortOrder) = this?.let {
|
||||
SortOrder.UPDATED -> getString(stringRes.recently_updated)
|
||||
SortOrder.ADDED -> getString(stringRes.whats_new)
|
||||
SortOrder.NAME -> getString(stringRes.name)
|
||||
// SortOrder.SIZE -> getString(stringRes.size)
|
||||
SortOrder.SIZE -> getString(stringRes.size)
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.looker.droidify.datastore.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class LegacyInstallerComponent {
|
||||
@Serializable
|
||||
object Unspecified : LegacyInstallerComponent()
|
||||
|
||||
@Serializable
|
||||
object AlwaysChoose : LegacyInstallerComponent()
|
||||
|
||||
@Serializable
|
||||
data class Component(
|
||||
val clazz: String,
|
||||
val activity: String,
|
||||
) : LegacyInstallerComponent() {
|
||||
fun update(
|
||||
newClazz: String? = null,
|
||||
newActivity: String? = null,
|
||||
): Component = copy(
|
||||
clazz = newClazz ?: clazz,
|
||||
activity = newActivity ?: activity
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,8 @@ package com.looker.droidify.datastore.model
|
||||
enum class SortOrder {
|
||||
UPDATED,
|
||||
ADDED,
|
||||
NAME
|
||||
NAME,
|
||||
SIZE,
|
||||
}
|
||||
|
||||
fun supportedSortOrders(): List<SortOrder> = listOf(SortOrder.UPDATED, SortOrder.ADDED, SortOrder.NAME)
|
||||
|
||||
51
app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt
Normal file
51
app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.looker.droidify.di
|
||||
|
||||
import android.content.Context
|
||||
import com.looker.droidify.data.local.DroidifyDatabase
|
||||
import com.looker.droidify.data.local.dao.AppDao
|
||||
import com.looker.droidify.data.local.dao.AuthDao
|
||||
import com.looker.droidify.data.local.dao.IndexDao
|
||||
import com.looker.droidify.data.local.dao.RepoDao
|
||||
import com.looker.droidify.data.local.droidifyDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideDatabase(
|
||||
@ApplicationContext
|
||||
context: Context,
|
||||
): DroidifyDatabase = droidifyDatabase(context)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAppDao(
|
||||
db: DroidifyDatabase,
|
||||
): AppDao = db.appDao()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideRepoDao(
|
||||
db: DroidifyDatabase,
|
||||
): RepoDao = db.repoDao()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAuthDao(
|
||||
db: DroidifyDatabase,
|
||||
): AuthDao = db.authDao()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideIndexDao(
|
||||
db: DroidifyDatabase,
|
||||
): IndexDao = db.indexDao()
|
||||
}
|
||||
23
app/src/main/kotlin/com/looker/droidify/di/SyncableModule.kt
Normal file
23
app/src/main/kotlin/com/looker/droidify/di/SyncableModule.kt
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.looker.droidify.di
|
||||
|
||||
import android.content.Context
|
||||
import com.looker.droidify.sync.LocalSyncable
|
||||
import com.looker.droidify.sync.Syncable
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object SyncableModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSyncable(
|
||||
@ApplicationContext context: Context,
|
||||
): Syncable<IndexV2> = LocalSyncable(context)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ data class App(
|
||||
val categories: List<String>,
|
||||
val links: Links,
|
||||
val metadata: Metadata,
|
||||
val author: Author,
|
||||
val author: Author?,
|
||||
val screenshots: Screenshots,
|
||||
val graphics: Graphics,
|
||||
val donation: Donation,
|
||||
@@ -15,34 +15,35 @@ data class App(
|
||||
)
|
||||
|
||||
data class Author(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val email: String,
|
||||
val web: String
|
||||
val id: Int,
|
||||
val name: String?,
|
||||
val email: String?,
|
||||
val phone: String?,
|
||||
val web: String?,
|
||||
)
|
||||
|
||||
data class Donation(
|
||||
val regularUrl: String? = null,
|
||||
val regularUrl: List<String>? = null,
|
||||
val bitcoinAddress: String? = null,
|
||||
val flattrId: String? = null,
|
||||
val liteCoinAddress: String? = null,
|
||||
val litecoinAddress: String? = null,
|
||||
val openCollectiveId: String? = null,
|
||||
val librePayId: String? = null,
|
||||
val liberapayId: String? = null,
|
||||
)
|
||||
|
||||
data class Graphics(
|
||||
val featureGraphic: String = "",
|
||||
val promoGraphic: String = "",
|
||||
val tvBanner: String = "",
|
||||
val video: String = ""
|
||||
val featureGraphic: String? = null,
|
||||
val promoGraphic: String? = null,
|
||||
val tvBanner: String? = null,
|
||||
val video: String? = null,
|
||||
)
|
||||
|
||||
data class Links(
|
||||
val changelog: String = "",
|
||||
val issueTracker: String = "",
|
||||
val sourceCode: String = "",
|
||||
val translation: String = "",
|
||||
val webSite: String = ""
|
||||
val changelog: String? = null,
|
||||
val issueTracker: String? = null,
|
||||
val sourceCode: String? = null,
|
||||
val translation: String? = null,
|
||||
val webSite: String? = null,
|
||||
)
|
||||
|
||||
data class Metadata(
|
||||
|
||||
@@ -27,6 +27,22 @@ fun ByteArray.hex(): String = joinToString(separator = "") { byte ->
|
||||
fun Fingerprint.formattedString(): String = value.windowed(2, 2, false)
|
||||
.take(FINGERPRINT_LENGTH / 2).joinToString(separator = " ") { it.uppercase(Locale.US) }
|
||||
|
||||
fun String.fingerprint(): Fingerprint = Fingerprint(
|
||||
MessageDigest.getInstance("SHA-256")
|
||||
.digest(
|
||||
this
|
||||
.chunked(2)
|
||||
.mapNotNull { byteStr ->
|
||||
try {
|
||||
byteStr.toInt(16).toByte()
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toByteArray(),
|
||||
).hex(),
|
||||
)
|
||||
|
||||
fun Certificate.fingerprint(): Fingerprint {
|
||||
val bytes = encoded
|
||||
return if (bytes.size >= 256) {
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
package com.looker.droidify.domain.model
|
||||
|
||||
data class Repo(
|
||||
val id: Long,
|
||||
val id: Int,
|
||||
val enabled: Boolean,
|
||||
val address: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val fingerprint: Fingerprint?,
|
||||
val authentication: Authentication,
|
||||
val authentication: Authentication?,
|
||||
val versionInfo: VersionInfo,
|
||||
val mirrors: List<String>,
|
||||
val antiFeatures: List<AntiFeature>,
|
||||
val categories: List<Category>
|
||||
) {
|
||||
val shouldAuthenticate =
|
||||
authentication.username.isNotEmpty() && authentication.password.isNotEmpty()
|
||||
val shouldAuthenticate = authentication != null
|
||||
|
||||
fun update(fingerprint: Fingerprint, timestamp: Long? = null, etag: String? = null): Repo {
|
||||
return copy(
|
||||
fingerprint = fingerprint,
|
||||
versionInfo = timestamp?.let { VersionInfo(timestamp = it, etag = etag) } ?: versionInfo
|
||||
versionInfo = timestamp?.let { VersionInfo(timestamp = it, etag = etag) }
|
||||
?: versionInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -28,22 +26,22 @@ data class AntiFeature(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val icon: String = "",
|
||||
val description: String = ""
|
||||
val description: String = "",
|
||||
)
|
||||
|
||||
data class Category(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val icon: String = "",
|
||||
val description: String = ""
|
||||
val description: String = "",
|
||||
)
|
||||
|
||||
data class Authentication(
|
||||
val username: String,
|
||||
val password: String
|
||||
val password: String,
|
||||
)
|
||||
|
||||
data class VersionInfo(
|
||||
val timestamp: Long,
|
||||
val etag: String?
|
||||
val etag: String?,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.looker.droidify.index
|
||||
|
||||
import android.util.Xml
|
||||
import com.looker.droidify.domain.model.fingerprint
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.model.Repository.Companion.defaultRepository
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Direct copy of implementation from https://github.com/NeoApplications/Neo-Store/blob/master/src/main/kotlin/com/machiav3lli/fdroid/data/database/entity/Repository.kt
|
||||
* */
|
||||
object OemRepositoryParser {
|
||||
|
||||
private val rootDirs = arrayOf("/system", "/product", "/vendor", "/odm", "/oem")
|
||||
private val supportedPackageNames = arrayOf("com.looker.droidify", "org.fdroid.fdroid")
|
||||
private const val FILE_NAME = "additional_repos.xml"
|
||||
|
||||
fun getSystemDefaultRepos() = rootDirs.flatMap { rootDir ->
|
||||
supportedPackageNames.map { packageName -> "$rootDir/etc/$packageName/$FILE_NAME" }
|
||||
}.flatMap { path ->
|
||||
val file = File(path)
|
||||
if (file.exists()) parse(file.inputStream()) else emptyList()
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
|
||||
fun parse(inputStream: InputStream): List<Repository> = with(Xml.newPullParser()) {
|
||||
val repoItems = mutableListOf<String>()
|
||||
inputStream.use { input ->
|
||||
setInput(input, null)
|
||||
var isItem = false
|
||||
while (next() != XmlPullParser.END_DOCUMENT) {
|
||||
when (eventType) {
|
||||
XmlPullParser.START_TAG -> if (name == "item") isItem = true
|
||||
XmlPullParser.TEXT -> if (isItem) repoItems.add(text)
|
||||
XmlPullParser.END_TAG -> isItem = false
|
||||
}
|
||||
}
|
||||
}
|
||||
repoItems.chunked(7).mapNotNull { itemsSet -> fromXML(itemsSet) }
|
||||
}
|
||||
|
||||
private fun fromXML(xml: List<String>) = runCatching {
|
||||
defaultRepository(
|
||||
name = xml[0],
|
||||
address = xml[1],
|
||||
description = xml[2].replace(Regex("\\s+"), " ").trim(),
|
||||
version = xml[3].toInt(),
|
||||
enabled = xml[4].toInt() > 0,
|
||||
fingerprint = xml[6].let {
|
||||
if (it.length > 32) it.fingerprint().value
|
||||
else it
|
||||
},
|
||||
authentication = "",
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.looker.droidify.index
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.domain.model.fingerprint
|
||||
import com.looker.droidify.model.Product
|
||||
@@ -15,10 +15,13 @@ import com.looker.droidify.utility.common.extension.toFormattedString
|
||||
import com.looker.droidify.utility.common.result.Result
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.getProgress
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.security.CodeSigner
|
||||
import java.security.cert.Certificate
|
||||
@@ -33,7 +36,7 @@ object RepositoryUpdater {
|
||||
// TODO Add support for Index-V2 and also cleanup everything here
|
||||
enum class IndexType(
|
||||
val jarName: String,
|
||||
val contentName: String
|
||||
val contentName: String,
|
||||
) {
|
||||
INDEX_V1("index-v1.jar", "index-v1.json")
|
||||
}
|
||||
@@ -51,7 +54,7 @@ object RepositoryUpdater {
|
||||
|
||||
constructor(errorType: ErrorType, message: String, cause: Exception) : super(
|
||||
message,
|
||||
cause
|
||||
cause,
|
||||
) {
|
||||
this.errorType = errorType
|
||||
}
|
||||
@@ -89,13 +92,13 @@ object RepositoryUpdater {
|
||||
context: Context,
|
||||
repository: Repository,
|
||||
unstable: Boolean,
|
||||
callback: (Stage, Long, Long?) -> Unit
|
||||
callback: (Stage, Long, Long?) -> Unit,
|
||||
) = update(
|
||||
context = context,
|
||||
repository = repository,
|
||||
unstable = unstable,
|
||||
indexTypes = listOf(IndexType.INDEX_V1),
|
||||
callback = callback
|
||||
callback = callback,
|
||||
)
|
||||
|
||||
private suspend fun update(
|
||||
@@ -103,7 +106,7 @@ object RepositoryUpdater {
|
||||
repository: Repository,
|
||||
unstable: Boolean,
|
||||
indexTypes: List<IndexType>,
|
||||
callback: (Stage, Long, Long?) -> Unit
|
||||
callback: (Stage, Long, Long?) -> Unit,
|
||||
): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
val indexType = indexTypes[0]
|
||||
when (val request = downloadIndex(context, repository, indexType, callback)) {
|
||||
@@ -120,14 +123,14 @@ object RepositoryUpdater {
|
||||
repository = repository,
|
||||
indexTypes = indexTypes.subList(1, indexTypes.size),
|
||||
unstable = unstable,
|
||||
callback = callback
|
||||
callback = callback,
|
||||
)
|
||||
} else {
|
||||
Result.Error(
|
||||
UpdateException(
|
||||
ErrorType.HTTP,
|
||||
"Invalid response: HTTP ${result.statusCode}"
|
||||
)
|
||||
"Invalid response: HTTP ${result.statusCode}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -146,7 +149,7 @@ object RepositoryUpdater {
|
||||
file = request.data.file,
|
||||
lastModified = request.data.lastModified,
|
||||
entityTag = request.data.entityTag,
|
||||
callback = callback
|
||||
callback = callback,
|
||||
)
|
||||
Result.Success(isFileParsedSuccessfully)
|
||||
} catch (e: UpdateException) {
|
||||
@@ -161,20 +164,20 @@ object RepositoryUpdater {
|
||||
context: Context,
|
||||
repository: Repository,
|
||||
indexType: IndexType,
|
||||
callback: (Stage, Long, Long?) -> Unit
|
||||
callback: (Stage, Long, Long?) -> Unit,
|
||||
): Result<IndexFile> = withContext(Dispatchers.IO) {
|
||||
val file = Cache.getTemporaryFile(context)
|
||||
val result = downloader.downloadToFile(
|
||||
url = Uri.parse(repository.address).buildUpon()
|
||||
url = repository.address.toUri().buildUpon()
|
||||
.appendPath(indexType.jarName).build().toString(),
|
||||
target = file,
|
||||
headers = {
|
||||
ifModifiedSince(repository.lastModified)
|
||||
etag(repository.entityTag)
|
||||
authentication(repository.authentication)
|
||||
}
|
||||
},
|
||||
) { read, total ->
|
||||
callback(Stage.DOWNLOAD, read.value, total.value.takeIf { it != 0L })
|
||||
callback(Stage.DOWNLOAD, read.value, total?.value)
|
||||
}
|
||||
|
||||
when (result) {
|
||||
@@ -185,8 +188,8 @@ object RepositoryUpdater {
|
||||
lastModified = result.lastModified?.toFormattedString() ?: "",
|
||||
entityTag = result.etag ?: "",
|
||||
statusCode = result.statusCode,
|
||||
file = file
|
||||
)
|
||||
file = file,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -203,8 +206,8 @@ object RepositoryUpdater {
|
||||
Result.Error(
|
||||
UpdateException(
|
||||
errorType = errorType,
|
||||
message = "Failed with Status: ${result.statusCode}"
|
||||
)
|
||||
message = "Failed with Status: ${result.statusCode}",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -228,7 +231,7 @@ object RepositoryUpdater {
|
||||
mergerFile: File = Cache.getTemporaryFile(context),
|
||||
lastModified: String,
|
||||
entityTag: String,
|
||||
callback: (Stage, Long, Long?) -> Unit
|
||||
callback: (Stage, Long, Long?) -> Unit,
|
||||
): Boolean {
|
||||
var rollback = true
|
||||
return synchronized(updaterLock) {
|
||||
@@ -258,7 +261,7 @@ object RepositoryUpdater {
|
||||
name: String,
|
||||
description: String,
|
||||
version: Int,
|
||||
timestamp: Long
|
||||
timestamp: Long,
|
||||
) {
|
||||
changedRepository = repository.update(
|
||||
mirrors,
|
||||
@@ -267,7 +270,7 @@ object RepositoryUpdater {
|
||||
version,
|
||||
lastModified,
|
||||
entityTag,
|
||||
timestamp
|
||||
timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -284,7 +287,7 @@ object RepositoryUpdater {
|
||||
|
||||
override fun onReleases(
|
||||
packageName: String,
|
||||
releases: List<Release>
|
||||
releases: List<Release>,
|
||||
) {
|
||||
if (Thread.interrupted()) {
|
||||
throw InterruptedException()
|
||||
@@ -295,7 +298,7 @@ object RepositoryUpdater {
|
||||
unmergedReleases.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (Thread.interrupted()) {
|
||||
@@ -318,11 +321,11 @@ object RepositoryUpdater {
|
||||
callback(
|
||||
Stage.MERGE,
|
||||
progress.toLong(),
|
||||
totalCount.toLong()
|
||||
totalCount.toLong(),
|
||||
)
|
||||
Database.UpdaterAdapter.putTemporary(
|
||||
products
|
||||
.map { transformProduct(it, features, unstable) }
|
||||
.map { transformProduct(it, features, unstable) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -336,7 +339,7 @@ object RepositoryUpdater {
|
||||
throw UpdateException(
|
||||
ErrorType.VALIDATION,
|
||||
"New index is older than current index:" +
|
||||
" ${workRepository.timestamp} < ${repository.timestamp}"
|
||||
" ${workRepository.timestamp} < ${repository.timestamp}",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -349,13 +352,13 @@ object RepositoryUpdater {
|
||||
|
||||
val commitRepository = if (!workRepository.fingerprint.equals(
|
||||
fingerprint,
|
||||
ignoreCase = true
|
||||
ignoreCase = true,
|
||||
)
|
||||
) {
|
||||
if (workRepository.fingerprint.isNotEmpty()) {
|
||||
throw UpdateException(
|
||||
ErrorType.VALIDATION,
|
||||
"Certificate fingerprints do not match"
|
||||
"Certificate fingerprints do not match",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -391,7 +394,7 @@ object RepositoryUpdater {
|
||||
get() = codeSigners?.singleOrNull()
|
||||
?: throw UpdateException(
|
||||
ErrorType.VALIDATION,
|
||||
"index.jar must be signed by a single code signer"
|
||||
"index.jar must be signed by a single code signer",
|
||||
)
|
||||
|
||||
@get:Throws(UpdateException::class)
|
||||
@@ -399,13 +402,13 @@ object RepositoryUpdater {
|
||||
get() = signerCertPath?.certificates?.singleOrNull()
|
||||
?: throw UpdateException(
|
||||
ErrorType.VALIDATION,
|
||||
"index.jar code signer should have only one certificate"
|
||||
"index.jar code signer should have only one certificate",
|
||||
)
|
||||
|
||||
private fun transformProduct(
|
||||
product: Product,
|
||||
features: Set<String>,
|
||||
unstable: Boolean
|
||||
unstable: Boolean,
|
||||
): Product {
|
||||
val releasePairs = product.releases
|
||||
.distinctBy { it.identifier }
|
||||
@@ -445,7 +448,7 @@ object RepositoryUpdater {
|
||||
selected = firstSelected?.let {
|
||||
it.first.versionCode == release.versionCode &&
|
||||
it.second == incompatibilities
|
||||
} ?: false
|
||||
} ?: false,
|
||||
)
|
||||
}
|
||||
return product.copy(releases = releases)
|
||||
@@ -457,5 +460,5 @@ data class IndexFile(
|
||||
val lastModified: String,
|
||||
val entityTag: String,
|
||||
val statusCode: Int,
|
||||
val file: File
|
||||
val file: File,
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class InstallManager(
|
||||
private val context: Context,
|
||||
settingsRepository: SettingsRepository
|
||||
private val settingsRepository: SettingsRepository
|
||||
) {
|
||||
|
||||
private val installItems = Channel<InstallItem>()
|
||||
@@ -115,7 +115,7 @@ class InstallManager(
|
||||
private suspend fun setInstaller(installerType: InstallerType) {
|
||||
lock.withLock {
|
||||
_installer = when (installerType) {
|
||||
InstallerType.LEGACY -> LegacyInstaller(context)
|
||||
InstallerType.LEGACY -> LegacyInstaller(context, settingsRepository)
|
||||
InstallerType.SESSION -> SessionInstaller(context)
|
||||
InstallerType.SHIZUKU -> ShizukuInstaller(context)
|
||||
InstallerType.ROOT -> RootInstaller(context)
|
||||
|
||||
@@ -8,7 +8,9 @@ import com.looker.droidify.utility.common.extension.getLauncherActivities
|
||||
import com.looker.droidify.utility.common.extension.getPackageInfoCompat
|
||||
import com.looker.droidify.utility.common.extension.intent
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.ShizukuProvider
|
||||
import rikka.sui.Sui
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 87263
|
||||
@@ -16,42 +18,47 @@ private const val SHIZUKU_PERMISSION_REQUEST_CODE = 87263
|
||||
fun launchShizuku(context: Context) {
|
||||
val activities =
|
||||
context.packageManager.getLauncherActivities(ShizukuProvider.MANAGER_APPLICATION_ID)
|
||||
if (activities.isEmpty()) return
|
||||
val intent = intent(Intent.ACTION_MAIN) {
|
||||
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
setComponent(
|
||||
ComponentName(
|
||||
ShizukuProvider.MANAGER_APPLICATION_ID,
|
||||
activities.first().first
|
||||
)
|
||||
activities.first().first,
|
||||
),
|
||||
)
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun initSui(context: Context) = Sui.init(context.packageName)
|
||||
|
||||
fun isSuiAvailable() = Sui.isSui()
|
||||
|
||||
fun isShizukuInstalled(context: Context) =
|
||||
context.packageManager.getPackageInfoCompat(ShizukuProvider.MANAGER_APPLICATION_ID) != null
|
||||
|
||||
fun isShizukuAlive() = rikka.shizuku.Shizuku.pingBinder()
|
||||
fun isShizukuAlive() = Shizuku.pingBinder()
|
||||
|
||||
fun isShizukuGranted() = rikka.shizuku.Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||
fun isShizukuGranted() = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
suspend fun requestPermissionListener() = suspendCancellableCoroutine {
|
||||
val listener = rikka.shizuku.Shizuku.OnRequestPermissionResultListener { requestCode, grantResult ->
|
||||
val listener = Shizuku.OnRequestPermissionResultListener { requestCode, grantResult ->
|
||||
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||
it.resume(grantResult == PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
}
|
||||
rikka.shizuku.Shizuku.addRequestPermissionResultListener(listener)
|
||||
rikka.shizuku.Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
Shizuku.addRequestPermissionResultListener(listener)
|
||||
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
it.invokeOnCancellation {
|
||||
rikka.shizuku.Shizuku.removeRequestPermissionResultListener(listener)
|
||||
Shizuku.removeRequestPermissionResultListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestShizuku() {
|
||||
rikka.shizuku.Shizuku.shouldShowRequestPermissionRationale()
|
||||
rikka.shizuku.Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
Shizuku.shouldShowRequestPermissionRationale()
|
||||
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
|
||||
fun isMagiskGranted(): Boolean {
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
package com.looker.droidify.installer.installers
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AndroidRuntimeException
|
||||
import androidx.core.net.toUri
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.get
|
||||
import com.looker.droidify.datastore.model.LegacyInstallerComponent
|
||||
import com.looker.droidify.domain.model.PackageName
|
||||
import com.looker.droidify.installer.model.InstallItem
|
||||
import com.looker.droidify.installer.model.InstallState
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.utility.common.extension.intent
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class LegacyInstaller(private val context: Context) : Installer {
|
||||
class LegacyInstaller(
|
||||
private val context: Context,
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : Installer {
|
||||
|
||||
companion object {
|
||||
private const val APK_MIME = "application/vnd.android.package-archive"
|
||||
@@ -22,30 +31,51 @@ class LegacyInstaller(private val context: Context) : Installer {
|
||||
|
||||
override suspend fun install(
|
||||
installItem: InstallItem,
|
||||
): InstallState = suspendCancellableCoroutine { cont ->
|
||||
): InstallState {
|
||||
val installFlag = if (SdkCheck.isNougat) Intent.FLAG_GRANT_READ_URI_PERMISSION else 0
|
||||
val fileUri = if (SdkCheck.isNougat) {
|
||||
Cache.getReleaseUri(
|
||||
context,
|
||||
installItem.installFileName
|
||||
)
|
||||
Cache.getReleaseUri(context, installItem.installFileName)
|
||||
} else {
|
||||
Cache.getReleaseFile(context, installItem.installFileName).toUri()
|
||||
}
|
||||
val installIntent = intent(Intent.ACTION_INSTALL_PACKAGE) {
|
||||
|
||||
val comp = settingsRepository.get { legacyInstallerComponent }.firstOrNull()
|
||||
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
val intent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
|
||||
setDataAndType(fileUri, APK_MIME)
|
||||
flags = installFlag
|
||||
when (comp) {
|
||||
is LegacyInstallerComponent.Component -> {
|
||||
component = ComponentName(comp.clazz, comp.activity)
|
||||
}
|
||||
else -> {
|
||||
// For Unspecified and AlwaysChoose, don't set component
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val installIntent = when (comp) {
|
||||
LegacyInstallerComponent.AlwaysChoose -> Intent.createChooser(intent, context.getString(
|
||||
R.string.select_installer))
|
||||
else -> intent
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(installIntent)
|
||||
cont.resume(InstallState.Installed)
|
||||
} catch (e: AndroidRuntimeException) {
|
||||
installIntent.flags = installFlag or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
try {
|
||||
context.startActivity(installIntent)
|
||||
cont.resume(InstallState.Installed)
|
||||
} catch (e: Exception) {
|
||||
cont.resume(InstallState.Failed)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cont.resume(InstallState.Failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uninstall(packageName: PackageName) =
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.looker.droidify.installer.installers.shizuku
|
||||
|
||||
import android.content.Context
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.utility.common.extension.size
|
||||
import com.looker.droidify.domain.model.PackageName
|
||||
import com.looker.droidify.installer.installers.Installer
|
||||
import com.looker.droidify.installer.installers.uninstallPackage
|
||||
import com.looker.droidify.installer.model.InstallItem
|
||||
import com.looker.droidify.installer.model.InstallState
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.utility.common.extension.size
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStream
|
||||
@@ -21,13 +21,14 @@ class ShizukuInstaller(private val context: Context) : Installer {
|
||||
}
|
||||
|
||||
override suspend fun install(
|
||||
installItem: InstallItem
|
||||
installItem: InstallItem,
|
||||
): InstallState = suspendCancellableCoroutine { cont ->
|
||||
var sessionId: String? = null
|
||||
val file = Cache.getReleaseFile(context, installItem.installFileName)
|
||||
val packageName = installItem.packageName.name
|
||||
try {
|
||||
val fileSize = file.size ?: run {
|
||||
val fileSize = file.length()
|
||||
if (fileSize == 0L) {
|
||||
cont.cancel()
|
||||
error("File is not valid: Size ${file.size}")
|
||||
}
|
||||
@@ -43,26 +44,26 @@ class ShizukuInstaller(private val context: Context) : Installer {
|
||||
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
||||
?: run {
|
||||
cont.cancel()
|
||||
throw RuntimeException("Failed to create install session")
|
||||
error("Failed to create install session")
|
||||
}
|
||||
if (cont.isCompleted) return@suspendCancellableCoroutine
|
||||
|
||||
val writeResult = exec("pm install-write -S $fileSize $sessionId base -", it)
|
||||
if (writeResult.resultCode != 0) {
|
||||
cont.cancel()
|
||||
throw RuntimeException("Failed to write APK to session $sessionId")
|
||||
error("Failed to write APK to session $sessionId")
|
||||
}
|
||||
if (cont.isCompleted) return@suspendCancellableCoroutine
|
||||
|
||||
val commitResult = exec("pm install-commit $sessionId")
|
||||
if (commitResult.resultCode != 0) {
|
||||
cont.cancel()
|
||||
throw RuntimeException("Failed to commit install session $sessionId")
|
||||
error("Failed to commit install session $sessionId")
|
||||
}
|
||||
if (cont.isCompleted) return@suspendCancellableCoroutine
|
||||
cont.resume(InstallState.Installed)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
if (sessionId != null) exec("pm install-abandon $sessionId")
|
||||
cont.resume(InstallState.Failed)
|
||||
}
|
||||
@@ -71,7 +72,7 @@ class ShizukuInstaller(private val context: Context) : Installer {
|
||||
override suspend fun uninstall(packageName: PackageName) =
|
||||
context.uninstallPackage(packageName)
|
||||
|
||||
override fun close() {}
|
||||
override fun close() = Unit
|
||||
|
||||
private data class ShellResult(val resultCode: Int, val out: String)
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ data class Repository(
|
||||
return defaultRepository(address, name, "", 0, true, fingerprint, authentication)
|
||||
}
|
||||
|
||||
private fun defaultRepository(
|
||||
fun defaultRepository(
|
||||
address: String,
|
||||
name: String,
|
||||
description: String,
|
||||
@@ -137,9 +137,9 @@ data class Repository(
|
||||
),
|
||||
defaultRepository(
|
||||
address = "https://microg.org/fdroid/repo",
|
||||
name = "MicroG Project",
|
||||
description = "The official repository for MicroG." +
|
||||
" MicroG is a lightweight open-source implementation" +
|
||||
name = "microG Project",
|
||||
description = "The official repository for microG." +
|
||||
" microG is a lightweight open source implementation" +
|
||||
" of Google Play Services.",
|
||||
fingerprint = "9BD06727E62796C0130EB6DAB39B73157451582CBD138E86C468ACC395D14165"
|
||||
),
|
||||
@@ -171,13 +171,6 @@ data class Repository(
|
||||
description = "Collabora Office is an office suite based on LibreOffice.",
|
||||
fingerprint = "573258C84E149B5F4D9299E7434B2B69A8410372921D4AE586BA91EC767892CC"
|
||||
),
|
||||
defaultRepository(
|
||||
address = "https://fdroid.libretro.com/repo",
|
||||
name = "LibRetro",
|
||||
description = "The official canary repository for this great" +
|
||||
" retro emulators hub.",
|
||||
fingerprint = "3F05B24D497515F31FEAB421297C79B19552C5C81186B3750B7C131EF41D733D"
|
||||
),
|
||||
defaultRepository(
|
||||
address = "https://cdn.kde.org/android/fdroid/repo",
|
||||
name = "KDE Android",
|
||||
@@ -200,7 +193,7 @@ data class Repository(
|
||||
address = "https://fdroid.fedilab.app/repo",
|
||||
name = "Fedilab",
|
||||
description = "The official repository for Fedilab. Fedilab is a " +
|
||||
"multi-accounts client for Mastodon, Peertube, and other free" +
|
||||
"multi-accounts client for Mastodon, PeerTube, and other free" +
|
||||
" software social networks.",
|
||||
fingerprint = "11F0A69910A4280E2CD3CCC3146337D006BE539B18E1A9FEACE15FF757A94FEB"
|
||||
),
|
||||
@@ -209,14 +202,7 @@ data class Repository(
|
||||
name = "Kali Nethunter",
|
||||
description = "Kali Nethunter's official selection of original b" +
|
||||
"inaries.",
|
||||
fingerprint = "7E418D34C3AD4F3C37D7E6B0FACE13332364459C862134EB099A3BDA2CCF4494"
|
||||
),
|
||||
defaultRepository(
|
||||
address = "https://secfirst.org/fdroid/repo",
|
||||
name = "Umbrella",
|
||||
description = "The official repository for Umbrella. Umbrella is" +
|
||||
" a collection of security advices, tutorials, tools etc.",
|
||||
fingerprint = "39EB57052F8D684514176819D1645F6A0A7BD943DBC31AB101949006AC0BC228"
|
||||
fingerprint = "FE7A23DFC003A1CF2D2ADD2469B9C0C49B206BA5DC9EDD6563B3B7EB6A8F5FAB"
|
||||
),
|
||||
defaultRepository(
|
||||
address = "https://thecapslock.gitlab.io/fdroid-patched-apps/fdroid/repo",
|
||||
@@ -257,14 +243,14 @@ data class Repository(
|
||||
name = "Threema Libre",
|
||||
description = "The official repository for Threema Libre. R" +
|
||||
"equires Threema Shop license. Threema Libre is an open" +
|
||||
"-source messanger focused on security and privacy.",
|
||||
"-source messenger focused on security and privacy.",
|
||||
fingerprint = "5734E753899B25775D90FE85362A49866E05AC4F83C05BEF5A92880D2910639E"
|
||||
),
|
||||
defaultRepository(
|
||||
address = "https://fdroid.getsession.org/fdroid/repo",
|
||||
name = "Session",
|
||||
description = "The official repository for Session. Session" +
|
||||
" is an open-source messanger focused on security and privacy.",
|
||||
" is an open-source messenger focused on security and privacy.",
|
||||
fingerprint = "DB0E5297EB65CC22D6BD93C869943BDCFCB6A07DC69A48A0DD8C7BA698EC04E6"
|
||||
),
|
||||
defaultRepository(
|
||||
@@ -413,5 +399,12 @@ data class Repository(
|
||||
fingerprint = "49F37E74DEE483DCA2B991334FB5A0200787430D0B5F9A783DD5F13695E9517B"
|
||||
)
|
||||
)
|
||||
|
||||
val toRemove: List<String> = listOf(
|
||||
// Add repository addresses that should be removed during database upgrades and remove them from the lists above
|
||||
// Example: "https://example.com/fdroid/repo"
|
||||
"https://secfirst.org/fdroid/repo",
|
||||
"https://fdroid.libretro.com/repo"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,4 @@ interface Downloader {
|
||||
): NetworkResponse
|
||||
}
|
||||
|
||||
typealias ProgressListener = suspend (bytesReceived: DataSize, contentLength: DataSize) -> Unit
|
||||
typealias ProgressListener = suspend (bytesReceived: DataSize, contentLength: DataSize?) -> Unit
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.looker.droidify.network.header.HeadersBuilder
|
||||
import com.looker.droidify.network.header.KtorHeadersBuilder
|
||||
import com.looker.droidify.network.validation.FileValidator
|
||||
import com.looker.droidify.network.validation.ValidationException
|
||||
import com.looker.droidify.utility.common.extension.size
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.HttpClientEngine
|
||||
@@ -51,12 +50,9 @@ internal class KtorDownloader(
|
||||
|
||||
override suspend fun headCall(
|
||||
url: String,
|
||||
headers: HeadersBuilder.() -> Unit
|
||||
headers: HeadersBuilder.() -> Unit,
|
||||
): NetworkResponse {
|
||||
val headRequest = createRequest(
|
||||
url = url,
|
||||
headers = headers
|
||||
)
|
||||
val headRequest = request(url, headers = headers)
|
||||
return client.head(headRequest).asNetworkResponse()
|
||||
}
|
||||
|
||||
@@ -65,24 +61,26 @@ internal class KtorDownloader(
|
||||
target: File,
|
||||
validator: FileValidator?,
|
||||
headers: HeadersBuilder.() -> Unit,
|
||||
block: ProgressListener?
|
||||
block: ProgressListener?,
|
||||
): NetworkResponse = withContext(dispatcher) {
|
||||
try {
|
||||
val request = createRequest(
|
||||
val fileSize = target.length()
|
||||
val request = request(
|
||||
url = url,
|
||||
headers = {
|
||||
inRange(target.size)
|
||||
fileSize = fileSize,
|
||||
block = block,
|
||||
) {
|
||||
inRange(fileSize)
|
||||
headers()
|
||||
},
|
||||
fileSize = target.size,
|
||||
block = block
|
||||
)
|
||||
}
|
||||
client.prepareGet(request).execute { response ->
|
||||
val networkResponse = response.asNetworkResponse()
|
||||
if (networkResponse !is NetworkResponse.Success) {
|
||||
return@execute networkResponse
|
||||
}
|
||||
response.bodyAsChannel().copyTo(target.outputStream())
|
||||
target.outputStream().use { output ->
|
||||
response.bodyAsChannel().copyTo(output)
|
||||
}
|
||||
validator?.validate(target)
|
||||
networkResponse
|
||||
}
|
||||
@@ -95,37 +93,34 @@ internal class KtorDownloader(
|
||||
} catch (e: ValidationException) {
|
||||
target.delete()
|
||||
NetworkResponse.Error.Validation(e)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
NetworkResponse.Error.Unknown(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun client(
|
||||
engine: HttpClientEngine = OkHttp.create()
|
||||
): HttpClient {
|
||||
return HttpClient(engine) {
|
||||
engine: HttpClientEngine = OkHttp.create(),
|
||||
) = HttpClient(engine) {
|
||||
userAgentConfig()
|
||||
timeoutConfig()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun createRequest(
|
||||
private fun request(
|
||||
url: String,
|
||||
fileSize: Long = 0L,
|
||||
block: ProgressListener? = null,
|
||||
headers: HeadersBuilder.() -> Unit,
|
||||
fileSize: Long? = null,
|
||||
block: ProgressListener? = null
|
||||
) = request {
|
||||
url(url)
|
||||
this.headers {
|
||||
KtorHeadersBuilder(this).headers()
|
||||
}
|
||||
onDownload { read, total ->
|
||||
headers { KtorHeadersBuilder(this).headers() }
|
||||
if (block != null) {
|
||||
onDownload { read, total ->
|
||||
block(
|
||||
DataSize(read + (fileSize ?: 0L)),
|
||||
DataSize((total ?: 0L) + (fileSize ?: 0L))
|
||||
DataSize(read + fileSize),
|
||||
total?.let { DataSize(total + fileSize) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@ interface HeadersBuilder {
|
||||
|
||||
fun authentication(base64: String)
|
||||
|
||||
fun inRange(start: Number?, end: Number? = null)
|
||||
fun inRange(start: Long, end: Long? = null)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
internal class KtorHeadersBuilder(
|
||||
private val builder: io.ktor.http.HeadersBuilder
|
||||
private val builder: io.ktor.http.HeadersBuilder,
|
||||
) : HeadersBuilder {
|
||||
|
||||
override fun String.headsWith(value: Any?) {
|
||||
@@ -38,8 +38,7 @@ internal class KtorHeadersBuilder(
|
||||
HttpHeaders.Authorization headsWith base64
|
||||
}
|
||||
|
||||
override fun inRange(start: Number?, end: Number?) {
|
||||
if (start == null) return
|
||||
override fun inRange(start: Long, end: Long?) {
|
||||
val valueString = if (end != null) "bytes=$start-$end" else "bytes=$start-"
|
||||
HttpHeaders.Range headsWith valueString
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
data object Idle : State("")
|
||||
data class Connecting(val name: String) : State(name)
|
||||
data class Downloading(val name: String, val read: DataSize, val total: DataSize?) : State(
|
||||
name
|
||||
name,
|
||||
)
|
||||
|
||||
data class Error(val name: String) : State(name)
|
||||
@@ -84,7 +84,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
|
||||
data class DownloadState(
|
||||
val currentItem: State = State.Idle,
|
||||
val queue: List<String> = emptyList()
|
||||
val queue: List<String> = emptyList(),
|
||||
) {
|
||||
infix fun isDownloading(packageName: String): Boolean =
|
||||
currentItem.packageName == packageName && (
|
||||
@@ -108,7 +108,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
val release: Release,
|
||||
val url: String,
|
||||
val authentication: String,
|
||||
val isUpdate: Boolean = false
|
||||
val isUpdate: Boolean = false,
|
||||
) {
|
||||
val notificationTag: String
|
||||
get() = "download-$packageName"
|
||||
@@ -129,7 +129,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
name: String,
|
||||
repository: Repository,
|
||||
release: Release,
|
||||
isUpdate: Boolean = false
|
||||
isUpdate: Boolean = false,
|
||||
) {
|
||||
val task = Task(
|
||||
packageName = packageName,
|
||||
@@ -137,7 +137,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
release = release,
|
||||
url = release.getDownloadUrl(repository),
|
||||
authentication = repository.authentication,
|
||||
isUpdate = isUpdate
|
||||
isUpdate = isUpdate,
|
||||
)
|
||||
if (Cache.getReleaseFile(this@DownloadService, release.cacheFileName).exists()) {
|
||||
lifecycleScope.launch { publishSuccess(task) }
|
||||
@@ -147,7 +147,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
cancelCurrentTask(packageName)
|
||||
notificationManager?.cancel(
|
||||
task.notificationTag,
|
||||
Constants.NOTIFICATION_ID_DOWNLOADING
|
||||
Constants.NOTIFICATION_ID_DOWNLOADING,
|
||||
)
|
||||
tasks += task
|
||||
if (currentTask == null) {
|
||||
@@ -174,7 +174,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
)
|
||||
createNotificationChannel(
|
||||
id = NOTIFICATION_CHANNEL_INSTALL,
|
||||
name = getString(stringRes.install)
|
||||
name = getString(stringRes.install),
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -250,13 +250,13 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentIntent(intent)
|
||||
.errorNotificationContent(task, errorType)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.errorNotificationContent(
|
||||
task: Task,
|
||||
errorType: ErrorType
|
||||
errorType: ErrorType,
|
||||
): NotificationCompat.Builder {
|
||||
val title = if (errorType is ErrorType.Validation) {
|
||||
stringRes.could_not_validate_FORMAT
|
||||
@@ -325,8 +325,8 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
this,
|
||||
0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
?.let { notification ->
|
||||
startForeground(
|
||||
Constants.NOTIFICATION_ID_DOWNLOADING,
|
||||
notification.build()
|
||||
notification.build(),
|
||||
)
|
||||
} ?: run {
|
||||
log("Invalid Download State: $state", "DownloadService", Log.ERROR)
|
||||
@@ -345,7 +345,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.downloadingNotificationContent(
|
||||
state: State
|
||||
state: State,
|
||||
): NotificationCompat.Builder? {
|
||||
return when (state) {
|
||||
is State.Connecting -> {
|
||||
@@ -403,19 +403,19 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
|
||||
private fun CoroutineScope.downloadFile(
|
||||
task: Task,
|
||||
target: File
|
||||
target: File,
|
||||
) = launch {
|
||||
try {
|
||||
val releaseValidator = ReleaseFileValidator(
|
||||
context = this@DownloadService,
|
||||
packageName = task.packageName,
|
||||
release = task.release
|
||||
release = task.release,
|
||||
)
|
||||
val response = downloader.downloadToFile(
|
||||
url = task.url,
|
||||
target = target,
|
||||
validator = releaseValidator,
|
||||
headers = { authentication(task.authentication) }
|
||||
headers = { if (task.authentication.isNotEmpty()) authentication(task.authentication) },
|
||||
) { read, total ->
|
||||
yield()
|
||||
updateCurrentState(State.Downloading(task.packageName, read, total))
|
||||
@@ -425,7 +425,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
is NetworkResponse.Success -> {
|
||||
val releaseFile = Cache.getReleaseFile(
|
||||
this@DownloadService,
|
||||
task.release.cacheFileName
|
||||
task.release.cacheFileName,
|
||||
)
|
||||
target.renameTo(releaseFile)
|
||||
publishSuccess(task)
|
||||
@@ -438,7 +438,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
is NetworkResponse.Error.IO -> ErrorType.IO
|
||||
is NetworkResponse.Error.SocketTimeout -> ErrorType.SocketTimeout
|
||||
is NetworkResponse.Error.Validation -> ErrorType.Validation(
|
||||
response.exception
|
||||
response.exception,
|
||||
)
|
||||
|
||||
else -> ErrorType.Http
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.looker.droidify.sync
|
||||
|
||||
import android.content.Context
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.sync.common.JsonParser
|
||||
import com.looker.droidify.sync.v2.V2Parser
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
class LocalSyncable(
|
||||
private val context: Context,
|
||||
) : Syncable<IndexV2> {
|
||||
override val parser: Parser<IndexV2>
|
||||
get() = V2Parser(Dispatchers.IO, JsonParser)
|
||||
|
||||
override suspend fun sync(repo: Repo): Pair<Fingerprint, IndexV2?> {
|
||||
val file = Cache.getTemporaryFile(context).apply {
|
||||
outputStream().use {
|
||||
it.write(context.assets.open("izzy_index_v2.json").readBytes())
|
||||
}
|
||||
}
|
||||
return parser.parse(file, repo)
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,14 @@ package com.looker.droidify.sync
|
||||
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
|
||||
/**
|
||||
* Expected Architecture: [https://excalidraw.com/#json=JqpGunWTJONjq-ecDNiPg,j9t0X4coeNvIG7B33GTq6A]
|
||||
*
|
||||
* Current Issue: When downloading entry.jar we need to re-call the synchronizer,
|
||||
* which this arch doesn't allow.
|
||||
*/
|
||||
interface Syncable<T> {
|
||||
|
||||
val parser: Parser<T>
|
||||
|
||||
suspend fun sync(
|
||||
repo: Repo,
|
||||
): Pair<Fingerprint, com.looker.droidify.sync.v2.model.IndexV2?>
|
||||
): Pair<Fingerprint, IndexV2?>
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.looker.droidify.sync.v1.model.RepoV1
|
||||
import com.looker.droidify.sync.v1.model.maxSdk
|
||||
import com.looker.droidify.sync.v1.model.name
|
||||
import com.looker.droidify.sync.v2.model.AntiFeatureV2
|
||||
import com.looker.droidify.sync.v2.model.ApkFileV2
|
||||
import com.looker.droidify.sync.v2.model.CategoryV2
|
||||
import com.looker.droidify.sync.v2.model.FeatureV2
|
||||
import com.looker.droidify.sync.v2.model.FileV2
|
||||
@@ -94,7 +95,7 @@ private fun AppV1.toV2(preferredSigner: String?): MetadataV2 = MetadataV2(
|
||||
added = added ?: 0L,
|
||||
lastUpdated = lastUpdated ?: 0L,
|
||||
icon = localized?.localizedIcon(packageName, icon) { it.icon },
|
||||
name = localized?.localizedString(name) { it.name },
|
||||
name = localized?.localizedString(name) { it.name } ?: emptyMap(),
|
||||
description = localized?.localizedString(description) { it.description },
|
||||
summary = localized?.localizedString(summary) { it.summary },
|
||||
authorEmail = authorEmail,
|
||||
@@ -157,7 +158,7 @@ private fun PackageV1.toVersionV2(
|
||||
packageAntiFeatures: List<String>,
|
||||
): VersionV2 = VersionV2(
|
||||
added = added ?: 0L,
|
||||
file = FileV2(
|
||||
file = ApkFileV2(
|
||||
name = "/$apkName",
|
||||
sha256 = hash,
|
||||
size = size,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.looker.droidify.sync.common
|
||||
|
||||
import android.content.Context
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
@@ -16,23 +16,25 @@ suspend fun Downloader.downloadIndex(
|
||||
url: String,
|
||||
diff: Boolean = false,
|
||||
): File = withContext(Dispatchers.IO) {
|
||||
val tempFile = Cache.getIndexFile(context, "repo_${repo.id}_$fileName")
|
||||
val indexFile = Cache.getIndexFile(context, "repo_${repo.id}_$fileName")
|
||||
downloadToFile(
|
||||
url = url,
|
||||
target = tempFile,
|
||||
target = indexFile,
|
||||
headers = {
|
||||
if (repo.shouldAuthenticate) {
|
||||
with(requireNotNull(repo.authentication)) {
|
||||
authentication(
|
||||
repo.authentication.username,
|
||||
repo.authentication.password
|
||||
username = username,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (repo.versionInfo.timestamp > 0L && !diff) {
|
||||
ifModifiedSince(Date(repo.versionInfo.timestamp))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
tempFile
|
||||
indexFile
|
||||
}
|
||||
|
||||
const val INDEX_V1_NAME = "index-v1.jar"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.looker.droidify.sync.v2
|
||||
|
||||
import android.content.Context
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.sync.Parser
|
||||
import com.looker.droidify.sync.Syncable
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.sync.common.ENTRY_V2_NAME
|
||||
import com.looker.droidify.sync.common.INDEX_V2_NAME
|
||||
import com.looker.droidify.sync.common.IndexJarValidator
|
||||
@@ -15,7 +14,9 @@ import com.looker.droidify.sync.common.downloadIndex
|
||||
import com.looker.droidify.sync.v2.model.Entry
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
import com.looker.droidify.sync.v2.model.IndexV2Diff
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -51,7 +52,7 @@ class EntrySyncable(
|
||||
context = context,
|
||||
repo = repo,
|
||||
url = repo.address.removeSuffix("/") + "/$ENTRY_V2_NAME",
|
||||
fileName = ENTRY_V2_NAME
|
||||
fileName = ENTRY_V2_NAME,
|
||||
)
|
||||
val (fingerprint, entry) = parser.parse(jar, repo)
|
||||
jar.delete()
|
||||
@@ -61,7 +62,6 @@ class EntrySyncable(
|
||||
val indexPath = repo.address.removeSuffix("/") + index.name
|
||||
val indexFile = Cache.getIndexFile(context, "repo_${repo.id}_$INDEX_V2_NAME")
|
||||
val indexV2 = if (index != entry.index && indexFile.exists()) {
|
||||
// example https://apt.izzysoft.de/fdroid/repo/diff/1725372028000.json
|
||||
val diffFile = downloader.downloadIndex(
|
||||
context = context,
|
||||
repo = repo,
|
||||
@@ -69,17 +69,12 @@ class EntrySyncable(
|
||||
fileName = "diff_${repo.versionInfo.timestamp}.json",
|
||||
diff = true,
|
||||
)
|
||||
// TODO: Maybe parse in parallel
|
||||
diffParser.parse(diffFile, repo).second.let {
|
||||
val diff = async { diffParser.parse(diffFile, repo).second }
|
||||
val oldIndex = async { indexParser.parse(indexFile, repo).second }
|
||||
diff.await().patchInto(oldIndex.await()) { index ->
|
||||
diffFile.delete()
|
||||
it.patchInto(
|
||||
indexParser.parse(
|
||||
indexFile,
|
||||
repo
|
||||
).second) { index ->
|
||||
Json.encodeToStream(index, indexFile.outputStream())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// example https://apt.izzysoft.de/fdroid/repo/index-v2.json
|
||||
val newIndexFile = downloader.downloadIndex(
|
||||
|
||||
@@ -12,3 +12,10 @@ data class FileV2(
|
||||
val sha256: String? = null,
|
||||
val size: Long? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApkFileV2(
|
||||
val name: String,
|
||||
val sha256: String,
|
||||
val size: Long,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,44 @@
|
||||
package com.looker.droidify.sync.v2.model
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
|
||||
typealias LocalizedString = Map<String, String>
|
||||
typealias NullableLocalizedString = Map<String, String?>
|
||||
typealias LocalizedIcon = Map<String, FileV2>
|
||||
typealias LocalizedList = Map<String, List<String>>
|
||||
typealias LocalizedFiles = Map<String, List<FileV2>>
|
||||
|
||||
typealias DefaultName = String
|
||||
typealias Tag = String
|
||||
|
||||
typealias AntiFeatureReason = LocalizedString
|
||||
|
||||
fun Map<String, Any>?.localesSize(): Int? = this?.keys?.size
|
||||
|
||||
fun Map<String, Any>?.locales(): List<String> = buildList {
|
||||
if (!isNullOrEmpty()) {
|
||||
for (locale in this@locales!!.keys) {
|
||||
add(locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Map<String, T>?.localizedValue(locale: String): T? {
|
||||
if (isNullOrEmpty()) return null
|
||||
val localeList = LocaleListCompat.forLanguageTags(locale)
|
||||
val match = localeList.getFirstMatch(keys.toTypedArray()) ?: return null
|
||||
return get(match.toLanguageTag()) ?: run {
|
||||
val langCountryTag = "${match.language}-${match.country}"
|
||||
getOrStartsWith(langCountryTag) ?: run {
|
||||
val langTag = match.language
|
||||
getOrStartsWith(langTag) ?: get("en-US") ?: get("en") ?: values.first()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Map<String, T>.getOrStartsWith(s: String): T? = get(s) ?: run {
|
||||
entries.forEach { (key, value) ->
|
||||
if (key.startsWith(s)) return value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ data class PackageV2Diff(
|
||||
added = metadata?.added ?: 0L,
|
||||
lastUpdated = metadata?.lastUpdated ?: 0L,
|
||||
name = metadata?.name
|
||||
?.mapNotNull { (key, value) -> value?.let { key to value } }?.toMap(),
|
||||
?.mapNotNull { (key, value) -> value?.let { key to value } }?.toMap() ?: emptyMap(),
|
||||
summary = metadata?.summary
|
||||
?.mapNotNull { (key, value) -> value?.let { key to value } }?.toMap(),
|
||||
description = metadata?.description
|
||||
@@ -116,7 +116,7 @@ data class PackageV2Diff(
|
||||
|
||||
@Serializable
|
||||
data class MetadataV2(
|
||||
val name: LocalizedString? = null,
|
||||
val name: LocalizedString,
|
||||
val summary: LocalizedString? = null,
|
||||
val description: LocalizedString? = null,
|
||||
val icon: LocalizedIcon? = null,
|
||||
@@ -129,7 +129,7 @@ data class MetadataV2(
|
||||
val bitcoin: String? = null,
|
||||
val categories: List<String> = emptyList(),
|
||||
val changelog: String? = null,
|
||||
val donate: List<String> = emptyList(),
|
||||
val donate: List<String>? = null,
|
||||
val featureGraphic: LocalizedIcon? = null,
|
||||
val flattrID: String? = null,
|
||||
val issueTracker: String? = null,
|
||||
@@ -183,25 +183,25 @@ data class MetadataV2Diff(
|
||||
@Serializable
|
||||
data class VersionV2(
|
||||
val added: Long,
|
||||
val file: FileV2,
|
||||
val file: ApkFileV2,
|
||||
val src: FileV2? = null,
|
||||
val whatsNew: LocalizedString = emptyMap(),
|
||||
val manifest: ManifestV2,
|
||||
val antiFeatures: Map<String, LocalizedString> = emptyMap(),
|
||||
val antiFeatures: Map<Tag, AntiFeatureReason> = emptyMap(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VersionV2Diff(
|
||||
val added: Long? = null,
|
||||
val file: FileV2? = null,
|
||||
val file: ApkFileV2? = null,
|
||||
val src: FileV2? = null,
|
||||
val whatsNew: LocalizedString? = null,
|
||||
val manifest: ManifestV2? = null,
|
||||
val antiFeatures: Map<String, LocalizedString>? = null,
|
||||
val antiFeatures: Map<Tag, AntiFeatureReason>? = null,
|
||||
) {
|
||||
fun toVersion() = VersionV2(
|
||||
added = added ?: 0,
|
||||
file = file ?: FileV2(""),
|
||||
file = file ?: ApkFileV2("", "", -1L),
|
||||
src = src ?: FileV2(""),
|
||||
whatsNew = whatsNew ?: emptyMap(),
|
||||
manifest = manifest ?: ManifestV2(
|
||||
|
||||
@@ -10,8 +10,8 @@ data class RepoV2(
|
||||
val icon: LocalizedIcon? = null,
|
||||
val name: LocalizedString = emptyMap(),
|
||||
val description: LocalizedString = emptyMap(),
|
||||
val antiFeatures: Map<String, AntiFeatureV2> = emptyMap(),
|
||||
val categories: Map<String, CategoryV2> = emptyMap(),
|
||||
val antiFeatures: Map<Tag, AntiFeatureV2> = emptyMap(),
|
||||
val categories: Map<DefaultName, CategoryV2> = emptyMap(),
|
||||
val mirrors: List<MirrorV2> = emptyList(),
|
||||
val timestamp: Long,
|
||||
)
|
||||
@@ -22,8 +22,8 @@ data class RepoV2Diff(
|
||||
val icon: LocalizedIcon? = null,
|
||||
val name: LocalizedString? = null,
|
||||
val description: LocalizedString? = null,
|
||||
val antiFeatures: Map<String, AntiFeatureV2?>? = null,
|
||||
val categories: Map<String, CategoryV2?>? = null,
|
||||
val antiFeatures: Map<Tag, AntiFeatureV2?>? = null,
|
||||
val categories: Map<DefaultName, CategoryV2?>? = null,
|
||||
val mirrors: List<MirrorV2>? = null,
|
||||
val timestamp: Long,
|
||||
) {
|
||||
@@ -69,7 +69,7 @@ data class RepoV2Diff(
|
||||
data class MirrorV2(
|
||||
val url: String,
|
||||
val isPrimary: Boolean? = null,
|
||||
val location: String? = null
|
||||
val countryCode: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -75,7 +75,6 @@ import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.resources.TypefaceExtra
|
||||
import com.looker.droidify.utility.extension.resources.sizeScaled
|
||||
import com.looker.droidify.widget.StableRecyclerAdapter
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
@@ -87,10 +86,13 @@ import java.util.Locale
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sin
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.Instant
|
||||
import com.google.android.material.R as MaterialR
|
||||
import com.looker.droidify.R.drawable as drawableRes
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
StableRecyclerAdapter<AppDetailAdapter.ViewType, RecyclerView.ViewHolder>() {
|
||||
|
||||
@@ -557,7 +559,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
val size = itemView.findViewById<TextView>(R.id.size)!!
|
||||
val signature = itemView.findViewById<TextView>(R.id.signature)!!
|
||||
val compatibility = itemView.findViewById<TextView>(R.id.compatibility)!!
|
||||
val targetSdk = itemView.findViewById<TextView>(R.id.target_sdk)!!
|
||||
val sdkVer = itemView.findViewById<TextView>(R.id.sdk_ver)!!
|
||||
|
||||
val statefulViews: Sequence<View>
|
||||
get() = sequenceOf(
|
||||
@@ -569,7 +571,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
size,
|
||||
signature,
|
||||
compatibility,
|
||||
targetSdk,
|
||||
sdkVer,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1712,15 +1714,22 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
)
|
||||
}
|
||||
}
|
||||
with(holder.targetSdk) {
|
||||
val sdkVersion = sdkName.getOrDefault(
|
||||
with(holder.sdkVer) {
|
||||
val targetSdkVersion = sdkName.getOrDefault(
|
||||
item.release.targetSdkVersion,
|
||||
context.getString(
|
||||
stringRes.label_unknown_sdk,
|
||||
item.release.targetSdkVersion,
|
||||
),
|
||||
)
|
||||
text = context.getString(stringRes.label_targets_sdk, sdkVersion)
|
||||
val minSdkVersion = sdkName.getOrDefault(
|
||||
item.release.minSdkVersion,
|
||||
context.getString(
|
||||
stringRes.label_unknown_sdk,
|
||||
item.release.minSdkVersion,
|
||||
),
|
||||
)
|
||||
text = context.getString(stringRes.label_sdk_version, targetSdkVersion, minSdkVersion)
|
||||
}
|
||||
val enabled = status == Status.Idle
|
||||
holder.statefulViews.forEach { it.isEnabled = enabled }
|
||||
|
||||
@@ -66,13 +66,13 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
constructor(packageName: String, repoAddress: String? = null) : this() {
|
||||
arguments = bundleOf(
|
||||
ARG_PACKAGE_NAME to packageName,
|
||||
ARG_REPO_ADDRESS to repoAddress
|
||||
ARG_REPO_ADDRESS to repoAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private enum class Action(
|
||||
val id: Int,
|
||||
val adapterAction: AppDetailAdapter.Action
|
||||
val adapterAction: AppDetailAdapter.Action,
|
||||
) {
|
||||
INSTALL(1, AppDetailAdapter.Action.INSTALL),
|
||||
UPDATE(2, AppDetailAdapter.Action.UPDATE),
|
||||
@@ -85,7 +85,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
private class Installed(
|
||||
val installedItem: InstalledItem,
|
||||
val isSystem: Boolean,
|
||||
val launcherActivities: List<Pair<String, String>>
|
||||
val launcherActivities: List<Pair<String, String>>,
|
||||
)
|
||||
|
||||
private val viewModel: AppDetailViewModel by viewModels()
|
||||
@@ -109,7 +109,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
lifecycleScope.launch {
|
||||
binder.downloadState.collect(::updateDownloadState)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@@ -138,7 +138,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
this.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
false,
|
||||
)
|
||||
isMotionEventSplittingEnabled = false
|
||||
isVerticalScrollBarEnabled = false
|
||||
@@ -151,7 +151,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
|
||||
recyclerView = this
|
||||
systemBarsPadding(includeFab = false)
|
||||
}
|
||||
},
|
||||
)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
@@ -188,7 +188,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
products = products,
|
||||
installedItem = state.installedItem,
|
||||
isFavourite = state.isFavourite,
|
||||
allowIncompatibleVersion = state.allowIncompatibleVersions
|
||||
allowIncompatibleVersion = state.allowIncompatibleVersions,
|
||||
)
|
||||
updateButtons()
|
||||
}
|
||||
@@ -226,7 +226,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
}
|
||||
|
||||
private fun updateButtons(
|
||||
preference: ProductPreference = ProductPreferences[viewModel.packageName]
|
||||
preference: ProductPreference = ProductPreferences[viewModel.packageName],
|
||||
) {
|
||||
val installed = installed
|
||||
val product = products.findSuggested(installed?.installedItem)?.first
|
||||
@@ -278,7 +278,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
|
||||
private fun updateToolbarButtons(
|
||||
isActionVisible: Boolean = (recyclerView?.layoutManager as LinearLayoutManager)
|
||||
.findFirstVisibleItemPosition() == 0
|
||||
.findFirstVisibleItemPosition() == 0,
|
||||
) {
|
||||
toolbar.title = if (isActionVisible) {
|
||||
getString(stringRes.application)
|
||||
@@ -324,7 +324,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
is DownloadService.State.Connecting -> AppDetailAdapter.Status.Connecting
|
||||
is DownloadService.State.Downloading -> AppDetailAdapter.Status.Downloading(
|
||||
state.currentItem.read,
|
||||
state.currentItem.total
|
||||
state.currentItem.total,
|
||||
)
|
||||
|
||||
else -> AppDetailAdapter.Status.Idle
|
||||
@@ -340,7 +340,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
if (state.currentItem is DownloadService.State.Success && isResumed) {
|
||||
viewModel.installPackage(
|
||||
state.currentItem.packageName,
|
||||
state.currentItem.release.cacheFileName
|
||||
state.currentItem.release.cacheFileName,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -348,7 +348,8 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
override fun onActionClick(action: AppDetailAdapter.Action) {
|
||||
when (action) {
|
||||
AppDetailAdapter.Action.INSTALL,
|
||||
AppDetailAdapter.Action.UPDATE -> {
|
||||
AppDetailAdapter.Action.UPDATE,
|
||||
-> {
|
||||
if (Cache.getEmptySpace(requireContext()) < products.first().first.releases.first().size) {
|
||||
MessageDialog(Message.InsufficientStorage).show(childFragmentManager)
|
||||
return
|
||||
@@ -375,7 +376,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
if (launcherActivities.size >= 2) {
|
||||
LaunchDialog(launcherActivities).show(
|
||||
childFragmentManager,
|
||||
LaunchDialog::class.java.name
|
||||
LaunchDialog::class.java.name,
|
||||
)
|
||||
} else {
|
||||
launcherActivities.firstOrNull()?.let { startLauncherActivity(it.first) }
|
||||
@@ -386,8 +387,8 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
startActivity(
|
||||
Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
"package:${viewModel.packageName}".toUri()
|
||||
)
|
||||
"package:${viewModel.packageName}".toUri(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -405,18 +406,17 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
AppDetailAdapter.Action.SHARE -> {
|
||||
val repo = products[0].second
|
||||
val address = when {
|
||||
repo.name == "F-Droid" ->
|
||||
"https://f-droid.org/packages/" +
|
||||
"${viewModel.packageName}/"
|
||||
"https://f-droid.org/repo" in repo.mirrors ->
|
||||
"https://f-droid.org/packages/${viewModel.packageName}/"
|
||||
|
||||
"IzzyOnDroid" in repo.name -> {
|
||||
"https://f-droid.org/archive/repo" in repo.mirrors ->
|
||||
"https://f-droid.org/packages/${viewModel.packageName}/"
|
||||
|
||||
"https://apt.izzysoft.de/fdroid/repo" in repo.mirrors ->
|
||||
"https://apt.izzysoft.de/fdroid/index/apk/${viewModel.packageName}"
|
||||
}
|
||||
|
||||
else -> {
|
||||
"https://droidify.eu.org/app/?id=" +
|
||||
"${viewModel.packageName}&repo_address=${repo.address}"
|
||||
}
|
||||
else ->
|
||||
"https://droidify.eu.org/app/?id=${viewModel.packageName}&repo_address=${repo.address}"
|
||||
}
|
||||
val sendIntent = Intent(Intent.ACTION_SEND)
|
||||
.putExtra(Intent.EXTRA_TEXT, address)
|
||||
@@ -436,7 +436,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
.setComponent(ComponentName(viewModel.packageName, name))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -464,7 +464,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
val screenshotUrl = current.url(
|
||||
context = requireContext(),
|
||||
repository = productRepository.second,
|
||||
packageName = viewModel.packageName
|
||||
packageName = viewModel.packageName,
|
||||
)
|
||||
view.load(screenshotUrl) {
|
||||
allowHardware(false)
|
||||
@@ -484,8 +484,8 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
release.incompatibilities,
|
||||
release.platforms,
|
||||
release.minSdkVersion,
|
||||
release.maxSdkVersion
|
||||
)
|
||||
release.maxSdkVersion,
|
||||
),
|
||||
).show(childFragmentManager)
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
productRepository.first.name,
|
||||
productRepository.second,
|
||||
release,
|
||||
installedItem != null
|
||||
installedItem != null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,23 @@ import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.model.InstallerType
|
||||
import com.looker.droidify.domain.model.toPackageName
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.model.InstalledItem
|
||||
import com.looker.droidify.model.Product
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.installer.InstallManager
|
||||
import com.looker.droidify.installer.installers.isShizukuAlive
|
||||
import com.looker.droidify.installer.installers.isShizukuGranted
|
||||
import com.looker.droidify.installer.installers.isShizukuInstalled
|
||||
import com.looker.droidify.installer.installers.isSuiAvailable
|
||||
import com.looker.droidify.installer.installers.requestPermissionListener
|
||||
import com.looker.droidify.installer.model.InstallState
|
||||
import com.looker.droidify.installer.model.installFrom
|
||||
import com.looker.droidify.model.InstalledItem
|
||||
import com.looker.droidify.model.Product
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -33,7 +34,7 @@ import javax.inject.Inject
|
||||
class AppDetailViewModel @Inject constructor(
|
||||
private val installer: InstallManager,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
val packageName: String = requireNotNull(savedStateHandle[ARG_PACKAGE_NAME])
|
||||
@@ -52,7 +53,7 @@ class AppDetailViewModel @Inject constructor(
|
||||
Database.RepositoryAdapter.getAllStream(),
|
||||
Database.InstalledAdapter.getStream(packageName),
|
||||
repoAddress,
|
||||
flow { emit(settingsRepository.getInitial()) }
|
||||
flow { emit(settingsRepository.getInitial()) },
|
||||
) { products, repositories, installedItem, suggestedAddress, initialSettings ->
|
||||
val idAndRepos = repositories.associateBy { it.id }
|
||||
val filteredProducts = products.filter { product ->
|
||||
@@ -65,7 +66,7 @@ class AppDetailViewModel @Inject constructor(
|
||||
isFavourite = packageName in initialSettings.favouriteApps,
|
||||
allowIncompatibleVersions = initialSettings.incompatibleVersions,
|
||||
isSelf = packageName == BuildConfig.APPLICATION_ID,
|
||||
addressIfUnavailable = suggestedAddress
|
||||
addressIfUnavailable = suggestedAddress,
|
||||
)
|
||||
}.asStateFlow(AppDetailUiState())
|
||||
|
||||
@@ -74,6 +75,9 @@ class AppDetailViewModel @Inject constructor(
|
||||
runBlocking { settingsRepository.getInitial().installerType == InstallerType.SHIZUKU }
|
||||
if (!isSelected) return null
|
||||
val isAlive = isShizukuAlive()
|
||||
val isSuiAvailable = isSuiAvailable()
|
||||
if (isSuiAvailable) return null
|
||||
|
||||
val isGranted = if (isAlive) {
|
||||
if (isShizukuGranted()) {
|
||||
true
|
||||
@@ -144,5 +148,5 @@ data class AppDetailUiState(
|
||||
val isSelf: Boolean = false,
|
||||
val isFavourite: Boolean = false,
|
||||
val allowIncompatibleVersions: Boolean = false,
|
||||
val addressIfUnavailable: String? = null
|
||||
val addressIfUnavailable: String? = null,
|
||||
)
|
||||
|
||||
@@ -45,12 +45,11 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
enum class Source(
|
||||
val titleResId: Int,
|
||||
val sections: Boolean,
|
||||
val order: Boolean,
|
||||
val updateAll: Boolean,
|
||||
) {
|
||||
AVAILABLE(stringRes.available, true, true, false),
|
||||
INSTALLED(stringRes.installed, false, true, false),
|
||||
UPDATES(stringRes.updates, false, false, true)
|
||||
AVAILABLE(stringRes.available, true, false),
|
||||
INSTALLED(stringRes.installed, false, false),
|
||||
UPDATES(stringRes.updates, false, true)
|
||||
}
|
||||
|
||||
constructor(source: Source) : this() {
|
||||
@@ -134,7 +133,6 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
|
||||
|
||||
updateRequest()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
launch {
|
||||
@@ -143,7 +141,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
}
|
||||
}
|
||||
launch {
|
||||
viewModel.sortOrderFlow.collect {
|
||||
viewModel.state.collect {
|
||||
updateRequest()
|
||||
}
|
||||
}
|
||||
@@ -185,16 +183,12 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setSearchQuery(searchQuery: String) {
|
||||
viewModel.setSearchQuery(searchQuery) {
|
||||
updateRequest()
|
||||
}
|
||||
fun setSearchQuery(searchQuery: String) {
|
||||
viewModel.setSearchQuery(searchQuery)
|
||||
}
|
||||
|
||||
internal fun setSection(section: ProductItem.Section) {
|
||||
viewModel.setSection(section) {
|
||||
updateRequest()
|
||||
}
|
||||
fun setSection(section: ProductItem.Section) {
|
||||
viewModel.setSection(section)
|
||||
}
|
||||
|
||||
private fun updateRequest() {
|
||||
|
||||
@@ -16,10 +16,10 @@ import com.looker.droidify.service.Connection
|
||||
import com.looker.droidify.service.SyncService
|
||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -34,25 +34,37 @@ class AppListViewModel
|
||||
.get { ignoreSignature }
|
||||
.asStateFlow(false)
|
||||
|
||||
val sortOrderFlow = settingsRepository
|
||||
private val sortOrderFlow = settingsRepository
|
||||
.get { sortOrder }
|
||||
.asStateFlow(SortOrder.UPDATED)
|
||||
|
||||
private val sections = MutableStateFlow<ProductItem.Section>(All)
|
||||
|
||||
val searchQuery = MutableStateFlow("")
|
||||
|
||||
val state = combine(
|
||||
sortOrderFlow,
|
||||
sections,
|
||||
searchQuery,
|
||||
) { sortOrder, section, query ->
|
||||
AppListState(
|
||||
searchQuery = query,
|
||||
sections = section,
|
||||
sortOrder = sortOrder,
|
||||
)
|
||||
}.asStateFlow(AppListState())
|
||||
|
||||
val reposStream = Database.RepositoryAdapter
|
||||
.getAllStream()
|
||||
.asStateFlow(emptyList())
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val showUpdateAllButton = skipSignatureStream.flatMapConcat { skip ->
|
||||
val showUpdateAllButton = skipSignatureStream.flatMapLatest { skip ->
|
||||
Database.ProductAdapter
|
||||
.getUpdatesStream(skip)
|
||||
.map { it.isNotEmpty() }
|
||||
}.asStateFlow(false)
|
||||
|
||||
private val sections = MutableStateFlow<ProductItem.Section>(All)
|
||||
|
||||
val searchQuery = MutableStateFlow("")
|
||||
|
||||
val syncConnection = Connection(SyncService::class.java)
|
||||
|
||||
fun updateAll() {
|
||||
@@ -79,26 +91,25 @@ class AppListViewModel
|
||||
searchQuery = searchQuery.value,
|
||||
section = sections.value,
|
||||
order = sortOrderFlow.value,
|
||||
skipSignatureCheck = skipSignatureStream.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSection(newSection: ProductItem.Section, perform: () -> Unit) {
|
||||
fun setSection(newSection: ProductItem.Section) {
|
||||
viewModelScope.launch {
|
||||
if (newSection != sections.value) {
|
||||
sections.emit(newSection)
|
||||
launch(Dispatchers.Main) { perform() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSearchQuery(newSearchQuery: String, perform: () -> Unit) {
|
||||
fun setSearchQuery(newSearchQuery: String) {
|
||||
viewModelScope.launch {
|
||||
if (newSearchQuery != searchQuery.value) {
|
||||
searchQuery.emit(newSearchQuery)
|
||||
launch(Dispatchers.Main) { perform() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AppListState(
|
||||
val searchQuery: String = "",
|
||||
val sections: ProductItem.Section = All,
|
||||
val sortOrder: SortOrder = SortOrder.UPDATED,
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.looker.droidify.ui.Message
|
||||
import com.looker.droidify.ui.MessageDialog
|
||||
import com.looker.droidify.ui.ScreenFragment
|
||||
import com.looker.droidify.utility.common.extension.clipboardManager
|
||||
import com.looker.droidify.utility.common.extension.exceptCancellation
|
||||
import com.looker.droidify.utility.common.extension.get
|
||||
import com.looker.droidify.utility.common.extension.getMutatedIcon
|
||||
import com.looker.droidify.utility.common.nullIfEmpty
|
||||
@@ -136,7 +137,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
Selection.setSelection(
|
||||
text,
|
||||
realPosition(outputString, inputStart),
|
||||
realPosition(outputString, inputEnd)
|
||||
realPosition(outputString, inputEnd),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -155,7 +156,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
Pair(
|
||||
uri.buildUpon().path(uri.path?.pathCropped).query(null).fragment(null)
|
||||
.build().toString(),
|
||||
fingerprintText
|
||||
fingerprintText,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Pair(null, null)
|
||||
@@ -171,7 +172,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
setEndIconOnClickListener {
|
||||
SelectMirrorDialog(mirrors).show(
|
||||
childFragmentManager,
|
||||
SelectMirrorDialog::class.java.name
|
||||
SelectMirrorDialog::class.java.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -189,7 +190,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
if (index >= 0) {
|
||||
Pair(
|
||||
it.substring(0, index),
|
||||
it.substring(index + 1)
|
||||
it.substring(index + 1),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -319,7 +320,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
return if (endsWith != null) {
|
||||
cropped.substring(
|
||||
0,
|
||||
cropped.length - endsWith.length - 1
|
||||
cropped.length - endsWith.length - 1,
|
||||
)
|
||||
} else {
|
||||
cropped
|
||||
@@ -330,12 +331,12 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
val uri = try {
|
||||
val uri = URI(address)
|
||||
if (uri.isAbsolute) uri.normalize() else null
|
||||
} catch (e: URISyntaxException) {
|
||||
} catch (_: URISyntaxException) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
uri?.toURL()?.toURI()?.toString()?.removeSuffix("/")
|
||||
} catch (e: URISyntaxException) {
|
||||
} catch (_: URISyntaxException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -346,7 +347,10 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
|
||||
private fun onSaveRepositoryClick(check: Boolean) {
|
||||
if (!checkInProgress) {
|
||||
val address = normalizeAddress(binding.address.text.toString())!!
|
||||
val address = normalizeAddress(binding.address.text.toString()) ?: kotlin.run {
|
||||
failedAddressCheck()
|
||||
return
|
||||
}
|
||||
val fingerprint = binding.fingerprint.text.toString().replace(" ", "")
|
||||
val username = binding.username.text.toString().nullIfEmpty()
|
||||
val password = binding.password.text.toString().nullIfEmpty()
|
||||
@@ -354,7 +358,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
password?.let { p ->
|
||||
Base64.encodeToString(
|
||||
"$u:$p".toByteArray(Charset.defaultCharset()),
|
||||
Base64.NO_WRAP
|
||||
Base64.NO_WRAP,
|
||||
)
|
||||
}
|
||||
}?.let { "Basic $it" }.orEmpty()
|
||||
@@ -364,7 +368,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
val resultAddress = try {
|
||||
checkAddress(address, authentication)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
e.exceptCancellation()
|
||||
failedAddressCheck()
|
||||
null
|
||||
}
|
||||
@@ -378,7 +382,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
onSaveRepositoryProceedInvalidate(
|
||||
resultAddress,
|
||||
fingerprint,
|
||||
authentication
|
||||
authentication,
|
||||
)
|
||||
} else {
|
||||
invalidateState()
|
||||
@@ -393,7 +397,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
|
||||
private suspend fun checkAddress(
|
||||
rawAddress: String,
|
||||
authentication: String
|
||||
authentication: String,
|
||||
): String? = coroutineScope {
|
||||
checkInProgress = true
|
||||
invalidateState()
|
||||
@@ -403,7 +407,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
.forEach { address ->
|
||||
val response = downloader.headCall(
|
||||
url = "$address/index-v1.jar",
|
||||
headers = { authentication(authentication) }
|
||||
headers = { authentication(authentication) },
|
||||
)
|
||||
if (response is NetworkResponse.Success) return@coroutineScope address
|
||||
}
|
||||
@@ -413,7 +417,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
private fun onSaveRepositoryProceedInvalidate(
|
||||
address: String,
|
||||
fingerprint: String,
|
||||
authentication: String
|
||||
authentication: String,
|
||||
) {
|
||||
val binder = syncConnection.binder
|
||||
if (binder != null) {
|
||||
@@ -442,7 +446,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
Snackbar.make(
|
||||
requireView(),
|
||||
R.string.repository_unreachable,
|
||||
Snackbar.LENGTH_SHORT
|
||||
Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.looker.droidify.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -13,6 +14,7 @@ import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -36,6 +38,7 @@ import com.looker.droidify.datastore.extension.themeName
|
||||
import com.looker.droidify.datastore.extension.toTime
|
||||
import com.looker.droidify.datastore.model.AutoSync
|
||||
import com.looker.droidify.datastore.model.InstallerType
|
||||
import com.looker.droidify.datastore.model.LegacyInstallerComponent
|
||||
import com.looker.droidify.datastore.model.ProxyType
|
||||
import com.looker.droidify.datastore.model.Theme
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
@@ -72,7 +75,7 @@ class SettingsFragment : Fragment() {
|
||||
private const val FOXY_DROID_URL = "https://github.com/kitsunyan/foxy-droid"
|
||||
|
||||
private const val DROID_IFY_TITLE = "Droid-ify"
|
||||
private const val DROID_IFY_URL = "https://github.com/Iamlooker/Droid-ify"
|
||||
private const val DROID_IFY_URL = "https://github.com/Droid-ify/client"
|
||||
}
|
||||
|
||||
private val viewModel: SettingsViewModel by viewModels()
|
||||
@@ -114,7 +117,7 @@ class SettingsFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = SettingsPageBinding.inflate(inflater, container, false)
|
||||
binding.nestedScrollView.systemBarsPadding()
|
||||
@@ -128,42 +131,42 @@ class SettingsFragment : Fragment() {
|
||||
dynamicTheme.connect(
|
||||
titleText = getString(R.string.material_you),
|
||||
contentText = getString(R.string.material_you_desc),
|
||||
setting = viewModel.getInitialSetting { dynamicTheme }
|
||||
setting = viewModel.getInitialSetting { dynamicTheme },
|
||||
)
|
||||
homeScreenSwiping.connect(
|
||||
titleText = getString(R.string.home_screen_swiping),
|
||||
contentText = getString(R.string.home_screen_swiping_DESC),
|
||||
setting = viewModel.getInitialSetting { homeScreenSwiping }
|
||||
setting = viewModel.getInitialSetting { homeScreenSwiping },
|
||||
)
|
||||
autoUpdate.connect(
|
||||
titleText = getString(R.string.auto_update),
|
||||
contentText = getString(R.string.auto_update_apps),
|
||||
setting = viewModel.getInitialSetting { autoUpdate }
|
||||
setting = viewModel.getInitialSetting { autoUpdate },
|
||||
)
|
||||
notifyUpdates.connect(
|
||||
titleText = getString(R.string.notify_about_updates),
|
||||
contentText = getString(R.string.notify_about_updates_summary),
|
||||
setting = viewModel.getInitialSetting { notifyUpdate }
|
||||
setting = viewModel.getInitialSetting { notifyUpdate },
|
||||
)
|
||||
unstableUpdates.connect(
|
||||
titleText = getString(R.string.unstable_updates),
|
||||
contentText = getString(R.string.unstable_updates_summary),
|
||||
setting = viewModel.getInitialSetting { unstableUpdate }
|
||||
setting = viewModel.getInitialSetting { unstableUpdate },
|
||||
)
|
||||
ignoreSignature.connect(
|
||||
titleText = getString(R.string.ignore_signature),
|
||||
contentText = getString(R.string.ignore_signature_summary),
|
||||
setting = viewModel.getInitialSetting { ignoreSignature }
|
||||
setting = viewModel.getInitialSetting { ignoreSignature },
|
||||
)
|
||||
incompatibleUpdates.connect(
|
||||
titleText = getString(R.string.incompatible_versions),
|
||||
contentText = getString(R.string.incompatible_versions_summary),
|
||||
setting = viewModel.getInitialSetting { incompatibleVersions }
|
||||
setting = viewModel.getInitialSetting { incompatibleVersions },
|
||||
)
|
||||
language.connect(
|
||||
titleText = getString(R.string.prefs_language_title),
|
||||
map = { translateLocale(getLocaleOfCode(it)) },
|
||||
setting = viewModel.getSetting { language }
|
||||
setting = viewModel.getSetting { language },
|
||||
) { selectedLocale, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = selectedLocale,
|
||||
@@ -171,13 +174,13 @@ class SettingsFragment : Fragment() {
|
||||
title = R.string.prefs_language_title,
|
||||
iconRes = R.drawable.ic_language,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setLanguage
|
||||
onClick = viewModel::setLanguage,
|
||||
)
|
||||
}
|
||||
theme.connect(
|
||||
titleText = getString(R.string.theme),
|
||||
setting = viewModel.getSetting { theme },
|
||||
map = { themeName(it) }
|
||||
map = { themeName(it) },
|
||||
) { theme, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = theme,
|
||||
@@ -185,13 +188,13 @@ class SettingsFragment : Fragment() {
|
||||
title = R.string.themes,
|
||||
iconRes = R.drawable.ic_themes,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setTheme
|
||||
onClick = viewModel::setTheme,
|
||||
)
|
||||
}
|
||||
cleanUp.connect(
|
||||
titleText = getString(R.string.cleanup_title),
|
||||
setting = viewModel.getSetting { cleanUpInterval },
|
||||
map = { toTime(it) }
|
||||
map = { toTime(it) },
|
||||
) { duration, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = duration,
|
||||
@@ -199,13 +202,13 @@ class SettingsFragment : Fragment() {
|
||||
title = R.string.cleanup_title,
|
||||
iconRes = R.drawable.ic_time,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setCleanUpInterval
|
||||
onClick = viewModel::setCleanUpInterval,
|
||||
)
|
||||
}
|
||||
autoSync.connect(
|
||||
titleText = getString(R.string.sync_repositories_automatically),
|
||||
setting = viewModel.getSetting { autoSync },
|
||||
map = { autoSyncName(it) }
|
||||
map = { autoSyncName(it) },
|
||||
) { autoSync, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = autoSync,
|
||||
@@ -213,13 +216,13 @@ class SettingsFragment : Fragment() {
|
||||
title = R.string.sync_repositories_automatically,
|
||||
iconRes = R.drawable.ic_sync_type,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setAutoSync
|
||||
onClick = viewModel::setAutoSync,
|
||||
)
|
||||
}
|
||||
installer.connect(
|
||||
titleText = getString(R.string.installer),
|
||||
setting = viewModel.getSetting { installerType },
|
||||
map = { installerName(it) }
|
||||
map = { installerName(it) },
|
||||
) { installerType, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = installerType,
|
||||
@@ -227,13 +230,68 @@ class SettingsFragment : Fragment() {
|
||||
title = R.string.installer,
|
||||
iconRes = R.drawable.ic_apk_install,
|
||||
valueToString = valueToString,
|
||||
onClick = { viewModel.setInstaller(requireContext(), it) }
|
||||
onClick = { viewModel.setInstaller(requireContext(), it) },
|
||||
)
|
||||
}
|
||||
val pm = requireContext().packageManager
|
||||
legacyInstallerComponent.connect(
|
||||
titleText = getString(R.string.legacyInstallerComponent),
|
||||
setting = viewModel.getSetting { legacyInstallerComponent },
|
||||
map = {
|
||||
when (it) {
|
||||
is LegacyInstallerComponent.Component -> {
|
||||
val component = it
|
||||
val appLabel = runCatching {
|
||||
val info = pm.getApplicationInfo(component.clazz, 0)
|
||||
pm.getApplicationLabel(info).toString()
|
||||
}.getOrElse { component.clazz }
|
||||
"$appLabel (${component.activity})"
|
||||
}
|
||||
|
||||
LegacyInstallerComponent.Unspecified -> getString(R.string.unspecified)
|
||||
LegacyInstallerComponent.AlwaysChoose -> getString(R.string.always_choose)
|
||||
null -> getString(R.string.unspecified)
|
||||
}
|
||||
},
|
||||
) { component, valueToString ->
|
||||
val installerOptions = run {
|
||||
var contentProtocol = "content://"
|
||||
val intent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
|
||||
setDataAndType(
|
||||
contentProtocol.toUri(),
|
||||
"application/vnd.android.package-archive",
|
||||
)
|
||||
}
|
||||
val activities =
|
||||
pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
listOf(
|
||||
LegacyInstallerComponent.Unspecified,
|
||||
LegacyInstallerComponent.AlwaysChoose,
|
||||
) + activities.map {
|
||||
LegacyInstallerComponent.Component(
|
||||
clazz = it.activityInfo.packageName,
|
||||
activity = it.activityInfo.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
addSingleCorrectDialog(
|
||||
initialValue = component ?: LegacyInstallerComponent.Unspecified,
|
||||
values = installerOptions,
|
||||
title = R.string.legacyInstallerComponent,
|
||||
iconRes = R.drawable.ic_apk_install,
|
||||
valueToString = valueToString,
|
||||
onClick = { viewModel.setLegacyInstallerComponentComponent(it) },
|
||||
)
|
||||
}
|
||||
incompatibleUpdates.connect(
|
||||
titleText = getString(R.string.incompatible_versions),
|
||||
contentText = getString(R.string.incompatible_versions_summary),
|
||||
setting = viewModel.getInitialSetting { incompatibleVersions },
|
||||
)
|
||||
proxyType.connect(
|
||||
titleText = getString(R.string.proxy_type),
|
||||
setting = viewModel.getSetting { proxy.type },
|
||||
map = { proxyName(it) }
|
||||
map = { proxyName(it) },
|
||||
) { proxyType, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = proxyType,
|
||||
@@ -241,29 +299,29 @@ class SettingsFragment : Fragment() {
|
||||
title = R.string.proxy_type,
|
||||
iconRes = R.drawable.ic_proxy,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setProxyType
|
||||
onClick = viewModel::setProxyType,
|
||||
)
|
||||
}
|
||||
proxyHost.connect(
|
||||
titleText = getString(R.string.proxy_host),
|
||||
setting = viewModel.getSetting { proxy.host },
|
||||
map = { it }
|
||||
map = { it },
|
||||
) { host, _ ->
|
||||
addEditTextDialog(
|
||||
initialValue = host,
|
||||
title = R.string.proxy_host,
|
||||
onFinish = viewModel::setProxyHost
|
||||
onFinish = viewModel::setProxyHost,
|
||||
)
|
||||
}
|
||||
proxyPort.connect(
|
||||
titleText = getString(R.string.proxy_port),
|
||||
setting = viewModel.getSetting { proxy.port },
|
||||
map = { it.toString() }
|
||||
map = { it.toString() },
|
||||
) { port, _ ->
|
||||
addEditTextDialog(
|
||||
initialValue = port.toString(),
|
||||
title = R.string.proxy_port,
|
||||
onFinish = viewModel::setProxyPort
|
||||
onFinish = viewModel::setProxyPort,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -287,15 +345,15 @@ class SettingsFragment : Fragment() {
|
||||
allowBackgroundWork.root.setBackgroundColor(
|
||||
requireContext()
|
||||
.getColorFromAttr(MaterialR.attr.colorErrorContainer)
|
||||
.defaultColor
|
||||
.defaultColor,
|
||||
)
|
||||
allowBackgroundWork.title.setTextColor(
|
||||
requireContext()
|
||||
.getColorFromAttr(MaterialR.attr.colorOnErrorContainer)
|
||||
.getColorFromAttr(MaterialR.attr.colorOnErrorContainer),
|
||||
)
|
||||
allowBackgroundWork.content.setTextColor(
|
||||
requireContext()
|
||||
.getColorFromAttr(MaterialR.attr.colorOnErrorContainer)
|
||||
.getColorFromAttr(MaterialR.attr.colorOnErrorContainer),
|
||||
)
|
||||
creditFoxy.title.text = getString(R.string.special_credits)
|
||||
creditFoxy.content.text = FOXY_DROID_TITLE
|
||||
@@ -389,6 +447,9 @@ class SettingsFragment : Fragment() {
|
||||
proxyHost.root.isVisible = allowProxies
|
||||
proxyPort.root.isVisible = allowProxies
|
||||
forceCleanUp.root.isVisible = settings.cleanUpInterval == Duration.INFINITE
|
||||
|
||||
val useLegacyInstaller = settings.installerType == InstallerType.LEGACY
|
||||
legacyInstallerComponent.root.isVisible = useLegacyInstaller
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +465,7 @@ class SettingsFragment : Fragment() {
|
||||
(
|
||||
if (country?.isNotEmpty() == true && country.compareTo(
|
||||
language.toString(),
|
||||
true
|
||||
true,
|
||||
) != 0
|
||||
) {
|
||||
"($country)"
|
||||
@@ -437,12 +498,12 @@ class SettingsFragment : Fragment() {
|
||||
|
||||
localeCode.contains("-r") -> Locale(
|
||||
localeCode.substring(0, 2),
|
||||
localeCode.substring(4)
|
||||
localeCode.substring(4),
|
||||
)
|
||||
|
||||
localeCode.contains("_") -> Locale(
|
||||
localeCode.substring(0, 2),
|
||||
localeCode.substring(3)
|
||||
localeCode.substring(3),
|
||||
)
|
||||
|
||||
localeCode == "system" -> null
|
||||
@@ -453,7 +514,7 @@ class SettingsFragment : Fragment() {
|
||||
titleText: String,
|
||||
setting: Flow<T>,
|
||||
map: Context.(T) -> String,
|
||||
dialog: View.(T, valueToString: Context.(T) -> String) -> AlertDialog
|
||||
dialog: View.(T, valueToString: Context.(T) -> String) -> AlertDialog,
|
||||
) {
|
||||
title.text = titleText
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
@@ -473,7 +534,7 @@ class SettingsFragment : Fragment() {
|
||||
private fun SwitchTypeBinding.connect(
|
||||
titleText: String,
|
||||
contentText: String,
|
||||
setting: Flow<Boolean>
|
||||
setting: Flow<Boolean>,
|
||||
) {
|
||||
title.text = titleText
|
||||
content.text = contentText
|
||||
@@ -495,13 +556,13 @@ class SettingsFragment : Fragment() {
|
||||
@StringRes title: Int,
|
||||
@DrawableRes iconRes: Int,
|
||||
onClick: (T) -> Unit,
|
||||
valueToString: Context.(T) -> String
|
||||
valueToString: Context.(T) -> String,
|
||||
) = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setIcon(iconRes)
|
||||
.setSingleChoiceItems(
|
||||
values.map { context.valueToString(it) }.toTypedArray(),
|
||||
values.indexOf(initialValue)
|
||||
values.indexOf(initialValue),
|
||||
) { dialog, newValue ->
|
||||
dialog.dismiss()
|
||||
post {
|
||||
@@ -514,7 +575,7 @@ class SettingsFragment : Fragment() {
|
||||
private fun View.addEditTextDialog(
|
||||
initialValue: String,
|
||||
@StringRes title: Int,
|
||||
onFinish: (String) -> Unit
|
||||
onFinish: (String) -> Unit,
|
||||
): AlertDialog {
|
||||
val scroll = NestedScrollView(context)
|
||||
val customEditText = TextInputEditText(context)
|
||||
@@ -528,7 +589,7 @@ class SettingsFragment : Fragment() {
|
||||
scroll.addView(
|
||||
customEditText,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
return MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
@@ -540,7 +601,7 @@ class SettingsFragment : Fragment() {
|
||||
.create()
|
||||
.apply {
|
||||
window!!.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ import com.looker.droidify.datastore.model.AutoSync
|
||||
import com.looker.droidify.datastore.model.InstallerType
|
||||
import com.looker.droidify.datastore.model.InstallerType.ROOT
|
||||
import com.looker.droidify.datastore.model.InstallerType.SHIZUKU
|
||||
import com.looker.droidify.datastore.model.LegacyInstallerComponent
|
||||
import com.looker.droidify.datastore.model.ProxyType
|
||||
import com.looker.droidify.datastore.model.Theme
|
||||
import com.looker.droidify.installer.installers.initSui
|
||||
import com.looker.droidify.installer.installers.isMagiskGranted
|
||||
import com.looker.droidify.installer.installers.isShizukuAlive
|
||||
import com.looker.droidify.installer.installers.isShizukuGranted
|
||||
@@ -40,7 +42,7 @@ import kotlin.time.Duration
|
||||
class SettingsViewModel
|
||||
@Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val repositoryExporter: RepositoryExporter
|
||||
private val repositoryExporter: RepositoryExporter,
|
||||
) : ViewModel() {
|
||||
|
||||
private val initialSetting = flow {
|
||||
@@ -162,7 +164,7 @@ class SettingsViewModel
|
||||
viewModelScope.launch {
|
||||
when (installerType) {
|
||||
SHIZUKU -> {
|
||||
if (isShizukuInstalled(context)) {
|
||||
if (isShizukuInstalled(context) || initSui(context)) {
|
||||
if (!isShizukuAlive()) {
|
||||
createSnackbar(R.string.shizuku_not_alive)
|
||||
return@launch
|
||||
@@ -191,6 +193,12 @@ class SettingsViewModel
|
||||
}
|
||||
}
|
||||
|
||||
fun setLegacyInstallerComponentComponent(component: LegacyInstallerComponent?) {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.setLegacyInstallerComponent(component)
|
||||
}
|
||||
}
|
||||
|
||||
fun exportSettings(file: Uri) {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.export(file)
|
||||
@@ -227,12 +235,12 @@ class SettingsViewModel
|
||||
private fun String.toLocale(): Locale = when {
|
||||
contains("-r") -> Locale(
|
||||
substring(0, 2),
|
||||
substring(4)
|
||||
substring(4),
|
||||
)
|
||||
|
||||
contains("_") -> Locale(
|
||||
substring(0, 2),
|
||||
substring(3)
|
||||
substring(3),
|
||||
)
|
||||
|
||||
else -> Locale(this)
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.looker.droidify.R
|
||||
import com.looker.droidify.databinding.TabsToolbarBinding
|
||||
import com.looker.droidify.datastore.extension.sortOrderName
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.datastore.model.supportedSortOrders
|
||||
import com.looker.droidify.model.ProductItem
|
||||
import com.looker.droidify.service.Connection
|
||||
import com.looker.droidify.service.SyncService
|
||||
@@ -44,8 +45,8 @@ import com.looker.droidify.utility.common.extension.getMutatedIcon
|
||||
import com.looker.droidify.utility.common.extension.selectableBackground
|
||||
import com.looker.droidify.utility.common.extension.systemBarsPadding
|
||||
import com.looker.droidify.utility.common.sdkAbove
|
||||
import com.looker.droidify.utility.extension.resources.sizeScaled
|
||||
import com.looker.droidify.utility.extension.mainActivity
|
||||
import com.looker.droidify.utility.extension.resources.sizeScaled
|
||||
import com.looker.droidify.widget.DividerConfiguration
|
||||
import com.looker.droidify.widget.FocusSearchView
|
||||
import com.looker.droidify.widget.StableRecyclerAdapter
|
||||
@@ -121,7 +122,7 @@ class TabsFragment : ScreenFragment() {
|
||||
val source = AppListFragment.Source.entries[it.currentItem]
|
||||
updateUpdateNotificationBlocker(source)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private var sectionsAnimator: ValueAnimator? = null
|
||||
@@ -160,7 +161,8 @@ class TabsFragment : ScreenFragment() {
|
||||
val searchView = FocusSearchView(toolbar.context).apply {
|
||||
maxWidth = Int.MAX_VALUE
|
||||
queryHint = getString(stringRes.search)
|
||||
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
setOnQueryTextListener(
|
||||
object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
clearFocus()
|
||||
return true
|
||||
@@ -173,7 +175,8 @@ class TabsFragment : ScreenFragment() {
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
toolbar.menu.apply {
|
||||
@@ -187,9 +190,10 @@ class TabsFragment : ScreenFragment() {
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_search))
|
||||
.setActionView(searchView)
|
||||
.setShowAsActionFlags(
|
||||
MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
|
||||
MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW,
|
||||
)
|
||||
.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
.setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
viewModel.isSearchActionItemExpanded.value = true
|
||||
return true
|
||||
@@ -199,7 +203,8 @@ class TabsFragment : ScreenFragment() {
|
||||
viewModel.isSearchActionItemExpanded.value = false
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
syncRepositoriesMenuItem = add(0, 0, 0, stringRes.sync_repositories)
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sync))
|
||||
@@ -212,7 +217,7 @@ class TabsFragment : ScreenFragment() {
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sort))
|
||||
.let { menu ->
|
||||
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
val menuItems = SortOrder.entries.map { sortOrder ->
|
||||
val menuItems = supportedSortOrders().map { sortOrder ->
|
||||
menu.add(context.sortOrderName(sortOrder))
|
||||
.setOnMenuItemClickListener {
|
||||
viewModel.setSortOrder(sortOrder)
|
||||
@@ -224,9 +229,7 @@ class TabsFragment : ScreenFragment() {
|
||||
}
|
||||
|
||||
favouritesItem = add(1, 0, 0, stringRes.favourites)
|
||||
.setIcon(
|
||||
toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked)
|
||||
)
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked))
|
||||
.setOnMenuItemClickListener {
|
||||
view.post { mainActivity.navigateFavourites() }
|
||||
true
|
||||
@@ -262,7 +265,7 @@ class TabsFragment : ScreenFragment() {
|
||||
adapter = object : FragmentStateAdapter(this@TabsFragment) {
|
||||
override fun getItemCount(): Int = AppListFragment.Source.entries.size
|
||||
override fun createFragment(position: Int): Fragment = AppListFragment(
|
||||
AppListFragment.Source.entries[position]
|
||||
AppListFragment.Source.entries[position],
|
||||
)
|
||||
}
|
||||
content.addView(this)
|
||||
@@ -321,7 +324,7 @@ class TabsFragment : ScreenFragment() {
|
||||
|
||||
val backgroundPath = ShapeAppearanceModel.builder()
|
||||
.setAllCornerSizes(
|
||||
context?.resources?.getDimension(R.dimen.shape_large_corner) ?: 0F
|
||||
context?.resources?.getDimension(R.dimen.shape_large_corner) ?: 0F,
|
||||
)
|
||||
.build()
|
||||
val sectionBackground = MaterialShapeDrawable(backgroundPath)
|
||||
@@ -449,7 +452,7 @@ class TabsFragment : ScreenFragment() {
|
||||
val viewPager = viewPager
|
||||
viewPager?.setCurrentItem(
|
||||
AppListFragment.Source.UPDATES.ordinal,
|
||||
allowSmooth && viewPager.isLaidOut
|
||||
allowSmooth && viewPager.isLaidOut,
|
||||
)
|
||||
} else {
|
||||
needSelectUpdates = true
|
||||
@@ -461,7 +464,7 @@ class TabsFragment : ScreenFragment() {
|
||||
}
|
||||
|
||||
private fun updateSections(
|
||||
sectionsList: List<ProductItem.Section>
|
||||
sectionsList: List<ProductItem.Section>,
|
||||
) {
|
||||
sectionsAdapter?.sections = sectionsList
|
||||
layout?.run {
|
||||
@@ -518,7 +521,7 @@ class TabsFragment : ScreenFragment() {
|
||||
override fun onPageScrolled(
|
||||
position: Int,
|
||||
positionOffset: Float,
|
||||
positionOffsetPixels: Int
|
||||
positionOffsetPixels: Int,
|
||||
) {
|
||||
val layout = layout!!
|
||||
val fromSections = AppListFragment.Source.entries[position].sections
|
||||
@@ -547,15 +550,9 @@ class TabsFragment : ScreenFragment() {
|
||||
val source = AppListFragment.Source.entries[position]
|
||||
updateUpdateNotificationBlocker(source)
|
||||
sortOrderMenu!!.first.apply {
|
||||
isVisible = source.order
|
||||
setShowAsActionFlags(
|
||||
if (!source.order ||
|
||||
resources.configuration.screenWidthDp >= 300
|
||||
) {
|
||||
MenuItem.SHOW_AS_ACTION_ALWAYS
|
||||
} else {
|
||||
0
|
||||
}
|
||||
if (resources.configuration.screenWidthDp >= 300) MenuItem.SHOW_AS_ACTION_ALWAYS
|
||||
else 0,
|
||||
)
|
||||
}
|
||||
syncRepositoriesMenuItem!!.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
@@ -576,7 +573,7 @@ class TabsFragment : ScreenFragment() {
|
||||
}
|
||||
|
||||
private class SectionsAdapter(
|
||||
private val onClick: (ProductItem.Section) -> Unit
|
||||
private val onClick: (ProductItem.Section) -> Unit,
|
||||
) : StableRecyclerAdapter<SectionsAdapter.ViewType, RecyclerView.ViewHolder>() {
|
||||
enum class ViewType { SECTION }
|
||||
|
||||
@@ -590,13 +587,13 @@ class TabsFragment : ScreenFragment() {
|
||||
setPadding(16.dp, 0, 16.dp, 0)
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
}
|
||||
with(itemView as FrameLayout) {
|
||||
layoutParams = RecyclerView.LayoutParams(
|
||||
RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
48.dp
|
||||
48.dp,
|
||||
)
|
||||
background = context.selectableBackground
|
||||
addView(title)
|
||||
@@ -613,7 +610,7 @@ class TabsFragment : ScreenFragment() {
|
||||
fun configureDivider(
|
||||
context: Context,
|
||||
position: Int,
|
||||
configuration: DividerConfiguration
|
||||
configuration: DividerConfiguration,
|
||||
) {
|
||||
val currentSection = sections[position]
|
||||
val nextSection = sections.getOrNull(position + 1)
|
||||
@@ -624,7 +621,7 @@ class TabsFragment : ScreenFragment() {
|
||||
needDivider = true,
|
||||
toTop = false,
|
||||
paddingStart = padding,
|
||||
paddingEnd = padding
|
||||
paddingEnd = padding,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -633,7 +630,7 @@ class TabsFragment : ScreenFragment() {
|
||||
needDivider = false,
|
||||
toTop = false,
|
||||
paddingStart = 0,
|
||||
paddingEnd = 0
|
||||
paddingEnd = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -648,7 +645,7 @@ class TabsFragment : ScreenFragment() {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: ViewType
|
||||
viewType: ViewType,
|
||||
): RecyclerView.ViewHolder {
|
||||
return SectionViewHolder(parent.context).apply {
|
||||
itemView.setOnClickListener { onClick(sections[absoluteAdapterPosition]) }
|
||||
@@ -678,7 +675,7 @@ class TabsFragment : ScreenFragment() {
|
||||
}
|
||||
holder.title.text = when (section) {
|
||||
is ProductItem.Section.All -> holder.itemView.resources.getString(
|
||||
stringRes.all_applications
|
||||
stringRes.all_applications,
|
||||
)
|
||||
|
||||
is ProductItem.Section.Category -> section.name
|
||||
|
||||
@@ -20,7 +20,7 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class TabsViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
val currentSection =
|
||||
@@ -37,7 +37,7 @@ class TabsViewModel @Inject constructor(
|
||||
val sections =
|
||||
combine(
|
||||
Database.CategoryAdapter.getAllStream(),
|
||||
Database.RepositoryAdapter.getEnabledStream()
|
||||
Database.RepositoryAdapter.getEnabledStream(),
|
||||
) { categories, repos ->
|
||||
val productCategories = categories
|
||||
.asSequence()
|
||||
@@ -60,7 +60,7 @@ class TabsViewModel @Inject constructor(
|
||||
val backAction = combine(
|
||||
currentSection,
|
||||
isSearchActionItemExpanded,
|
||||
showSections
|
||||
showSections,
|
||||
) { currentSection, isSearchActionItemExpanded, showSections ->
|
||||
when {
|
||||
currentSection != ProductItem.Section.All -> BackAction.ProductAll
|
||||
@@ -80,6 +80,30 @@ class TabsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun calcBackAction(
|
||||
currentSection: ProductItem.Section,
|
||||
isSearchActionItemExpanded: Boolean,
|
||||
showSections: Boolean,
|
||||
): BackAction {
|
||||
return when {
|
||||
currentSection != ProductItem.Section.All -> {
|
||||
BackAction.ProductAll
|
||||
}
|
||||
|
||||
isSearchActionItemExpanded -> {
|
||||
BackAction.CollapseSearchView
|
||||
}
|
||||
|
||||
showSections -> {
|
||||
BackAction.HideSections
|
||||
}
|
||||
|
||||
else -> {
|
||||
BackAction.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STATE_SECTION = "section"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
package com.looker.droidify.utility.common.extension
|
||||
|
||||
inline fun <K, E> Map<K, E>.windowed(windowSize: Int, block: (Map<K, E>) -> Unit) {
|
||||
var index = 0
|
||||
val windowedPackages: HashMap<K, E> = HashMap(windowSize)
|
||||
forEach {
|
||||
index++
|
||||
windowedPackages.put(it.key, it.value)
|
||||
if (windowedPackages.size == windowSize || index == size) {
|
||||
block(windowedPackages)
|
||||
windowedPackages.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <K, E> Map<K, E>.updateAsMutable(block: MutableMap<K, E>.() -> Unit): Map<K, E> {
|
||||
return toMutableMap().apply(block)
|
||||
}
|
||||
|
||||
@@ -13,22 +13,22 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
context(ViewModel)
|
||||
context(viewModel: ViewModel)
|
||||
fun <T> Flow<T>.asStateFlow(
|
||||
initialValue: T,
|
||||
scope: CoroutineScope = viewModelScope,
|
||||
started: SharingStarted = SharingStarted.WhileSubscribed(5_000)
|
||||
scope: CoroutineScope = viewModel.viewModelScope,
|
||||
started: SharingStarted = SharingStarted.WhileSubscribed(5_000),
|
||||
): StateFlow<T> = stateIn(
|
||||
scope = scope,
|
||||
started = started,
|
||||
initialValue = initialValue
|
||||
initialValue = initialValue,
|
||||
)
|
||||
|
||||
context(CoroutineScope)
|
||||
context(scope: CoroutineScope)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T> ReceiveChannel<T>.filter(
|
||||
block: suspend (T) -> Boolean
|
||||
): ReceiveChannel<T> = produce(capacity = Channel.UNLIMITED) {
|
||||
block: suspend (T) -> Boolean,
|
||||
): ReceiveChannel<T> = scope.produce(capacity = Channel.UNLIMITED) {
|
||||
consumeEach { item ->
|
||||
if (block(item)) send(item)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ val Number.dpToPx
|
||||
Resources.getSystem().displayMetrics
|
||||
)
|
||||
|
||||
context(View)
|
||||
context(view: View)
|
||||
val Int.dp: Int
|
||||
get() = (this * resources.displayMetrics.density).roundToInt()
|
||||
get() = (this * view.resources.displayMetrics.density).roundToInt()
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/target_sdk"
|
||||
android:id="@+id/sdk_ver"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
|
||||
@@ -145,6 +145,9 @@
|
||||
<include
|
||||
android:id="@+id/installer"
|
||||
layout="@layout/enum_type" />
|
||||
<include
|
||||
android:id="@+id/legacy_installer_component"
|
||||
layout="@layout/enum_type" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -241,8 +241,13 @@
|
||||
<string name="label_unknown_sdk">غير معروف (%d)</string>
|
||||
<string name="error_shizuku_not_installed">Shizuku غير مثبت</string>
|
||||
<string name="error_shizuku_not_running_DESC">خدمة Shizuku لا تعمل. يُرجى التحقق من تطبيق Shizuku</string>
|
||||
<string name="label_targets_sdk">يستهدف: أندرويد %s</string>
|
||||
<string name="label_open_video">فيديو</string>
|
||||
<string name="error_shizuku_service_unavailable">Shizuku لا يعمل</string>
|
||||
<string name="switch_to_default_installer">بدّل إلى الافتراضي</string>
|
||||
<string name="unspecified">غير محدد</string>
|
||||
<string name="legacyInstallerComponent">مكون المثبت القديم</string>
|
||||
<string name="shizuku_legacy_installer">أداة تثبيت شيزوكو العتيقة</string>
|
||||
<string name="always_choose">اختر دائمًا</string>
|
||||
<string name="select_installer">اختيار المثبت</string>
|
||||
<string name="label_sdk_version">الأهداف: أندرويد %1$s |الحد الأدنى: أندرويد %2$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -235,7 +235,6 @@
|
||||
<string name="insufficient_storage">Не хапае месца</string>
|
||||
<string name="insufficient_storage_DESC">На вашай прыладзе не хапае памяці для ўсталявання гэтай праграмы. Паспрабуйце вызваліць крыху месца</string>
|
||||
<string name="open_shizuku">Адкрыць Shizuku</string>
|
||||
<string name="label_targets_sdk">Цэль: Android %s</string>
|
||||
<string name="switch_to_default_installer">Пераключыцца на стандартны</string>
|
||||
<string name="label_open_video">Відэа</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<string name="action_failed">Неуспешна операция</string>
|
||||
<string name="add_repository">Добави хранилище</string>
|
||||
<string name="address">Адрес</string>
|
||||
<string name="all_applications">Всички Приложения</string>
|
||||
<string name="all_applications">Всички приложения</string>
|
||||
<string name="all_applications_up_to_date">Всички приложения са актуални</string>
|
||||
<string name="already_exists">Вече съществува</string>
|
||||
<string name="always">Винаги</string>
|
||||
<string name="amoled">Чернa</string>
|
||||
<string name="application">Приложение</string>
|
||||
<string name="application_not_found">Приложението не може да бъде намерено</string>
|
||||
<string name="application_not_found">Приложението не беше намерено</string>
|
||||
<string name="author_email">Ел. поща на автора</string>
|
||||
<string name="available">Налични</string>
|
||||
<string name="bug_tracker">Тракер за грешки</string>
|
||||
@@ -21,7 +21,7 @@
|
||||
<string name="connecting">Свързване…</string>
|
||||
<string name="contains_non_free_media">Съдържа несвободна медия</string>
|
||||
<string name="could_not_sync_FORMAT">Неуспешно синхронизиране на %s</string>
|
||||
<string name="could_not_validate_FORMAT">Неуспешна валидиция на %s</string>
|
||||
<string name="could_not_validate_FORMAT">Неуспешно потвърждаване на %s</string>
|
||||
<string name="credits">Доброволци</string>
|
||||
<string name="delete">Изтрий</string>
|
||||
<string name="delete_repository_DESC">Изтриване на хранилището\?</string>
|
||||
@@ -31,25 +31,25 @@
|
||||
<string name="downloaded_FORMAT">Изтегли се %s</string>
|
||||
<string name="downloading">Изтегля се</string>
|
||||
<string name="downloading_FORMAT">Изтегля се %s…</string>
|
||||
<string name="edit_repository">Редактирай</string>
|
||||
<string name="has_advertising">Има реклами</string>
|
||||
<string name="has_security_vulnerabilities">Има уязвимости в сигурността</string>
|
||||
<string name="http_error_DESC">Невалиден отговор от сървъра.</string>
|
||||
<string name="edit_repository">Редактиране</string>
|
||||
<string name="has_advertising">Съдържа реклама</string>
|
||||
<string name="has_security_vulnerabilities">Има известни уязвимости в сигурността</string>
|
||||
<string name="http_error_DESC">Невалиден отговор от сървъра</string>
|
||||
<string name="http_proxy">HTTP прокси</string>
|
||||
<string name="ignore_all_updates">Игнорирай всички нови версии</string>
|
||||
<string name="incompatible_api_max_DESC_FORMAT">Максималната АПИ версия е %d.</string>
|
||||
<string name="incompatible_api_min_DESC_FORMAT">Минималната АПИ версия е %d.</string>
|
||||
<string name="incompatible_features_DESC">Липсващи функции.</string>
|
||||
<string name="incompatible_platforms_DESC_FORMAT">Вашата %1$s платформа не се поддържа. Поддържани платформи: %2$s.</string>
|
||||
<string name="ignore_all_updates">Игнориране на нови версии</string>
|
||||
<string name="incompatible_api_max_DESC_FORMAT">Максималната версия на API е %d</string>
|
||||
<string name="incompatible_api_min_DESC_FORMAT">Минималната версия на API е %d</string>
|
||||
<string name="incompatible_features_DESC">Липсващи функции</string>
|
||||
<string name="incompatible_platforms_DESC_FORMAT">Вашата %1$s архитектура не се поддържа. Поддържани архитектури: %2$s</string>
|
||||
<string name="incompatible_version">Несъвместима версия</string>
|
||||
<string name="incompatible_versions">Несъвместими версии</string>
|
||||
<string name="incompatible_with_FORMAT">Несъвместим с %s</string>
|
||||
<string name="install">Инсталирай</string>
|
||||
<string name="install_types">Начини на инсталиране</string>
|
||||
<string name="install_types">Инсталация</string>
|
||||
<string name="installed">Инсталирани</string>
|
||||
<string name="invalid_address">Невалиден адрес</string>
|
||||
<string name="invalid_permissions_error_DESC">Невалидни разрешения.</string>
|
||||
<string name="invalid_signature_error_DESC">Невалиден подпис.</string>
|
||||
<string name="invalid_permissions_error_DESC">Невалидни разрешения</string>
|
||||
<string name="invalid_signature_error_DESC">Невалиден подпис</string>
|
||||
<string name="launch">Стартирай</string>
|
||||
<string name="license">Лиценз</string>
|
||||
<string name="license_FORMAT">%s лиценз</string>
|
||||
@@ -65,10 +65,10 @@
|
||||
<string name="notify_about_updates">Уведомления за актуализации</string>
|
||||
<string name="number_of_applications">Брой приложения</string>
|
||||
<string name="ok">Окей</string>
|
||||
<string name="only_on_wifi">Само на Wi-Fi</string>
|
||||
<string name="only_on_wifi">Само при Wi-Fi</string>
|
||||
<string name="open_DESC_FORMAT">Отвори %s\?</string>
|
||||
<string name="other">Други</string>
|
||||
<string name="parsing_index_error_DESC">Не може да се прочете индекс файла.</string>
|
||||
<string name="parsing_index_error_DESC">Не можа да се анализира индексният файл</string>
|
||||
<string name="password">Парола</string>
|
||||
<string name="password_missing">Липсваща парола</string>
|
||||
<string name="proxy">Прокси</string>
|
||||
@@ -78,7 +78,7 @@
|
||||
<string name="recently_updated">Наскоро обновени</string>
|
||||
<string name="repositories">Хранилища</string>
|
||||
<string name="repository">Хранилище</string>
|
||||
<string name="repository_unsigned_DESC">Неподписано. Не може да провери списъка с неподписаните приложения. Внимавайте с тегленето на приложения от неподписани хранилища.</string>
|
||||
<string name="repository_unsigned_DESC">Не е намерен подпис. Не можа да се провери списъкът с приложения</string>
|
||||
<string name="requires_FORMAT">Изисква %s</string>
|
||||
<string name="save">Запази</string>
|
||||
<string name="saving_details">Запазване на подробности…</string>
|
||||
@@ -105,46 +105,46 @@
|
||||
<string name="version">Версия</string>
|
||||
<string name="versions">Версии</string>
|
||||
<string name="waiting_to_start_download">В очакване да започне изтеглянето…</string>
|
||||
<string name="whats_new">Какво е новото</string>
|
||||
<string name="whats_new">Какво е ново</string>
|
||||
<string name="show_less">Покажи по-малко</string>
|
||||
<string name="update_all">Инсталирай всички</string>
|
||||
<string name="anti_features">Антифункции</string>
|
||||
<string name="author_website">Уебстраница на автора</string>
|
||||
<string name="cant_edit_sync_DESC">Не може да се редактират синхронизиращи се хранилища.</string>
|
||||
<string name="cant_edit_sync_DESC">Не може да се редактират синхронизиращи се хранилища</string>
|
||||
<string name="could_not_download_FORMAT">Неуспешно изтегляне на %s</string>
|
||||
<string name="compiled_for_debugging">Компилирано за отстраняване на грешки</string>
|
||||
<string name="dark">Тъмнa</string>
|
||||
<string name="file_format_error_DESC">Невалиден файлов формат.</string>
|
||||
<string name="file_format_error_DESC">Невалиден файлов формат</string>
|
||||
<string name="fingerprint">Отпечатък</string>
|
||||
<string name="invalid_fingerprint_format">Невалиден формат на отпечатъка</string>
|
||||
<string name="has_non_free_dependencies">Има несвободни зависимости</string>
|
||||
<string name="has_non_free_dependencies">Зависи от други несвободни приложения</string>
|
||||
<string name="ignore_this_update">Игнорирай тази версия</string>
|
||||
<string name="incompatible_api_DESC_FORMAT">Вашата %1$s (АПИ версия %2$d) се поддържа. %3$s</string>
|
||||
<string name="incompatible_older_DESC">Тази версия е по-стара от инсталираната на вашето устройство. Деинсталирайте първо нея.</string>
|
||||
<string name="incompatible_older_DESC">Тази версия е по-стара от инсталираната на вашето устройство</string>
|
||||
<string name="invalid_username_format">Невалиден формат на потребителското име</string>
|
||||
<string name="unstable_updates">Нестабилни актуализации</string>
|
||||
<string name="incompatible_signature_DESC">Тази версия е подписана със сертификат, различен от този, инсталиран на вашето устройство. Деинсталирайте първо нея.</string>
|
||||
<string name="incompatible_signature_DESC">Тази версия е подписана с различен подпис от инсталирания на вашето устройство</string>
|
||||
<string name="incompatible_versions_summary">Показване на версии, несъвместими с устройството</string>
|
||||
<string name="integrity_check_error_DESC">Не може да се провери целостта.</string>
|
||||
<string name="invalid_metadata_error_DESC">Невалидни метаданни.</string>
|
||||
<string name="integrity_check_error_DESC">Не може да се провери целостта</string>
|
||||
<string name="invalid_metadata_error_DESC">Невалидни метаданни</string>
|
||||
<string name="plus_more_FORMAT">+%d повече</string>
|
||||
<string name="promotes_non_free_network_services">Насърчава несвободни мрежови услуги</string>
|
||||
<string name="promotes_non_free_network_services">Насърчава или зависи изцяло от несвободна мрежова услуга</string>
|
||||
<string name="search">Търсене</string>
|
||||
<string name="link_copied_to_clipboard">Линкът е копиран</string>
|
||||
<string name="new_updates_available">Налични нови версии на приложението</string>
|
||||
<string name="new_updates_available">Налични са нови актуализации</string>
|
||||
<string name="no_applications_available">Няма налични приложения</string>
|
||||
<plurals name="new_updates_DESC_FORMAT">
|
||||
<item quantity="one">%d приложение има нова версия.</item>
|
||||
<item quantity="other">%d приложения имат нова версия.</item>
|
||||
<item quantity="one">%d приложението има налични актуализации</item>
|
||||
<item quantity="other">%d приложенията имат налични актуализации</item>
|
||||
</plurals>
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="no_matching_applications_found">Не могат да бъдат намерени такива приложения</string>
|
||||
<string name="notify_about_updates_summary">Покажи известие, при налични нови версии</string>
|
||||
<string name="no_matching_applications_found">Няма резултати</string>
|
||||
<string name="notify_about_updates_summary">Показване на известие, когато е налична нова версия на приложение</string>
|
||||
<string name="only_compatible_with_FORMAT">Съвместим само с %s</string>
|
||||
<string name="permissions">Разрешения</string>
|
||||
<string name="processing_FORMAT">Обработка на %1$s…</string>
|
||||
<string name="project_website">Уебстраница на проекта</string>
|
||||
<string name="promotes_non_free_software">Насърчава несвободен софтуер</string>
|
||||
<string name="project_website">Уеб страница</string>
|
||||
<string name="promotes_non_free_software">Насърчава несвободни добавки</string>
|
||||
<string name="provided_by_FORMAT">Предоставено от %s</string>
|
||||
<string name="themes">Теми</string>
|
||||
<string name="uninstall">Деинсталиране</string>
|
||||
@@ -154,72 +154,88 @@
|
||||
<string name="version_FORMAT">Версия %s</string>
|
||||
<string name="sync_repositories">Синхронизирай хранилищата</string>
|
||||
<string name="system">Системна</string>
|
||||
<string name="tap_to_install_DESC">Докосни за инсталиране.</string>
|
||||
<string name="unknown_error_DESC">Неизвестна грешка.</string>
|
||||
<string name="unstable_updates_summary">Предложи инсталирането на нестабилни версии</string>
|
||||
<string name="upstream_source_code_is_not_free">Актуалният програмен код вече не е със свободен лиценз</string>
|
||||
<string name="tap_to_install_DESC">Докоснете за инсталиране</string>
|
||||
<string name="unknown_error_DESC">Неизвестна грешка</string>
|
||||
<string name="unstable_updates_summary">Предложи инсталиране на нестабилни версии на приложения</string>
|
||||
<string name="upstream_source_code_is_not_free">Изходният код нагоре по веригата е несвободен</string>
|
||||
<string name="username_missing">Потребителско име липсва</string>
|
||||
<string name="validation_index_error_DESC">Не може да валидира индексът.</string>
|
||||
<string name="website">Уебстраница</string>
|
||||
<string name="validation_index_error_DESC">Индексът не може да бъде потвърден</string>
|
||||
<string name="website">Уеб страница на проекта</string>
|
||||
<string name="prefs_language_title">Език</string>
|
||||
<string name="prefs_personalization">Персонализация</string>
|
||||
<string name="installer">Инсталатор</string>
|
||||
<string name="legacy_installer">Стар Инсталатор</string>
|
||||
<string name="session_installer">Session Инсталатор</string>
|
||||
<string name="root_installer">Root Инсталатор</string>
|
||||
<string name="shizuku_installer">Shizuku Инсталатор</string>
|
||||
<string name="legacy_installer">Наследен инсталатор</string>
|
||||
<string name="session_installer">Session инсталатор</string>
|
||||
<string name="root_installer">Root инсталатор</string>
|
||||
<string name="shizuku_installer">Shizuku/Sui инсталатор</string>
|
||||
<plurals name="days">
|
||||
<item quantity="one">Ден</item>
|
||||
<item quantity="other">Дни</item>
|
||||
<item quantity="one">ден</item>
|
||||
<item quantity="other">дни</item>
|
||||
</plurals>
|
||||
<string name="only_on_wifi_with_charging">Само при Wi-Fi и зареждане</string>
|
||||
<string name="cleanup_title">Интервал за почистване на APK</string>
|
||||
<plurals name="hours">
|
||||
<item quantity="one">Час</item>
|
||||
<item quantity="other">Часа</item>
|
||||
<item quantity="one">час</item>
|
||||
<item quantity="other">часа</item>
|
||||
</plurals>
|
||||
<string name="io_error_DESC">Невъзможност за извършване на определени действия.</string>
|
||||
<string name="material_you_desc">Използвайте material you цветова тема</string>
|
||||
<string name="io_error_DESC">Невъзможност за извършване на определени действия</string>
|
||||
<string name="material_you_desc">Използване на Material You цветове</string>
|
||||
<string name="material_you">Material You</string>
|
||||
<string name="auto_update">Автоматично актуализиране на приложения</string>
|
||||
<string name="installing">Инсталиране</string>
|
||||
<string name="auto_update_apps">Опитайте се да инсталирате актуализации автоматично</string>
|
||||
<string name="auto_update_apps">Автоматично инсталиране на актуализации</string>
|
||||
<string name="waiting_to_start_installation">Изчакване за стартиране на инсталацията…</string>
|
||||
<string name="favourites">Любими</string>
|
||||
<string name="enable_repo">Активирайте хранилището</string>
|
||||
<string name="enable_repo">Активиране на хранилище</string>
|
||||
<string name="force_clean_up">Принудително почистване</string>
|
||||
<string name="force_clean_up_DESC">Почиства излишните файлове</string>
|
||||
<string name="repository_unreachable">Хранилището е недостъпно</string>
|
||||
<string name="socket_error_DESC">Сървърът не успя да предостави нов пакет.</string>
|
||||
<string name="socket_error_DESC">Сървърът не успя да предостави нов пакет</string>
|
||||
<string name="has_non_free_components">Има несвободни компоненти</string>
|
||||
<string name="connection_error_DESC">Неуспешно свързване със сървъра</string>
|
||||
<string name="home_screen_swiping">Плъзгане на началния екран</string>
|
||||
<string name="contains_nsfw">Съдържа неподходящо за работа съдържание</string>
|
||||
<string name="shizuku_not_alive">Shizuku не работи</string>
|
||||
<string name="home_screen_swiping">Плъзгане на страница</string>
|
||||
<string name="contains_nsfw">Съдържа неподходящо за работа (NSFW) съдържание</string>
|
||||
<string name="shizuku_not_alive">Shizuku/Sui не работи</string>
|
||||
<string name="proxy_port_error_not_int">Прокси портът може да бъде само цяло число</string>
|
||||
<string name="home_screen_swiping_DESC">Позволете на потребителя да плъзга между страниците в началния екран</string>
|
||||
<string name="repository_not_found">Следното хранилище не бе намерено</string>
|
||||
<string name="home_screen_swiping_DESC">Плъзнете наляво или надясно, за да превключите страницата</string>
|
||||
<string name="repository_not_found">Хранилището не беше намерено</string>
|
||||
<string name="special_credits">Специални благодарности</string>
|
||||
<string name="shizuku_not_installed">Shizuku не е инсталиран</string>
|
||||
<string name="import_export">Внасяне/Изнасяне</string>
|
||||
<string name="import_settings_title">Внеси Настройки</string>
|
||||
<string name="import_settings_DESC">Внасяне на настройки и любими от файл</string>
|
||||
<string name="export_settings_title">Изнеси Настройки</string>
|
||||
<string name="export_repos_DESC">Изнеси всички хранилища във файл</string>
|
||||
<string name="import_repos_title">Внеси хранилища</string>
|
||||
<string name="export_settings_DESC">Изнасяне на настройки и любими във файл</string>
|
||||
<string name="export_repos_title">Изнеси хранилища</string>
|
||||
<string name="import_export">Архивиране и възстановяване</string>
|
||||
<string name="import_settings_title">Внасяне на предпочитания и настройки</string>
|
||||
<string name="import_settings_DESC">Внасяне на настройки и предпочитания от файл</string>
|
||||
<string name="export_settings_title">Изнасяне на предпочитания и настройки</string>
|
||||
<string name="export_repos_DESC">Изнасяне на хранилища във файл</string>
|
||||
<string name="import_repos_title">Внасяне на хранилища</string>
|
||||
<string name="export_settings_DESC">Изнасяне на настройки и предпочитания във файл</string>
|
||||
<string name="export_repos_title">Изнасяне на хранилища</string>
|
||||
<string name="cannot_open_link">Линкът не може да се отвори</string>
|
||||
<string name="import_repos_DESC">Внеси всички хранилища от файл</string>
|
||||
<string name="has_tethered_network">Обвързан с определена мрежова услуга</string>
|
||||
<string name="require_background_access">Изискване на фонов достъп</string>
|
||||
<string name="require_background_access_DESC">Необходим е фонов достъп, за да стартирате правилно фоновото синхронизиране</string>
|
||||
<string name="import_repos_DESC">Внасяне на хранилища от файл</string>
|
||||
<string name="has_tethered_network">Зависи от определена инстанция на мрежова услуга</string>
|
||||
<string name="require_background_access">Деактивиране на оптимизациите на батерията</string>
|
||||
<string name="require_background_access_DESC">Деактивирането на оптимизациите на батерията е необходимо, за да може фоновата синхронизация да работи правилно</string>
|
||||
<string name="installation_failed_DESC">Неуспешно инсталиране на %s</string>
|
||||
<string name="uninstalled_application">Деинсталирано</string>
|
||||
<string name="uninstalled_application_DESC">%s е било деинсталирано</string>
|
||||
<string name="installation_failed">Неуспешна инсталация</string>
|
||||
<string name="ignore_signature">Пренебрегване на подписа</string>
|
||||
<string name="ignore_signature_summary">*Внимание* Игнорирайте проверката на подписа при инсталиране на APK за LSPosed потребители или напреднали потребители</string>
|
||||
<string name="ignore_signature">Игнориране проверката на подписа</string>
|
||||
<string name="ignore_signature_summary">Игнориране проверката на подписа при инсталиране на приложение</string>
|
||||
<string name="insufficient_storage">Недостатъчно място</string>
|
||||
<string name="insufficient_storage_DESC">Няма достатъчно свободно място на устройството за инсталиране на това приложение. Опитайте да освободите малко място</string>
|
||||
<string name="insufficient_storage_DESC">Няма достатъчно свободно място за инсталиране на това приложение</string>
|
||||
<string name="error_shizuku_not_installed">Shizuku/Sui не е инсталиран</string>
|
||||
<string name="label_open_video">Видео</string>
|
||||
<string name="switch_to_default_installer">Превключване към по подразбиране</string>
|
||||
<string name="open_shizuku">Отворете Shizuku</string>
|
||||
<string name="error_shizuku_not_running_DESC">Услугата Shizuku/Sui не работи</string>
|
||||
<string name="error_shizuku_not_granted">Не е предоставено разрешение за Shizuku/Sui</string>
|
||||
<string name="error_shizuku_not_granted_DESC">Разрешението за Shizuku/Sui не е предоставено</string>
|
||||
<string name="error_shizuku_not_installed_DESC">Shizuku/Sui не е инсталирана</string>
|
||||
<string name="error_shizuku_service_unavailable">Shizuku/Sui услугата не работи</string>
|
||||
<string name="label_unknown_sdk">Неизвестен (%d)</string>
|
||||
<string name="legacyInstallerComponent">Компонент на наследен инсталатор</string>
|
||||
<string name="unspecified">Неопределен</string>
|
||||
<string name="shizuku_legacy_installer">Shizuku/Sui наследен инсталатор</string>
|
||||
<string name="always_choose">Винаги избирай</string>
|
||||
<string name="select_installer">Изберете инсталатор</string>
|
||||
<string name="label_sdk_version">Цели: Android %1$s | Минимум: Android %2$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -193,4 +193,43 @@
|
||||
<string name="cannot_open_link">লিংকটি ওপেন করা সম্ভব হয়নি</string>
|
||||
<string name="import_export">ইম্পোর্ট/এক্সপোর্ট</string>
|
||||
<string name="import_settings_title">সেটিংস ইম্পোর্ট করুন</string>
|
||||
<string name="uninstalled_application">অপসারিত</string>
|
||||
<string name="special_credits">বিশেষ কৃতজ্ঞতাস্বীকার</string>
|
||||
<string name="error_shizuku_not_granted_DESC">শিজুকু সেবার অনুমতি দেওয়া হয়নি। শিজুকু অ্যাপটি দেখো</string>
|
||||
<string name="ignore_signature_summary">*সতর্কতা* এপিকে ইন্সটলে সময় স্বাক্ষর যাচাইকরণ এড়াও, এলএসপোজড ব্যবহারকারী বা অগ্রগামী ব্যবহারকারীর জন্য</string>
|
||||
<string name="contains_nsfw">নিষিদ্ধ বিষয়বস্ত বিদ্যমান</string>
|
||||
<string name="label_open_video">ভিডিও</string>
|
||||
<string name="export_settings_title">পছন্দসমূহ রপ্তানি করো</string>
|
||||
<string name="insufficient_storage">অপর্যাপ্ত জায়গা</string>
|
||||
<string name="shizuku_not_alive">শিজুকু চলছে না</string>
|
||||
<string name="error_shizuku_not_running_DESC">শিজুকু চলছে না। অনুগ্রহ করে শিজুকু অ্যাপ দেখো</string>
|
||||
<string name="export_repos_DESC">সব ভাণ্ডার ফাইলে রপ্তানি</string>
|
||||
<string name="home_screen_swiping_DESC">ব্যবহারকারীকে মূলপাতাসমূহের মধ্যে টান দেওয়া অনুমোদন করো</string>
|
||||
<string name="connection_error_DESC">সার্ভারে যুক্ত হতে ব্যর্থ</string>
|
||||
<string name="shizuku_not_installed">শিজুকু ইন্সটল করা নেই</string>
|
||||
<string name="installation_failed">ইন্সটল ব্যর্থ</string>
|
||||
<string name="installation_failed_DESC">%s ইন্সটল করতে ব্যর্থ</string>
|
||||
<string name="ignore_signature">স্বাক্ষর উপেক্ষা করো</string>
|
||||
<string name="error_shizuku_service_unavailable">শিজুকু চলছে না</string>
|
||||
<string name="error_shizuku_not_granted">শিজুকু অনুমতি নেই</string>
|
||||
<string name="error_shizuku_not_installed">শিজুকু ইন্সটল করা হয়নি</string>
|
||||
<string name="error_shizuku_not_installed_DESC">শিজুকু মনে হচ্ছে না ইন্সটল করা</string>
|
||||
<string name="import_settings_DESC">পছন্দসমূহ ফাইল থেকে আমদানি করো</string>
|
||||
<string name="export_settings_DESC">পছন্দসমূহ ফাইলে রপ্তানি করো</string>
|
||||
<string name="import_repos_title">ভাণ্ডার আমদানি করো</string>
|
||||
<string name="import_repos_DESC">ফাইল থেকে সব ভাণ্ডার আমদানি করো</string>
|
||||
<string name="export_repos_title">ভাণ্ডার রপ্তানি</string>
|
||||
<string name="has_non_free_components">অমুক্ত অংশ আছে</string>
|
||||
<string name="has_tethered_network">একটি নির্দিষ্ট নেটওয়ার্ক সেবার সাথে আবদ্ধ</string>
|
||||
<string name="home_screen_swiping">মূলপাতা টান দেওয়া(সোয়াইপিং)</string>
|
||||
<string name="socket_error_DESC">সার্ভার নতুন প্যাকেট সরবরাহে ব্যর্থ।</string>
|
||||
<string name="uninstalled_application_DESC">%s অপসারিত হয়েছে</string>
|
||||
<string name="insufficient_storage_DESC">এই অ্যাপটি ইন্সটল করার জন্য পর্যাপ্ত জায়গা নেই। কিছু জায়গা পরিষ্কারের চেষ্টা করো</string>
|
||||
<string name="open_shizuku">শিজুকু খুলো</string>
|
||||
<string name="proxy_port_error_not_int">প্রক্সি পোর্ট শুধু পূর্ণ ধনাত্মক সংখ্যা হতে পারে</string>
|
||||
<string name="require_background_access">পটভূমিতে চলার অনুমতি প্রয়োজন</string>
|
||||
<string name="require_background_access_DESC">পটভূমি অনুমোদন প্রয়োজন পটভূমি সিঙ্কের জন্য</string>
|
||||
<string name="repository_not_found">এই ভাণ্ডারগুলো পাওয়া যায়নি</string>
|
||||
<string name="switch_to_default_installer">সহজাতে পরিবর্তন করো</string>
|
||||
<string name="label_unknown_sdk">অজানা (%d)</string>
|
||||
</resources>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<string name="has_security_vulnerabilities">Té vulnerabilitats de seguretat</string>
|
||||
<plurals name="hours">
|
||||
<item quantity="one">Hora</item>
|
||||
<item quantity="many"></item>
|
||||
<item quantity="other">Hores</item>
|
||||
</plurals>
|
||||
<string name="http_error_DESC">Resposta de servidor nul.</string>
|
||||
@@ -168,6 +169,7 @@
|
||||
<string name="never">Mai</string>
|
||||
<plurals name="new_updates_DESC_FORMAT">
|
||||
<item quantity="one">%d l\'aplicació té una versió nova.</item>
|
||||
<item quantity="many"></item>
|
||||
<item quantity="other">%d aplicacions amb versions noves.</item>
|
||||
</plurals>
|
||||
<string name="no_applications_available">Cap aplicació disponible</string>
|
||||
|
||||
8
app/src/main/res/values-ckb/strings.xml
Normal file
8
app/src/main/res/values-ckb/strings.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="action_failed">کردارەکە شکستیهێنا</string>
|
||||
<string name="amoled">ڕەش</string>
|
||||
<string name="dark">تاریک</string>
|
||||
<string name="address">ناونیشان</string>
|
||||
<string name="always">هەمووکات</string>
|
||||
</resources>
|
||||
@@ -1,37 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="add_repository">Přidat zdroj</string>
|
||||
<string name="add_repository">Přidat repozitář</string>
|
||||
<string name="address">Adresa</string>
|
||||
<string name="all_applications_up_to_date">Všechny vaše aplikace jsou aktuální</string>
|
||||
<string name="application_not_found">Nezdařilo se najít tuto aplikaci</string>
|
||||
<string name="application_not_found">Aplikace nebyla nalezena</string>
|
||||
<string name="available">Procházet</string>
|
||||
<string name="bug_tracker">Sledování chyb</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="installed">Instalováno</string>
|
||||
<string name="incompatible_older_DESC">Tato verze je starší než ta instalovaná na vašem zařízení. Nejdříve odinstalujte ji.</string>
|
||||
<string name="incompatible_versions_summary">Zobrait nekompatibilní verze aplikace s vaším zařízením</string>
|
||||
<string name="invalid_signature_error_DESC">Neplatný podpis.</string>
|
||||
<string name="incompatible_older_DESC">Tato verze je starší než ta nainstalovaná na vašem zařízení</string>
|
||||
<string name="incompatible_versions_summary">Zobrait verze aplikace nekompatibilní s vaším zařízením</string>
|
||||
<string name="invalid_signature_error_DESC">Neplatný podpis</string>
|
||||
<string name="link_copied_to_clipboard">Odkaz zkopírován</string>
|
||||
<string name="light">Světlé</string>
|
||||
<string name="number_of_applications">Počet aplikací</string>
|
||||
<string name="processing_FORMAT">Zpracovávání %1$s…</string>
|
||||
<string name="parsing_index_error_DESC">Nezdařilo se zpracovat soubor indexu.</string>
|
||||
<string name="parsing_index_error_DESC">Nezdařilo se zpracovat soubor indexu</string>
|
||||
<string name="password">Heslo</string>
|
||||
<string name="password_missing">Chybí heslo</string>
|
||||
<string name="project_website">Web projektu</string>
|
||||
<string name="project_website">Webové stránky</string>
|
||||
<string name="proxy_host">Hostitel proxy</string>
|
||||
<string name="recently_updated">Nedávno aktualizované</string>
|
||||
<string name="repository">Zdroj</string>
|
||||
<string name="repository">Repozitář</string>
|
||||
<string name="proxy_port">Port proxy</string>
|
||||
<string name="proxy_type">Typ proxy</string>
|
||||
<string name="repositories">Zdroje</string>
|
||||
<string name="anti_features">Anti-funkce</string>
|
||||
<string name="already_exists">Již existuje</string>
|
||||
<string name="always">Vždy</string>
|
||||
<string name="cant_edit_sync_DESC">Nezdařilo se upravit zdroj protože se právě synchronizuje.</string>
|
||||
<string name="cant_edit_sync_DESC">Repozitář nemůžete upravit, protože se právě synchronizuje</string>
|
||||
<string name="changes">Změny</string>
|
||||
<string name="changelog">Seznam změn</string>
|
||||
<string name="checking_repository">Kontroluji zdroj…</string>
|
||||
<string name="checking_repository">Kontroluji repozitář…</string>
|
||||
<string name="confirmation">Potvrzení</string>
|
||||
<string name="connecting">Spojuji…</string>
|
||||
<string name="contains_non_free_media">Obsahuje ne-svobodná média</string>
|
||||
@@ -39,40 +39,40 @@
|
||||
<string name="could_not_sync_FORMAT">Nepodařilo se synchronizovat %s</string>
|
||||
<string name="could_not_validate_FORMAT">Nepodařilo se ověřit %s</string>
|
||||
<string name="dark">Tmavé</string>
|
||||
<string name="delete">Smazat</string>
|
||||
<string name="delete_repository_DESC">Smazat zdroj\?</string>
|
||||
<string name="delete">Odstranit</string>
|
||||
<string name="delete_repository_DESC">Odstranit repozitář?</string>
|
||||
<string name="description">Popis</string>
|
||||
<string name="details">Detaily</string>
|
||||
<string name="donate">Přispět</string>
|
||||
<string name="downloaded_FORMAT">Staženo %s</string>
|
||||
<string name="downloading">Stahuji</string>
|
||||
<string name="downloading_FORMAT">Stahuji %s…</string>
|
||||
<string name="edit_repository">Upravit zdroj</string>
|
||||
<string name="file_format_error_DESC">Neplatný formát souboru.</string>
|
||||
<string name="edit_repository">Upravit</string>
|
||||
<string name="file_format_error_DESC">Neplatný formát souboru</string>
|
||||
<string name="fingerprint">Otisk prstu</string>
|
||||
<string name="has_advertising">Obsahuje reklamy</string>
|
||||
<string name="has_non_free_dependencies">Obsahuje nesvobodné závislosti</string>
|
||||
<string name="has_security_vulnerabilities">Obsahuje bezpečnostní zranitelnosti</string>
|
||||
<string name="http_error_DESC">Neplatná odpověď serveru.</string>
|
||||
<string name="has_non_free_dependencies">Závisí na jiných nesvobodných aplikacích</string>
|
||||
<string name="has_security_vulnerabilities">Má známé zranitelnosti v zabezpečení</string>
|
||||
<string name="http_error_DESC">Neplatná odpověď serveru</string>
|
||||
<string name="http_proxy">HTTP proxy</string>
|
||||
<string name="ignore_all_updates">Ignorovat všechny nové verze</string>
|
||||
<string name="ignore_all_updates">Ignorovat nové verze</string>
|
||||
<string name="ignore_this_update">Ignorovat tuto verzi</string>
|
||||
<string name="incompatible_api_DESC_FORMAT">Váš %1$s (verze API %2$d) není podporován. %3$s</string>
|
||||
<string name="incompatible_api_max_DESC_FORMAT">Maximální verze API je %d.</string>
|
||||
<string name="incompatible_api_min_DESC_FORMAT">Minimální verze API je %d.</string>
|
||||
<string name="incompatible_api_max_DESC_FORMAT">Maximální verze API je %d</string>
|
||||
<string name="incompatible_api_min_DESC_FORMAT">Minimální verze API je %d</string>
|
||||
<string name="credits">Kredity</string>
|
||||
<string name="incompatible_features_DESC">Chybějící funkce.</string>
|
||||
<string name="incompatible_platforms_DESC_FORMAT">Vaše %1$s platforma není podporována. Podporované platformy: %2$s.</string>
|
||||
<string name="incompatible_features_DESC">Chybějící funkce</string>
|
||||
<string name="incompatible_platforms_DESC_FORMAT">Vaše architektura %1$s není podporována. Podporované architektury: %2$s</string>
|
||||
<string name="incompatible_version">Nekompatibilní verze</string>
|
||||
<string name="incompatible_versions">Nekompatibilní verze</string>
|
||||
<string name="incompatible_with_FORMAT">Nekompatibilní s %s</string>
|
||||
<string name="install">Instalovat</string>
|
||||
<string name="install_types">Typy Instalace</string>
|
||||
<string name="integrity_check_error_DESC">Nezdařilo se zkontrolovat integritu.</string>
|
||||
<string name="install_types">Instalace</string>
|
||||
<string name="integrity_check_error_DESC">Nezdařilo se zkontrolovat integritu</string>
|
||||
<string name="invalid_address">Neplatná adresa</string>
|
||||
<string name="invalid_fingerprint_format">Neplatný formát otisku prstu</string>
|
||||
<string name="invalid_metadata_error_DESC">Neplatná metadata.</string>
|
||||
<string name="invalid_permissions_error_DESC">Neplatná oprávnění.</string>
|
||||
<string name="invalid_metadata_error_DESC">Neplatná metadata</string>
|
||||
<string name="invalid_permissions_error_DESC">Neplatná oprávnění</string>
|
||||
<string name="invalid_username_format">Neplatný formát uživatelského jména</string>
|
||||
<string name="launch">Spustit</string>
|
||||
<string name="license">Licence</string>
|
||||
@@ -82,33 +82,33 @@
|
||||
<string name="name">Název</string>
|
||||
<string name="network_error_DESC">Chyba sítě</string>
|
||||
<string name="never">Nikdy</string>
|
||||
<string name="new_updates_available">Jsou dostupné nové verze aplikací</string>
|
||||
<string name="new_updates_available">Jsou dostupné nové aktualizace</string>
|
||||
<plurals name="new_updates_DESC_FORMAT">
|
||||
<item quantity="one">%d aplikace má novou verzi.</item>
|
||||
<item quantity="few">%d aplikace mají novou verzi.</item>
|
||||
<item quantity="other">%d aplikací má novou verzi.</item>
|
||||
<item quantity="one">%d aplikace má dostupné aktualizace</item>
|
||||
<item quantity="few">%d aplikace mají dostupné aktualizace</item>
|
||||
<item quantity="other">%d aplikací má dostupné aktualizace</item>
|
||||
</plurals>
|
||||
<string name="no_applications_available">Žádné dostupné aplikace</string>
|
||||
<string name="no_applications_installed">Žádné instalované aplikace</string>
|
||||
<string name="no_applications_installed">Žádné nainstalované aplikace</string>
|
||||
<string name="no_description_available_DESC">Žádný dostupný popis</string>
|
||||
<string name="no_matching_applications_found">Nepodařilo se najít žádné takové aplikace</string>
|
||||
<string name="no_matching_applications_found">Žádné výsledky</string>
|
||||
<string name="no_proxy">Žádná proxy</string>
|
||||
<string name="notify_about_updates">Oznámení o aktualizacích</string>
|
||||
<string name="notify_about_updates_summary">Zobrazit oznámení když jsou dostupné nové verze</string>
|
||||
<string name="notify_about_updates_summary">Zobrazit oznámení, když je dostupná nová verze aplikace</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="only_compatible_with_FORMAT">Kompatibilní pouze s %s</string>
|
||||
<string name="only_on_wifi">Pouze na Wi-Fi</string>
|
||||
<string name="open_DESC_FORMAT">Otevřít %s\?</string>
|
||||
<string name="other">Ostatní</string>
|
||||
<string name="incompatible_signature_DESC">Tato verze je podepsána jiným certifikátem než ta instalována na vašem zařízení. Nejdříve odisntalujte ji.</string>
|
||||
<string name="incompatible_signature_DESC">Tato verze je podepsána jiným podpisem než ta nainstalovaná na vašem zařízení</string>
|
||||
<string name="permissions">Oprávnění</string>
|
||||
<string name="plus_more_FORMAT">+%d více</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="promotes_non_free_network_services">Propaguje nesvobodné internetové služby</string>
|
||||
<string name="promotes_non_free_software">Propaguje ne-svobodný software</string>
|
||||
<string name="promotes_non_free_network_services">Propaguje nebo zcela závisí na nesvobodné síťové službě</string>
|
||||
<string name="promotes_non_free_software">Propaguje nesvobodné doplňky</string>
|
||||
<string name="provided_by_FORMAT">Poskytuje %s</string>
|
||||
<string name="proxy">Proxy</string>
|
||||
<string name="repository_unsigned_DESC">Nepodepsáno. Nezdařilo se ověřit seznam aplikací. Buďte opatrní při stahování aplikací z nepodepsaných zdrojů.</string>
|
||||
<string name="repository_unsigned_DESC">Nenalezen žádný podpis. Nepodařilo se ověřit seznam aplikací</string>
|
||||
<string name="requires_FORMAT">Vyžaduje %s</string>
|
||||
<string name="save">Uložit</string>
|
||||
<string name="saving_details">Ukládám detaily…</string>
|
||||
@@ -130,27 +130,27 @@
|
||||
<string name="syncing">Synchronizuji</string>
|
||||
<string name="syncing_FORMAT">Synchronizuji %s…</string>
|
||||
<string name="system">Systém</string>
|
||||
<string name="tap_to_install_DESC">Klikněte pro instalaci.</string>
|
||||
<string name="tap_to_install_DESC">Klikněte pro instalaci</string>
|
||||
<string name="theme">Téma</string>
|
||||
<string name="themes">Témata</string>
|
||||
<string name="tracks_or_reports_your_activity">Sleduje nebo hlásí vaší aktivitu</string>
|
||||
<string name="uninstall">Odinstalovat</string>
|
||||
<string name="unknown">Neznámé</string>
|
||||
<string name="unknown_error_DESC">Neznámá chyba.</string>
|
||||
<string name="unknown_error_DESC">Neznámá chyba</string>
|
||||
<string name="unknown_FORMAT">Neznámé: %s</string>
|
||||
<string name="unstable_updates">Nestabilní aktualizace</string>
|
||||
<string name="update">Aktualizovat</string>
|
||||
<string name="updates">Aktualizace</string>
|
||||
<string name="upstream_source_code_is_not_free">Originální zdrojový kód není svobodný</string>
|
||||
<string name="upstream_source_code_is_not_free">Původní zdrojový kód není svobodný</string>
|
||||
<string name="username">Uživatelské jméno</string>
|
||||
<string name="username_missing">Chybí uživatelské jméno</string>
|
||||
<string name="validation_index_error_DESC">Index nemohl být ověřen.</string>
|
||||
<string name="validation_index_error_DESC">Index nemohl být ověřen</string>
|
||||
<string name="version">Verze</string>
|
||||
<string name="version_FORMAT">Verze %s</string>
|
||||
<string name="versions">Verze</string>
|
||||
<string name="waiting_to_start_download">Čekám na zahájení stahování…</string>
|
||||
<string name="whats_new">Co je nového</string>
|
||||
<string name="website">Web</string>
|
||||
<string name="website">Web projektu</string>
|
||||
<string name="prefs_language_title">Jazyk</string>
|
||||
<string name="prefs_personalization">Personalizace</string>
|
||||
<string name="show_less">Zobrazit méně</string>
|
||||
@@ -164,10 +164,10 @@
|
||||
<string name="compiled_for_debugging">Zkompilováno pro ladění</string>
|
||||
<string name="installer">Instalátor</string>
|
||||
<string name="legacy_installer">Původní instalátor</string>
|
||||
<string name="session_installer">Instalátor pomocí relací</string>
|
||||
<string name="session_installer">Relační instalátor</string>
|
||||
<string name="root_installer">Root instalátor</string>
|
||||
<string name="shizuku_installer">Instalátor Shizuku</string>
|
||||
<string name="unstable_updates_summary">Navrhnout instalaci nestabilních verzí</string>
|
||||
<string name="shizuku_installer">Instalátor Shizuku/Sui</string>
|
||||
<string name="unstable_updates_summary">Navrhovat instalaci nestabilních verzí aplikací</string>
|
||||
<string name="select_mirror">Vybrat mirror</string>
|
||||
<plurals name="days">
|
||||
<item quantity="one">den</item>
|
||||
@@ -180,9 +180,9 @@
|
||||
<item quantity="other">hodin</item>
|
||||
</plurals>
|
||||
<string name="cleanup_title">Interval čištění APK</string>
|
||||
<string name="only_on_wifi_with_charging">Pouze na Wi-Fi a při nabíjení</string>
|
||||
<string name="io_error_DESC">Nepodařilo se vykonat některé akce.</string>
|
||||
<string name="material_you_desc">Použít barevný motiv Material You</string>
|
||||
<string name="only_on_wifi_with_charging">Pouze na Wi-Fi při nabíjení</string>
|
||||
<string name="io_error_DESC">Nepodařilo se vykonat některé akce</string>
|
||||
<string name="material_you_desc">Použít barvy Material You</string>
|
||||
<string name="material_you">Material You</string>
|
||||
<string name="favourites">Oblíbené</string>
|
||||
<string name="force_clean_up_DESC">Vyčistí přebytečné soubory</string>
|
||||
@@ -191,49 +191,54 @@
|
||||
<string name="enable_repo">Povolit repozitář</string>
|
||||
<string name="waiting_to_start_installation">Čekání na spuštění instalace…</string>
|
||||
<string name="installing">Instalace</string>
|
||||
<string name="auto_update">Automatická aktualizace aplikací</string>
|
||||
<string name="auto_update_apps">Pokusit se automaticky nainstalovat aktualizace</string>
|
||||
<string name="auto_update">Automaticky aktualizovat aplikace</string>
|
||||
<string name="auto_update_apps">Automaticky instalovat aktualizace</string>
|
||||
<string name="has_non_free_components">Obsahuje nesvobodné součásti</string>
|
||||
<string name="socket_error_DESC">Server neodeslal nový paket.</string>
|
||||
<string name="socket_error_DESC">Server neodeslal nový paket</string>
|
||||
<string name="connection_error_DESC">Nepodařilo se připojit k serveru</string>
|
||||
<string name="shizuku_not_alive">Shizuku není spuštěno</string>
|
||||
<string name="shizuku_not_installed">Shizuku není nainstalováno</string>
|
||||
<string name="contains_nsfw">Obsahuje obsah nevhodný do práce</string>
|
||||
<string name="special_credits">Speciální poděkování</string>
|
||||
<string name="home_screen_swiping">Posouvání na domovské stránce</string>
|
||||
<string name="home_screen_swiping_DESC">Umožnit uživateli posouvat mezi stránkami na domovské stránce</string>
|
||||
<string name="repository_not_found">Následující repozitář nebyl nalezen</string>
|
||||
<string name="shizuku_not_alive">Shizuku/Sui není spuštěno</string>
|
||||
<string name="shizuku_not_installed">Shizuku/Sui není nainstalováno</string>
|
||||
<string name="contains_nsfw">Obsahuje obsah nevhodný do práce (NSFW)</string>
|
||||
<string name="special_credits">Zvláštní poděkování</string>
|
||||
<string name="home_screen_swiping">Posouvání stránek</string>
|
||||
<string name="home_screen_swiping_DESC">Posuňte vlevo nebo v pravo pro změnu stránky</string>
|
||||
<string name="repository_not_found">Repozitář nebyl nalezen</string>
|
||||
<string name="proxy_port_error_not_int">Port proxy smí být pouze celé číslo</string>
|
||||
<string name="import_settings_title">Importovat nastavení</string>
|
||||
<string name="import_export">Import/export</string>
|
||||
<string name="import_settings_title">Importovat oblíbené a nastavení</string>
|
||||
<string name="import_export">Záloha a obnovení</string>
|
||||
<string name="import_settings_DESC">Importovat nastavení a oblíbené ze souboru</string>
|
||||
<string name="export_settings_title">Exportovat nastavení</string>
|
||||
<string name="export_repos_DESC">Exportovat všechny repozitáře do souboru</string>
|
||||
<string name="export_settings_title">Exportovat oblíbené a nastavení</string>
|
||||
<string name="export_repos_DESC">Exportovat repozitáře do souboru</string>
|
||||
<string name="import_repos_title">Importovat repozitáře</string>
|
||||
<string name="export_settings_DESC">Exportovat nastavení a oblíbené do souboru</string>
|
||||
<string name="export_repos_title">Exportovat repozitáře</string>
|
||||
<string name="import_repos_DESC">Importovat všechny repozitáře ze souboru</string>
|
||||
<string name="cannot_open_link">Nelze otevřít odkaz</string>
|
||||
<string name="has_tethered_network">Připojeno k určité síťové službě</string>
|
||||
<string name="require_background_access_DESC">Přístup na pozadí je vyžadován pro správné spuštění synchronizace na pozadí</string>
|
||||
<string name="require_background_access">Vyžadovat přístup na pozadí</string>
|
||||
<string name="ignore_signature">Ignorovat podpis</string>
|
||||
<string name="ignore_signature_summary">*Varování* Ignorovat ověřování podpisu při instalaci aplikace, pro uživatele LSPosed nebo pokročilé uživatele</string>
|
||||
<string name="import_repos_DESC">Importovat repozitáře ze souboru</string>
|
||||
<string name="cannot_open_link">Odkaz se nepodařilo otevřít</string>
|
||||
<string name="has_tethered_network">Závisí na určité instanci síťové služby</string>
|
||||
<string name="require_background_access_DESC">Zakázání optimalizací baterie je vyžadováno pro správnou funkčnost synchronizace na pozadí</string>
|
||||
<string name="require_background_access">Zakázat optimalizace baterie</string>
|
||||
<string name="ignore_signature">Ignorovat ověření podpisu</string>
|
||||
<string name="ignore_signature_summary">Ignorovat ověření podpisu při instalaci aplikace</string>
|
||||
<string name="uninstalled_application_DESC">Aplikace %s byla odinstalována</string>
|
||||
<string name="installation_failed">Instalace selhala</string>
|
||||
<string name="installation_failed_DESC">Nepodařilo se nainstalovat aplikaci %s</string>
|
||||
<string name="uninstalled_application">Odinstalováno</string>
|
||||
<string name="insufficient_storage">Nedostatek místa</string>
|
||||
<string name="insufficient_storage_DESC">Na zařízení není dostatek místa k instalaci této aplikace. Zkuste uvolnit trochu místa</string>
|
||||
<string name="error_shizuku_service_unavailable">Shizuku není spuštěno</string>
|
||||
<string name="error_shizuku_not_granted">Chybějící oprávnění Shizuku</string>
|
||||
<string name="error_shizuku_not_granted_DESC">Nebylo uděleno oprávnění ke službě Shizuku. Zkontrolujte prosím aplikaci Shizuku</string>
|
||||
<string name="error_shizuku_not_installed_DESC">Aplikace Shizuku nejspíše není nainstalována</string>
|
||||
<string name="insufficient_storage_DESC">Nemáte dostatek místa k instalaci této aplikace</string>
|
||||
<string name="error_shizuku_service_unavailable">Služba Shizuku/Sui není spuštěna</string>
|
||||
<string name="error_shizuku_not_granted">Oprávnění Shizuku/Sui neuděleno</string>
|
||||
<string name="error_shizuku_not_granted_DESC">Nebylo uděleno oprávnění Shizuku/Sui</string>
|
||||
<string name="error_shizuku_not_installed_DESC">Shizuku/Sui není nainstalováno</string>
|
||||
<string name="switch_to_default_installer">Přepnout na výchozí</string>
|
||||
<string name="label_unknown_sdk">Neznámé (%d)</string>
|
||||
<string name="error_shizuku_not_running_DESC">Služba Shizuku není spuštěna. Zkontrolujte prosím aplikaci Shizuku</string>
|
||||
<string name="error_shizuku_not_installed">Shizuku není nainstalováno</string>
|
||||
<string name="error_shizuku_not_running_DESC">Služba Shizuku/Sui není spuštěna</string>
|
||||
<string name="error_shizuku_not_installed">Shizuku/Sui není nainstalováno</string>
|
||||
<string name="label_open_video">Video</string>
|
||||
<string name="open_shizuku">Otevřít Shizuku</string>
|
||||
<string name="label_targets_sdk">Cíle: Android %s</string>
|
||||
<string name="always_choose">Vždy se zeptat</string>
|
||||
<string name="select_installer">Vyberte instalátor</string>
|
||||
<string name="shizuku_legacy_installer">Starý instalátor Shizuku/Sui</string>
|
||||
<string name="legacyInstallerComponent">Komponenta starého instalátoru</string>
|
||||
<string name="label_sdk_version">Cíl: Android %1$s | Minimum: Android %2$s</string>
|
||||
<string name="unspecified">Nespecifikováno</string>
|
||||
</resources>
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
<string name="downloaded_FORMAT">Hentet %s</string>
|
||||
<string name="downloading">Henter</string>
|
||||
<string name="downloading_FORMAT">Henter %s…</string>
|
||||
<string name="import_export">Import/Eksport</string>
|
||||
<string name="import_settings_title">Importér Indstillinger</string>
|
||||
<string name="import_export">Import/eksport</string>
|
||||
<string name="import_settings_title">Importér indstillinger</string>
|
||||
<string name="import_settings_DESC">Importér indstillinger og favoritter fra fil</string>
|
||||
<string name="export_settings_title">Eksportér Indstillinger</string>
|
||||
<string name="export_settings_title">Eksportér indstillinger</string>
|
||||
<string name="favourites">Favoritter</string>
|
||||
<string name="file_format_error_DESC">Ugyldigt filformat.</string>
|
||||
<string name="fingerprint">Fingeraftryk</string>
|
||||
@@ -60,8 +60,8 @@
|
||||
<string name="connection_error_DESC">Kunne ikke forbinde til server</string>
|
||||
<string name="ignore_all_updates">Ignorer alle nye versioner</string>
|
||||
<string name="incompatible_api_DESC_FORMAT">Din %1$s (API-version %2$d) understøttes ikke. %3$s</string>
|
||||
<string name="incompatible_api_max_DESC_FORMAT">Maksimal API-version er %d.</string>
|
||||
<string name="incompatible_api_min_DESC_FORMAT">Minimum API-version er %d.</string>
|
||||
<string name="incompatible_api_max_DESC_FORMAT">Maks. API-version er %d.</string>
|
||||
<string name="incompatible_api_min_DESC_FORMAT">Min. API-version er %d.</string>
|
||||
<string name="incompatible_features_DESC">Manglende funktioner.</string>
|
||||
<string name="incompatible_older_DESC">Denne version er ældre end den, der er installeret på din enhed. Afinstaller den først.</string>
|
||||
<string name="incompatible_version">Inkompatibel version</string>
|
||||
@@ -71,7 +71,7 @@
|
||||
<string name="install">Installer</string>
|
||||
<string name="install_types">Installationstyper</string>
|
||||
<string name="installer">Installatør</string>
|
||||
<string name="shizuku_installer">Shizuku Installatør</string>
|
||||
<string name="shizuku_installer">Shizuku-installatør</string>
|
||||
<string name="shizuku_not_alive">Shizuku kører ikke</string>
|
||||
<string name="shizuku_not_installed">Shizuku er ikke installeret</string>
|
||||
<string name="installing">Installerer</string>
|
||||
@@ -86,11 +86,11 @@
|
||||
<string name="light">Lys</string>
|
||||
<string name="link_copied_to_clipboard">Link kopieret</string>
|
||||
<string name="links">Links</string>
|
||||
<string name="home_screen_swiping">Strygning på Startskærm</string>
|
||||
<string name="home_screen_swiping">Strygning på startside</string>
|
||||
<string name="socket_error_DESC">Server kunne ikke levere ny pakke.</string>
|
||||
<string name="http_proxy">HTTP-proxy</string>
|
||||
<string name="incompatible_platforms_DESC_FORMAT">Din %1$s platform understøttes ikke. Understøttede platforme: %2$s.</string>
|
||||
<string name="root_installer">Root Installatør</string>
|
||||
<string name="root_installer">Root-installatør</string>
|
||||
<string name="material_you">Material You</string>
|
||||
<string name="material_you_desc">Brug Material You-farvetema</string>
|
||||
<string name="merging_FORMAT">Fletter %s</string>
|
||||
@@ -113,10 +113,10 @@
|
||||
<string name="ok">OK</string>
|
||||
<string name="only_compatible_with_FORMAT">Kun kompatibel med %s</string>
|
||||
<string name="only_on_wifi">Kun på Wi-Fi</string>
|
||||
<string name="only_on_wifi_with_charging">Kun på Wi-Fi & Opladning</string>
|
||||
<string name="only_on_wifi_with_charging">Kun på Wi-Fi og opladning</string>
|
||||
<string name="open_DESC_FORMAT">Åbn %s?</string>
|
||||
<string name="other">Andet</string>
|
||||
<string name="parsing_index_error_DESC">Kunne ikke analysere indeksfilen.</string>
|
||||
<string name="parsing_index_error_DESC">Kunne ikke fortolke indeksfilen.</string>
|
||||
<string name="password">Adgangskode</string>
|
||||
<string name="password_missing">Manglende adgangskode</string>
|
||||
<string name="plus_more_FORMAT">+%d mere</string>
|
||||
@@ -179,7 +179,7 @@
|
||||
<string name="export_settings_DESC">Eksportér indstillinger og favoritter til fil</string>
|
||||
<string name="has_non_free_components">Har ikke-frie komponenter</string>
|
||||
<string name="has_tethered_network">Bundet til en bestemt netværkstjeneste</string>
|
||||
<string name="home_screen_swiping_DESC">Tillad at stryge mellem sider på startskærm</string>
|
||||
<string name="home_screen_swiping_DESC">Tillad at stryge mellem sider på startside</string>
|
||||
<string name="ignore_this_update">Ignorer denne version</string>
|
||||
<string name="incompatible_signature_DESC">Denne version er signeret med et andet certifikat end den, der er installeret på din enhed. Afinstaller den først.</string>
|
||||
<string name="installed">Installeret</string>
|
||||
@@ -201,25 +201,39 @@
|
||||
<string name="compiled_for_debugging">Kompileret til fejlfinding</string>
|
||||
<string name="delete_repository_DESC">Slet repositoriet?</string>
|
||||
<string name="edit_repository">Rediger repository</string>
|
||||
<string name="import_repos_title">Importér Repositories</string>
|
||||
<string name="import_repos_title">Importér repositories</string>
|
||||
<string name="import_repos_DESC">Importér alle repositories fra fil</string>
|
||||
<string name="export_repos_title">Eksportér Repositories</string>
|
||||
<string name="export_repos_title">Eksportér repositories</string>
|
||||
<string name="export_repos_DESC">Eksportér alle repositories til fil</string>
|
||||
<string name="enable_repo">Aktivér repositoriet</string>
|
||||
<string name="credits">Krediteringer</string>
|
||||
<string name="update">Opdatering</string>
|
||||
<string name="require_background_access_DESC">Baggrundsadgang er nødvendig for at køre baggrundssynkronisering korrekt</string>
|
||||
<string name="require_background_access">Kræver Baggrundsadgang</string>
|
||||
<string name="legacy_installer">Ældre Installatør</string>
|
||||
<string name="session_installer">Session Installatør</string>
|
||||
<string name="require_background_access">Kræver baggrundsadgang</string>
|
||||
<string name="legacy_installer">Ældre installatør</string>
|
||||
<string name="session_installer">Sessionsinstallatør</string>
|
||||
<string name="special_credits">Særlige Krediteringer</string>
|
||||
<string name="contains_nsfw">Indeholder potentielt stødende indhold</string>
|
||||
<string name="ignore_signature_summary">*Advarsel* Ignorer signaturverifikation ved APK-installation; for LSPosed- eller avancerede brugere</string>
|
||||
<string name="installation_failed">Installation Mislykkedes</string>
|
||||
<string name="ignore_signature_summary">*Advarsel* Ignorer signaturverifikation ved APK-installation. For LSPosed- eller avancerede brugere</string>
|
||||
<string name="installation_failed">Installation mislykkedes</string>
|
||||
<string name="installation_failed_DESC">Kunne ikke installere %s</string>
|
||||
<string name="uninstalled_application">Afinstalleret</string>
|
||||
<string name="uninstalled_application_DESC">%s blev afinstalleret</string>
|
||||
<string name="ignore_signature">Ignorer Signatur</string>
|
||||
<string name="ignore_signature">Ignorer signatur</string>
|
||||
<string name="insufficient_storage">Utilstrækkelig plads</string>
|
||||
<string name="insufficient_storage_DESC">Enheden har ikke nok ledig plads til at installere denne applikation. Prøv at frigøre noget plads</string>
|
||||
<string name="error_shizuku_not_running_DESC">Shizuku-tjenesten kører ikke. Tjek venligst i Shizuku-appen</string>
|
||||
<string name="error_shizuku_not_granted">Shizuku-tilladelse mangler</string>
|
||||
<string name="error_shizuku_not_granted_DESC">Shizuku-tilladelse er ikke givet. Tjek venligst i Shizuku-appen</string>
|
||||
<string name="error_shizuku_not_installed">Shizuku ikke installeret</string>
|
||||
<string name="error_shizuku_not_installed_DESC">Shizuku ser ikke ud til at være installeret</string>
|
||||
<string name="open_shizuku">Åbn Shizuku</string>
|
||||
<string name="label_unknown_sdk">Ukendt (%d)</string>
|
||||
<string name="switch_to_default_installer">Skift til standard</string>
|
||||
<string name="label_open_video">Video</string>
|
||||
<string name="error_shizuku_service_unavailable">Shizuku kører ikke</string>
|
||||
<string name="always_choose">Vælg altid</string>
|
||||
<string name="select_installer">Vælg installationsprogram</string>
|
||||
<string name="unspecified">Uspecificeret</string>
|
||||
<string name="label_sdk_version">Mål: Android %1$s | Minimum: Android %2$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="add_repository">Repository hinzufügen</string>
|
||||
<string name="all_applications">Alle Anwendungen</string>
|
||||
<string name="all_applications_up_to_date">All deine Anwendungen sind aktuell</string>
|
||||
<string name="add_repository">Paketquelle hinzufügen</string>
|
||||
<string name="all_applications">Alle Apps</string>
|
||||
<string name="all_applications_up_to_date">Alle Apps sind aktuell</string>
|
||||
<string name="already_exists">Bereits vorhanden</string>
|
||||
<string name="always">Immer</string>
|
||||
<string name="address">Adresse</string>
|
||||
<string name="action_failed">Vorgang fehlgeschlagen</string>
|
||||
<string name="amoled">Schwarz</string>
|
||||
<string name="application">Anwendung</string>
|
||||
<string name="application">App</string>
|
||||
<string name="anti_features">Unerwünschte Merkmale</string>
|
||||
<string name="available">Entdecken</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="application_not_found">Diese Anwendung konnte nicht gefunden werden</string>
|
||||
<string name="application_not_found">App konnte nicht gefunden werden</string>
|
||||
<string name="bug_tracker">Fehlerverwaltung</string>
|
||||
<string name="changelog">Änderungsprotokoll</string>
|
||||
<string name="cant_edit_sync_DESC">Die Paketquelle kann nicht bearbeitet werden, da sie gerade synchronisiert wird.</string>
|
||||
<string name="checking_repository">Paketquelle wird abgefragt …</string>
|
||||
<string name="checking_repository">Paketquelle wird abgefragt …</string>
|
||||
<string name="compiled_for_debugging">Kompiliert für die Fehlersuche</string>
|
||||
<string name="connecting">Verbinde …</string>
|
||||
<string name="connecting">Wird verbunden …</string>
|
||||
<string name="confirmation">Bestätigung</string>
|
||||
<string name="could_not_validate_FORMAT">Konnte %s nicht validieren</string>
|
||||
<string name="could_not_validate_FORMAT">%s konnte nicht überprüft werden</string>
|
||||
<string name="dark">Dunkel</string>
|
||||
<string name="contains_non_free_media">Enthält nicht-freie Medien</string>
|
||||
<string name="credits">Mitwirkende</string>
|
||||
@@ -37,31 +37,31 @@
|
||||
<string name="incompatible_features_DESC">Fehlende Funktionen.</string>
|
||||
<string name="incompatible_platforms_DESC_FORMAT">Deine %1$s-Plattform wird nicht unterstützt. Unterstützte Plattformen: %2$s.</string>
|
||||
<string name="installed">Installiert</string>
|
||||
<string name="incompatible_versions_summary">Mit dem Gerät inkompatible Anwendungsversionen anzeigen</string>
|
||||
<string name="install_types">Installationstypen</string>
|
||||
<string name="incompatible_versions_summary">Mit diesem Gerät inkompatible App-Versionen anzeigen</string>
|
||||
<string name="install_types">Installationsarten</string>
|
||||
<string name="install">Installieren</string>
|
||||
<string name="integrity_check_error_DESC">Integrität konnte nicht überprüft werden.</string>
|
||||
<string name="invalid_metadata_error_DESC">Ungültige Metadaten.</string>
|
||||
<string name="invalid_signature_error_DESC">Ungültige Signatur.</string>
|
||||
<string name="launch">Öffnen</string>
|
||||
<string name="light">Hell</string>
|
||||
<string name="new_updates_available">Neue Anwendungsversionen verfügbar</string>
|
||||
<string name="new_updates_available">Neue Versionen von Apps verfügbar</string>
|
||||
<string name="never">Nie</string>
|
||||
<string name="network_error_DESC">Netzwerkfehler</string>
|
||||
<string name="notify_about_updates">Über neue Versionen benachrichtigen</string>
|
||||
<string name="notify_about_updates">Benachrichtigung über Aktualisierungen</string>
|
||||
<string name="no_description_available_DESC">Keine Beschreibung vorhanden</string>
|
||||
<string name="no_matching_applications_found">Keine derartigen Anwendungen konnten gefunden werden</string>
|
||||
<string name="no_applications_installed">Keine installierten Anwendungen</string>
|
||||
<string name="only_on_wifi">Nur bei Wi-Fi</string>
|
||||
<string name="open_DESC_FORMAT">Öffne %s\?</string>
|
||||
<string name="no_matching_applications_found">Keine derartigen Apps auffindbar</string>
|
||||
<string name="no_applications_installed">Keine installierten Apps</string>
|
||||
<string name="only_on_wifi">Nur mit WLAN</string>
|
||||
<string name="open_DESC_FORMAT">%s öffnen?</string>
|
||||
<string name="other">Andere</string>
|
||||
<string name="number_of_applications">Anzahl der Anwendungen</string>
|
||||
<string name="number_of_applications">Anzahl der Apps</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="permissions">Berechtigungen</string>
|
||||
<string name="plus_more_FORMAT">+%d mehr</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="processing_FORMAT">Verarbeitung %1$s …</string>
|
||||
<string name="promotes_non_free_software">Bewirbt unfreie Software</string>
|
||||
<string name="processing_FORMAT">%1$s wird verarbeitet …</string>
|
||||
<string name="promotes_non_free_software">Bewirbt nicht-freie Software</string>
|
||||
<string name="proxy">Proxy</string>
|
||||
<string name="repositories">Paketquellen</string>
|
||||
<string name="requires_FORMAT">Benötigt %s</string>
|
||||
@@ -70,36 +70,36 @@
|
||||
<string name="skip">Überspringen</string>
|
||||
<string name="suggested">Empfohlen</string>
|
||||
<string name="syncing">Synchronisierung</string>
|
||||
<string name="themes">Themen</string>
|
||||
<string name="themes">Designs</string>
|
||||
<string name="unknown">Unbekannt</string>
|
||||
<string name="uninstall">Deinstallation</string>
|
||||
<string name="update">Aktualisierung</string>
|
||||
<string name="uninstall">Deinstallieren</string>
|
||||
<string name="update">Aktualisieren</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="versions">Versionen</string>
|
||||
<string name="whats_new">Was gibt es Neues</string>
|
||||
<string name="waiting_to_start_download">Warten auf den Downloadbeginn …</string>
|
||||
<string name="validation_index_error_DESC">Der Index konnte nicht validiert werden.</string>
|
||||
<string name="website">Webseite</string>
|
||||
<string name="whats_new">Neu hinzugefügt</string>
|
||||
<string name="waiting_to_start_download">Warten auf den Start des Downloads …</string>
|
||||
<string name="validation_index_error_DESC">Der Index konnte nicht überprüft werden.</string>
|
||||
<string name="website">Website</string>
|
||||
<string name="changes">Änderungen</string>
|
||||
<string name="author_email">Autor-E-Mail-Adresse</string>
|
||||
<string name="could_not_download_FORMAT">Konnte %s nicht herunterladen</string>
|
||||
<string name="author_website">Autor-Webseite</string>
|
||||
<string name="author_email">E-Mail-Adresse</string>
|
||||
<string name="could_not_download_FORMAT">%s konnte nicht heruntergeladen werden</string>
|
||||
<string name="author_website">Website</string>
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="recently_updated">Zuletzt aktualisiert</string>
|
||||
<string name="recently_updated">Kürzlich aktualisiert</string>
|
||||
<string name="license">Lizenz</string>
|
||||
<string name="could_not_sync_FORMAT">Konnte %s nicht synchronisieren</string>
|
||||
<string name="could_not_sync_FORMAT">%s konnte nicht synchronisiert werden</string>
|
||||
<string name="only_compatible_with_FORMAT">Nur kompatibel mit %s</string>
|
||||
<string name="license_FORMAT">%s-Lizenz</string>
|
||||
<string name="link_copied_to_clipboard">Link kopiert</string>
|
||||
<string name="project_website">Projekt-Website</string>
|
||||
<string name="proxy_type">Proxy Typ</string>
|
||||
<string name="project_website">Website des Projekts</string>
|
||||
<string name="proxy_type">Proxy-Art</string>
|
||||
<string name="repository">Paketquelle</string>
|
||||
<string name="parsing_index_error_DESC">Die Indexdatei konnte nicht geparst werden.</string>
|
||||
<string name="password">Passwort</string>
|
||||
<string name="notify_about_updates_summary">Eine Benachrichtigung anzeigen, wenn neue Versionen verfügbar sind</string>
|
||||
<string name="notify_about_updates_summary">Benachrichtigung anzeigen, wenn neue Versionen verfügbar sind</string>
|
||||
<string name="provided_by_FORMAT">Bereitgestellt von %s</string>
|
||||
<string name="source_code_no_longer_available">Quellcode nicht mehr verfügbar</string>
|
||||
<string name="invalid_username_format">Ungültiges Benutzernamen-Format</string>
|
||||
<string name="invalid_username_format">Ungültiges Format des Benutzernamens</string>
|
||||
<string name="no_proxy">Kein Proxy</string>
|
||||
<string name="password_missing">Passwort fehlt</string>
|
||||
<string name="source_code">Quellcode</string>
|
||||
@@ -109,58 +109,58 @@
|
||||
<string name="has_non_free_dependencies">Enthält nicht-freie Abhängigkeiten</string>
|
||||
<string name="incompatible_version">Inkompatible Version</string>
|
||||
<string name="system">System</string>
|
||||
<string name="theme">Thema</string>
|
||||
<string name="theme">Design</string>
|
||||
<string name="ignore_all_updates">Alle neuen Versionen ignorieren</string>
|
||||
<string name="ignore_this_update">Diese Version ignorieren</string>
|
||||
<string name="size">Größe</string>
|
||||
<string name="updates">Aktualisierungen</string>
|
||||
<string name="username">Benutzername</string>
|
||||
<string name="version_FORMAT">Version %s</string>
|
||||
<string name="downloading">Herunterladen</string>
|
||||
<string name="downloading">Wird heruntergeladen …</string>
|
||||
<string name="share">Teilen</string>
|
||||
<string name="show_more">Zeige mehr</string>
|
||||
<string name="show_older_versions">Ältere Versionen zeigen</string>
|
||||
<string name="show_more">Mehr anzeigen</string>
|
||||
<string name="show_older_versions">Ältere Versionen anzeigen</string>
|
||||
<string name="username_missing">Benutzername fehlt</string>
|
||||
<string name="edit_repository">Paketquelle bearbeiten</string>
|
||||
<string name="file_format_error_DESC">Ungültiges Dateiformat.</string>
|
||||
<string name="downloading_FORMAT">%s wird heruntergeladen …</string>
|
||||
<string name="downloading_FORMAT">%s wird heruntergeladen …</string>
|
||||
<string name="incompatible_with_FORMAT">Inkompatibel mit %s</string>
|
||||
<string name="incompatible_versions">Inkompatible Versionen</string>
|
||||
<string name="invalid_address">Ungültige Adresse</string>
|
||||
<string name="invalid_fingerprint_format">Ungültiges Fingerabdruckformat</string>
|
||||
<string name="invalid_permissions_error_DESC">Ungültige Berechtigungen.</string>
|
||||
<string name="promotes_non_free_network_services">Bewirbt unfreie Netzwerkdienste</string>
|
||||
<string name="promotes_non_free_network_services">Bewirbt nicht-freie Netzwerkdienste</string>
|
||||
<string name="unknown_FORMAT">Unbekannt: %s</string>
|
||||
<string name="unknown_error_DESC">Unbekannter Fehler.</string>
|
||||
<string name="syncing_FORMAT">Synchronisierung %s …</string>
|
||||
<string name="incompatible_signature_DESC">Diese Version ist mit einem anderen Zertifikat signiert, als die auf Deinem Gerät installierte. Deinstalliere diese zuerst.</string>
|
||||
<string name="delete_repository_DESC">Die Paketquelle löschen\?</string>
|
||||
<string name="incompatible_older_DESC">Diese Version ist älter als die auf deinem Gerät installierte. Deinstalliere diese zuerst.</string>
|
||||
<string name="syncing_FORMAT">%s wird synchronisiert …</string>
|
||||
<string name="incompatible_signature_DESC">Diese Version ist mit einem anderen Zertifikat signiert als die auf deinem Gerät installierte Version. Deinstalliere diese zuerst.</string>
|
||||
<string name="delete_repository_DESC">Paketquelle löschen?</string>
|
||||
<string name="incompatible_older_DESC">Diese Version ist älter als diejenige, die auf deinem Gerät installiert ist. Deinstalliere diese zuerst.</string>
|
||||
<string name="incompatible_api_DESC_FORMAT">Deine %1$s (API-Version %2$d) wird nicht unterstützt. %3$s</string>
|
||||
<string name="incompatible_api_min_DESC_FORMAT">Die minimale API-Version ist %d.</string>
|
||||
<string name="repository_unsigned_DESC">Nicht signiert. Die Anwendungsliste konnte nicht verifiziert werden. Sei vorsichtig beim Herunterladen von Anwendungen aus nicht signierten Paketquellen.</string>
|
||||
<string name="repository_unsigned_DESC">Nicht signiert. Die App-Liste konnte nicht verifiziert werden. Sei beim Herunterladen von Apps aus nicht signierten Paketquellen vorsichtig.</string>
|
||||
<string name="unstable_updates_summary">Installation von instabilen Versionen vorschlagen</string>
|
||||
<string name="unstable_updates">Instabile Aktualisierungen</string>
|
||||
<string name="proxy_host">Proxy Host</string>
|
||||
<string name="tap_to_install_DESC">Tippe um zu installieren.</string>
|
||||
<string name="tracks_or_reports_your_activity">Verfolgt oder erfasst deine Aktivitäten</string>
|
||||
<string name="proxy_port">Proxy Port</string>
|
||||
<string name="search">Suche</string>
|
||||
<string name="unstable_updates">Instabile Updates</string>
|
||||
<string name="proxy_host">Proxy-Host</string>
|
||||
<string name="tap_to_install_DESC">Zum Installieren tippen.</string>
|
||||
<string name="tracks_or_reports_your_activity">Verfolgt oder versendet deine Aktivitäten</string>
|
||||
<string name="proxy_port">Proxy-Port</string>
|
||||
<string name="search">Suchen</string>
|
||||
<string name="sorting_order">Sortierreihenfolge</string>
|
||||
<string name="socks_proxy">SOCKS Proxy</string>
|
||||
<string name="no_applications_available">Keine Anwendungen verfügbar</string>
|
||||
<string name="socks_proxy">SOCKS-Proxy</string>
|
||||
<string name="no_applications_available">Keine Apps verfügbar</string>
|
||||
<plurals name="new_updates_DESC_FORMAT">
|
||||
<item quantity="one">%d Anwendung hat eine neue Version.</item>
|
||||
<item quantity="other">%d Anwendungen haben eine neue Version.</item>
|
||||
<item quantity="one">%d App hat eine neue Version.</item>
|
||||
<item quantity="other">%d Apps haben eine neue Version.</item>
|
||||
</plurals>
|
||||
<string name="signed_using_unsafe_algorithm">Mit einem unsicheren Algorithmus signiert</string>
|
||||
<string name="select_mirror">Wähle einen Spiegel</string>
|
||||
<string name="links">Links</string>
|
||||
<string name="merging_FORMAT">Führe %s zusammen</string>
|
||||
<string name="merging_FORMAT">%s wird zusammengeführt …</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="upstream_source_code_is_not_free">Der Upstream-Quellcode ist nicht frei</string>
|
||||
<string name="prefs_language_title">Sprache</string>
|
||||
<string name="prefs_personalization">Personalisierung</string>
|
||||
<string name="prefs_personalization">Personalisieren</string>
|
||||
<string name="show_less">Weniger anzeigen</string>
|
||||
<string name="update_all">Alle aktualisieren</string>
|
||||
<plurals name="days">
|
||||
@@ -171,57 +171,57 @@
|
||||
<item quantity="one">Stunde</item>
|
||||
<item quantity="other">Stunden</item>
|
||||
</plurals>
|
||||
<string name="only_on_wifi_with_charging">Nur während des Ladevorgangs und aktiviertem WLAN</string>
|
||||
<string name="installer">Installationsmethode</string>
|
||||
<string name="only_on_wifi_with_charging">Nur mit WLAN während des Aufladens</string>
|
||||
<string name="installer">Installation</string>
|
||||
<string name="cleanup_title">APK-Bereinigungsintervall</string>
|
||||
<string name="root_installer">Root-Installation</string>
|
||||
<string name="legacy_installer">Alte Installationsmethode</string>
|
||||
<string name="session_installer">Sitzungs-Installation</string>
|
||||
<string name="legacy_installer">Legacy-Installation</string>
|
||||
<string name="session_installer">Sitzungsinstallation</string>
|
||||
<string name="shizuku_installer">Shizuku-Installation</string>
|
||||
<string name="io_error_DESC">Bestimmte Aktionen können nicht durchgeführt werden.</string>
|
||||
<string name="favourites">Favoriten</string>
|
||||
<string name="material_you">Material You</string>
|
||||
<string name="material_you_desc">Material You-Farbschema verwenden</string>
|
||||
<string name="repository_unreachable">Repository unerreichbar</string>
|
||||
<string name="force_clean_up">Aufräumen erzwingen</string>
|
||||
<string name="enable_repo">Repository aktivieren</string>
|
||||
<string name="repository_unreachable">Paketquelle unerreichbar</string>
|
||||
<string name="force_clean_up">Bereinigung erzwingen</string>
|
||||
<string name="enable_repo">Paketquelle aktivieren</string>
|
||||
<string name="force_clean_up_DESC">Entfernt doppelte Dateien</string>
|
||||
<string name="installing">Installation</string>
|
||||
<string name="waiting_to_start_installation">Warten auf den Beginn der Installation …</string>
|
||||
<string name="installing">Wird installiert …</string>
|
||||
<string name="waiting_to_start_installation">Warten auf den Start der Installation …</string>
|
||||
<string name="auto_update">Apps automatisch aktualisieren</string>
|
||||
<string name="auto_update_apps">Versuche, Updates automatisch zu installieren</string>
|
||||
<string name="has_non_free_components">Hat nicht-freie Komponenten</string>
|
||||
<string name="auto_update_apps">Updates möglichst automatisch installieren</string>
|
||||
<string name="has_non_free_components">Enthält nicht-freie Komponenten</string>
|
||||
<string name="socket_error_DESC">Server konnte kein neues Datenpaket liefern.</string>
|
||||
<string name="shizuku_not_alive">Shizuku läuft nicht</string>
|
||||
<string name="contains_nsfw">Enthält für den Arbeitsplatz unangemessene Inhalte</string>
|
||||
<string name="connection_error_DESC">Verbindung zum Server nicht möglich</string>
|
||||
<string name="shizuku_not_installed">Shizuku ist nicht installiert</string>
|
||||
<string name="home_screen_swiping">Wischgesten</string>
|
||||
<string name="home_screen_swiping_DESC">Dem Benutzer erlauben, auf dem Startbildschirm zwischen den Seiten zu wischen</string>
|
||||
<string name="home_screen_swiping_DESC">Durch Wischen nach links/rechts zwischen Seiten navigieren</string>
|
||||
<string name="special_credits">Besonderer Dank</string>
|
||||
<string name="proxy_port_error_not_int">Proxy-Port muss eine natürliche Zahl sein</string>
|
||||
<string name="repository_not_found">Folgende Repos konnten nicht gefunden werden</string>
|
||||
<string name="repository_not_found">Folgende Paketquelle konnten nicht gefunden werden</string>
|
||||
<string name="import_settings_title">Einstellungen importieren</string>
|
||||
<string name="import_export">Importieren/Exportieren</string>
|
||||
<string name="import_settings_DESC">Importiere Einstellung und Favoriten von Datei</string>
|
||||
<string name="import_settings_DESC">Einstellungen und Favoriten aus Datei importieren</string>
|
||||
<string name="export_settings_title">Einstellungen exportieren</string>
|
||||
<string name="export_repos_DESC">Alle Repositories in eine Datei exportieren</string>
|
||||
<string name="import_repos_title">Importiere eine Sammlung</string>
|
||||
<string name="export_repos_DESC">Paketquellen in eine Datei exportieren</string>
|
||||
<string name="import_repos_title">Paketquellen importieren</string>
|
||||
<string name="export_settings_DESC">Einstellungen und Favoriten in eine Datei exportieren</string>
|
||||
<string name="export_repos_title">Repositories exportieren</string>
|
||||
<string name="import_repos_DESC">Alle Repositories aus einer Datei importieren</string>
|
||||
<string name="export_repos_title">Paketquellen exportieren</string>
|
||||
<string name="import_repos_DESC">Paketquellen aus einer Datei importieren</string>
|
||||
<string name="cannot_open_link">Link kann nicht geöffnet werden</string>
|
||||
<string name="has_tethered_network">An einen bestimmten Netzwerkdienst gebunden</string>
|
||||
<string name="ignore_signature">Signatur ignorieren</string>
|
||||
<string name="ignore_signature_summary">*Achtung* Signaturüberprüfung bei der Installation der APK ignorieren. Für LSPosed-Benutzer oder Experten.</string>
|
||||
<string name="ignore_signature_summary">*Achtung* Signaturüberprüfung bei der Installation der APK ignorieren. Für LSPosed-Benutzer oder Experten</string>
|
||||
<string name="installation_failed">Installation fehlgeschlagen</string>
|
||||
<string name="uninstalled_application_DESC">%s wurde deinstalliert</string>
|
||||
<string name="installation_failed_DESC">%s konnte nicht installiert werden</string>
|
||||
<string name="uninstalled_application">Deinstalliert</string>
|
||||
<string name="require_background_access">Hintergrundzugriff anfordern</string>
|
||||
<string name="require_background_access_DESC">Hintergrundzugriff ist erforderlich, um die Hintergrundsynchronisation ordnungsgemäß durchzuführen</string>
|
||||
<string name="insufficient_storage">Nicht genug Speicherplatz</string>
|
||||
<string name="insufficient_storage_DESC">Es gibt nicht genug Speicherplatz, um diese Anwendung zu installieren. Versuche etwas Platz zu schafffen.</string>
|
||||
<string name="insufficient_storage">Nicht genügend Speicherplatz</string>
|
||||
<string name="insufficient_storage_DESC">Nicht genügend Speicherplatz, um diese App zu installieren. Versuche, etwas Platz zu schaffen</string>
|
||||
<string name="error_shizuku_not_granted">Shizuku-Erlaubnis fehlt</string>
|
||||
<string name="error_shizuku_not_granted_DESC">Erlaubnis für den Shizuku-Dienst ist nicht gewährt. Bitte in der Shizuku-App prüfen</string>
|
||||
<string name="error_shizuku_not_installed">Shizuku nicht installiert</string>
|
||||
@@ -230,4 +230,11 @@
|
||||
<string name="open_shizuku">Shizuku öffnen</string>
|
||||
<string name="error_shizuku_service_unavailable">Shizuku läuft nicht</string>
|
||||
<string name="error_shizuku_not_running_DESC">Der Shizuku-Dienst läuft nicht. Bitte in der Shizuku-App prüfen</string>
|
||||
<string name="label_open_video">Video</string>
|
||||
<string name="label_unknown_sdk">Unbekannt (%d)</string>
|
||||
<string name="unspecified">Nicht spezifiziert</string>
|
||||
<string name="shizuku_legacy_installer">Shizuku Lagacy-Installation</string>
|
||||
<string name="legacyInstallerComponent">Legacy-Installationskomponente</string>
|
||||
<string name="always_choose">Immer Wählen</string>
|
||||
<string name="label_sdk_version">Zielversion: Android %1$s | Minimum: Android %2$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -220,4 +220,22 @@
|
||||
<string name="uninstalled_application_DESC">Το %s απεγκαταστάθηκε</string>
|
||||
<string name="ignore_signature">Αγνόησή Υπογραφής</string>
|
||||
<string name="ignore_signature_summary">Αγνοήστε την επαλήθευση υπογραφής κατά την εγκατάσταση apk, για χρήστες με LSP ή προχωρημένους χρήστες</string>
|
||||
<string name="error_shizuku_not_granted_DESC">Η άδεια λειτουργίας της υπηρεσίας Shizuku δεν έχει παραχωρηθεί. Ελέγξτε την εφαρμογή Shizuku</string>
|
||||
<string name="error_shizuku_not_installed">Το Shizuku δεν έχει εγκατασταθεί</string>
|
||||
<string name="open_shizuku">Άνοιγμα Shizuku</string>
|
||||
<string name="label_unknown_sdk">Άγνωστο (%d)</string>
|
||||
<string name="label_open_video">Βίντεο</string>
|
||||
<string name="switch_to_default_installer">Μετάβαση στην Προεπιλογή</string>
|
||||
<string name="insufficient_storage">Ανεπαρκής Χώρος</string>
|
||||
<string name="error_shizuku_not_installed_DESC">Το Shizuku δεν φαίνεται να είναι εγκατεστημένο</string>
|
||||
<string name="error_shizuku_service_unavailable">Δεν είναι σε λειτουργία το Shizuku</string>
|
||||
<string name="error_shizuku_not_running_DESC">Η υπηρεσία Shizuku δεν λειτουργεί. Ελέγξτε την εφαρμογή Shizuku</string>
|
||||
<string name="error_shizuku_not_granted">Λείπει η άδεια Shizuku</string>
|
||||
<string name="insufficient_storage_DESC">Δεν υπάρχει αρκετός ελεύθερος χώρος στη συσκευή για την εγκατάσταση αυτής της εφαρμογής. Προσπαθήστε να ελευθερώσετε και να καθαρίσετε λίγο χώρο</string>
|
||||
<string name="unspecified">Απροσδιόριστο</string>
|
||||
<string name="legacyInstallerComponent">Εξάρτημα Απαρχαιωμένου Εγκαταστάτη</string>
|
||||
<string name="shizuku_legacy_installer">Απαρχαιωμένος Εγκαταστάτης Shizuku</string>
|
||||
<string name="always_choose">Πάντα Επιλογή</string>
|
||||
<string name="select_installer">Επιλέξτε εγκαταστάτη</string>
|
||||
<string name="label_sdk_version">Στοχεύει: Android %1$s | Ελάχιστο: Android %2$s</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user