47 Commits
main ... main

Author SHA1 Message Date
6cd519d0a6 UPDATING.md aktualisiert 2025-09-11 16:14:27 +02:00
a7fa9fcf80 README.md aktualisiert 2025-06-04 15:44:23 +02:00
Felitendo
e17ca02b6b fixed repository 2025-06-04 14:57:56 +02:00
Felitendo
c410026ad8 v0.0.3 2025-06-03 15:24:18 +02:00
Felitendo
a05b78c6ef fixed app download with invalid signature 2025-06-03 14:54:04 +02:00
Felitendo
f545ffae91 fixt repo link 2025-06-03 14:36:13 +02:00
bb5c1ec1fe README.md aktualisiert 2025-05-21 15:57:20 +02:00
Felitendo
987af545be small ui tweaks 2025-05-21 15:34:34 +02:00
Felitendo
297ba87f64 fixed layout issues 2025-05-21 15:08:24 +02:00
Felitendo
97f87eb3c8 moved request app button to the top (not tested) 2025-05-21 15:01:01 +02:00
Felitendo
c9100a927a Fixed build crashing 2025-05-21 14:41:59 +02:00
Felitendo
bf52c20cb4 Added a way for users to suggest apps 2025-05-21 13:34:57 +02:00
0d6e0bbcae .github/workflows/release_build.yml gelöscht 2025-05-21 12:27:29 +02:00
0a60e93e66 .github/workflows/release_build.yml aktualisiert 2025-05-21 10:39:02 +02:00
480d5a885b .github/workflows/release_build.yml aktualisiert 2025-05-21 10:21:00 +02:00
67206b176e .github/workflows/release_build.yml aktualisiert 2025-05-21 10:17:38 +02:00
021af3619e .github/workflows/release_build.yml aktualisiert 2025-05-21 10:11:35 +02:00
Felitendo
d11c751f07 Changed Home Screen to order Apps by latest update 2025-05-21 10:08:39 +02:00
f552c0bda1 README.md aktualisiert 2025-05-21 10:05:35 +02:00
fcccc2126a README.md aktualisiert 2025-05-20 23:51:32 +02:00
801fa055f4 README.md aktualisiert 2025-05-20 23:51:00 +02:00
bb00031a7b v0.0.2 2025-05-20 23:49:33 +02:00
fd97e6e514 README.md aktualisiert 2025-05-20 23:39:02 +02:00
08e351eecc README.md aktualisiert 2025-05-20 23:37:02 +02:00
a730aa478d README.md aktualisiert 2025-05-20 23:36:44 +02:00
49def9086b README.md aktualisiert 2025-05-20 23:36:32 +02:00
70781e741c app/build.gradle.kts aktualisiert 2025-05-20 23:11:36 +02:00
7fd7f0af8e .github/workflows/build_debug.yml aktualisiert 2025-05-20 23:10:56 +02:00
a7a8f9bce0 .github/workflows/build_debug.yml aktualisiert 2025-05-20 23:00:17 +02:00
8076fbce74 .github/workflows/build_debug.yml aktualisiert 2025-05-20 22:47:24 +02:00
9996ec3f1e .github/workflows/release_build.yml aktualisiert 2025-05-20 22:36:31 +02:00
ab5a200ac8 .github/workflows/release_build.yml aktualisiert 2025-05-20 22:27:33 +02:00
a7f730c0ff .github/workflows/release_build.yml aktualisiert 2025-05-20 22:16:21 +02:00
0928e06069 .github/workflows/release_build.yml aktualisiert 2025-05-20 22:01:18 +02:00
14bd5b3ea8 .github/workflows/release_build.yml aktualisiert 2025-05-20 21:52:19 +02:00
a7a3e13b71 .github/workflows/release_build.yml aktualisiert 2025-05-20 21:50:11 +02:00
a84b1190b4 .github/workflows/build_debug.yml aktualisiert 2025-05-20 21:30:05 +02:00
4ab967faf0 .github/workflows/build_debug.yml aktualisiert 2025-05-20 21:13:23 +02:00
39da43e271 .github/workflows/build_debug.yml aktualisiert 2025-05-20 20:57:18 +02:00
8ff5bfc991 Added Felo Store Repo as default 2025-05-20 19:49:35 +02:00
Felitendo
429081b94d refactored to new name 2025-05-20 17:57:02 +02:00
5b1a938ac1 merge upstream 2025-05-20 15:52:39 +02:00
f45acb7284 UPDATING.md aktualisiert 2025-05-20 15:49:32 +02:00
691c59950a UPDATING.md aktualisiert 2025-05-20 15:49:13 +02:00
Felitendo
c8ff87823a Added info for updating / merging upstream 2025-05-20 15:45:42 +02:00
Felitendo
40391ded24 Update to upstream v0.6.5
# Conflicts:
#	app/build.gradle.kts
2025-05-20 15:44:40 +02:00
Felitendo
3412ab24b8 Upgrade Test 2025-05-20 15:26:13 +02:00
286 changed files with 3035 additions and 7599 deletions

View File

@@ -1,5 +1,4 @@
name: Build Debug APK
on:
push:
branches:
@@ -13,19 +12,15 @@ on:
- '**.md'
types: submitted
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out repository
uses: actions/checkout@v4
@@ -33,10 +28,7 @@ jobs:
submodules: true
- name: Validate Gradle Wrapper
uses: gradle/actions/setup-gradle@v4
- name: Setup Gradle
uses: gradle/wrapper-validation-action@v3
uses: gradle/wrapper-validation-action@v1
- name: Set up Java 17
uses: actions/setup-java@v4
@@ -45,30 +37,51 @@ jobs:
distribution: 'adopt'
cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Grant execution permission to Gradle Wrapper
run: chmod +x gradlew
- name: Build Debug APK
run: ./gradlew assembleDebug
- name: Sign Apk
continue-on-error: true
id: sign_apk
uses: r0adkll/sign-android-release@v1
with:
releaseDir: app/build/outputs/apk/debug
signingKeyBase64: ${{ secrets.KEY_BASE64 }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEYSTORE_PASS }}
keyPassword: ${{ secrets.KEYSTORE_PASS }}
- name: Display APK directory contents
run: |
echo "Listing app/build/outputs/apk/debug contents:"
ls -la app/build/outputs/apk/debug/
- name: Remove file that aren't signed
# Using a manual signing approach instead of the failing action
- name: Sign APK manually
continue-on-error: true
run: |
ls | grep 'signed\.apk$' && find . -type f -name '*.apk' ! -name '*-signed.apk' -delete
if [ -f app/build/outputs/apk/debug/app-debug.apk ]; then
echo "Found APK file to sign"
# Create keystore from base64
echo "${{ secrets.KEY_BASE64 }}" | base64 -d > keystore.jks
# Sign APK using apksigner
$ANDROID_HOME/build-tools/*/apksigner sign --ks keystore.jks \
--ks-key-alias "${{ secrets.KEY_ALIAS }}" \
--ks-pass pass:"${{ secrets.KEYSTORE_PASS }}" \
--key-pass pass:"${{ secrets.KEYSTORE_PASS }}" \
--out app/build/outputs/apk/debug/app-debug-signed.apk \
app/build/outputs/apk/debug/app-debug.apk
echo "APK signed successfully"
else
echo "No APK file found to sign"
fi
- name: Display signed APK files
run: |
echo "Listing signed APK files:"
find app/build/outputs/apk/debug/ -name "*-signed.apk" || echo "No signed APK found"
# Using v3 of upload-artifact which is more widely supported
- name: Upload the APK
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: debug
path: app/build/outputs/apk/debug/app-debug*.apk
path: app/build/outputs/apk/debug/*.apk

View File

@@ -1,94 +0,0 @@
name: Build Release APK
on:
workflow_dispatch:
push:
tags:
- '*'
concurrency:
group: "release-build"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: true
- name: Validate Gradle Wrapper
uses: gradle/actions/setup-gradle@v4
- name: Setup Gradle
uses: gradle/wrapper-validation-action@v3
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'adopt'
cache: gradle
- name: Grant execution permission to Gradle Wrapper
run: chmod +x gradlew
- name: Build Release APK
run: ./gradlew assembleRelease
- name: Checks
run: find . -type f -name "*.apk"
- uses: r0adkll/sign-android-release@v1
name: Signing APK
id: sign_app
with:
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.KEY_BASE64 }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEYSTORE_PASS }}
keyPassword: ${{ secrets.KEYSTORE_PASS }}
env:
BUILD_TOOLS_VERSION: "35.0.0"
- name: Extract Version Code
id: extract_version
run: |
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"
- name: Read Changelog
id: read_changelog
run: |
CHANGELOG_PATH="metadata/en-US/changelogs/${{ steps.extract_version.outputs.version_code }}.txt"
if [[ -f "$CHANGELOG_PATH" ]]; then
CHANGELOG=$(cat "$CHANGELOG_PATH")
echo "::set-output name=changelog::$CHANGELOG"
else
echo "::set-output name=changelog::No changelog found for this version."
echo "No changelog found at: $CHANGELOG_PATH"
fi
- uses: softprops/action-gh-release@v2
name: Create Release
id: publish_release
with:
body: ${{ steps.read_changelog.outputs.changelog }}
tag_name: ${{ github.ref }}
name: Release ${{ github.ref }}
files: ${{steps.sign_app.outputs.signedReleaseFile}}
draft: true
prerelease: false
- uses: actions/upload-artifact@v4
with:
name: Signed APK
path: ${{steps.sign_app.outputs.signedReleaseFile}}

View File

@@ -1,33 +1,21 @@
<div align="center">
## Download
<img width="" src="metadata/en-US/images/featureGraphic.png" alt="Droid-ify" align="center">
[![GitHub stars](https://img.shields.io/github/stars/Iamlooker/Droid-ify?color=%2364f573&style=for-the-badge)](https://github.com/Iamlooker/Droid-ify/stargazers)
[![GitHub license](https://img.shields.io/github/license/Iamlooker/Droid-ify?color=%2364f573&style=for-the-badge)](https://github.com/Iamlooker/Droid-ify/blob/master/COPYING)
[![GitHub downloads](https://img.shields.io/github/downloads/Iamlooker/Droid-ify/total.svg?color=%23f5ad64&style=for-the-badge)](https://github.com/Iamlooker/Droid-ify/releases/)
[![GitHub latest release](https://img.shields.io/github/v/release/Iamlooker/Droid-ify?display_name=tag&color=%23f5ad64&style=for-the-badge)](https://github.com/Iamlooker/Droid-ify/releases/latest)
[![F-Droid latest release](https://img.shields.io/f-droid/v/com.looker.droidify?color=%23f5ad64&style=for-the-badge)](https://f-droid.org/packages/com.looker.droidify)
</div>
<div align="left">
- Click [here](https://git.felo.gg/FeloStore/FeloStore/releases/download/v0.0.3/felostore.apk) to download the latest version
## Features
* Clean Material 3 design
* Material & Clean design
* Fast repository syncing
* Smooth user experience
* Feature-rich
## Screenshots
<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.
@@ -36,26 +24,17 @@
- Select "Create New Keystore" and enter the required information, including a password.
- Wait for the build process to finish.
## TODO
- [ ] Add support for `index-v2`
- [ ] Add detekt code analysis
## Contributing
## Contribution
- Pick any issue you would like to resolve
- Fork the project
- Open a pull request
- Your pull request will undergo review
## Translations
[![Translation status](https://hosted.weblate.org/widgets/droidify/-/horizontal-auto.svg)](https://hosted.weblate.org/engage/droidify/?utm_source=widget)
- Open a Pull Request
- Your PR will undergo review
## License
```
Droid-ify
FeloStore
Copyright (C) 2023 LooKeR
This program is free software: you can redistribute it and/or modify

View File

@@ -11,5 +11,5 @@ It is only natural that users will grow impatient for an update, thats why I wil
## What now?
- Next release will be on **19-Aug**, and will contain many bug fixes and stability improvements.
- All the [progress](https://github.com/Droid-ify/client/pull/309) made in past months is still here and will help in future.
- All the [progress](https://github.com/FeloStore/client/pull/309) made in past months is still here and will help in future.
- We will be missing on the new index format introduced by fdroid for some future releases.

9
UPDATING.md Normal file
View File

@@ -0,0 +1,9 @@
1. Delete everything in the "Droid-ify/Releases" Repo
2. Put new version of Droid-ify from the GitHub's "Release"-Tab in the "Droid-ify/Releases" Repo
3. Commit the changes and push it
4. Run these commands in the "FeloStore/FeloStore" Repo:
- git fetch upstream
- git checkout main
- git merge upstream/main

View File

@@ -1,10 +1,10 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
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,59 +12,68 @@ plugins {
}
android {
val latestVersionName = "0.6.6"
namespace = "com.looker.droidify"
val latestVersionName = "0.0.3"
namespace = "de.felitendo.felostore"
buildToolsVersion = "35.0.0"
compileSdk = 35
defaultConfig {
minSdk = 23
targetSdk = 35
applicationId = "com.looker.droidify"
versionCode = 660
applicationId = "de.felitendo.felostore"
versionCode = 650
versionName = latestVersionName
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner = "com.looker.droidify.TestRunner"
vectorDrawables.useSupportLibrary = false
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
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)
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.generateKotlin", "true")
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"))
}
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
resValue("string", "application_name", "Droid-ify-Debug")
resValue("string", "application_name", "Felo Store")
}
release {
isMinifyEnabled = true
isShrinkResources = true
resValue("string", "application_name", "Droid-ify")
resValue("string", "application_name", "Felo Store")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard.pro",
"proguard.pro"
)
}
create("alpha") {
initWith(getByName("debug"))
applicationIdSuffix = ".alpha"
resValue("string", "application_name", "Droid-ify Alpha")
resValue("string", "application_name", "Felo Store")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard.pro",
"proguard.pro"
)
isDebuggable = true
isMinifyEnabled = true
@@ -73,7 +82,7 @@ android {
buildConfigField(
type = "String",
name = "VERSION_NAME",
value = "\"v$latestVersionName\"",
value = "\"v$latestVersionName\""
)
}
}
@@ -102,6 +111,18 @@ android {
}
}
ktlint {
android.set(true)
ignoreFailures.set(true)
debug.set(true)
reporters {
reporter(ReporterType.HTML)
}
filter {
exclude("**/generated/**")
}
}
dependencies {
coreLibraryDesugaring(libs.desugaring)
@@ -123,17 +144,19 @@ dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.datetime)
implementation(libs.bundles.coroutines)
implementation(libs.coroutines.core)
implementation(libs.coroutines.android)
implementation(libs.coroutines.guava)
implementation(libs.libsu.core)
implementation(libs.bundles.shizuku)
implementation(libs.shizuku.api)
api(libs.shizuku.provider)
implementation(libs.jackson.core)
implementation(libs.serialization)
implementation(libs.bundles.ktor)
implementation(libs.bundles.room)
ksp(libs.room.compiler)
implementation(libs.ktor.core)
implementation(libs.ktor.okhttp)
implementation(libs.work.ktx)
@@ -146,16 +169,13 @@ dependencies {
testImplementation(platform(libs.junit.bom))
testImplementation(libs.bundles.test.unit)
testRuntimeOnly(libs.junit.platform)
androidTestImplementation(libs.hilt.test)
androidTestImplementation(libs.room.test)
androidTestImplementation(platform(libs.junit.bom))
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()

View File

@@ -1,58 +0,0 @@
<?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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,34 +0,0 @@
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"))
}
}

View File

@@ -1,116 +0,0 @@
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(),
)

View File

@@ -1,16 +0,0 @@
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)
}
}

View File

@@ -1,21 +1,15 @@
package com.looker.droidify.index
package de.felitendo.felostore.index
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 de.felitendo.felostore.model.Repository
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)
@@ -27,9 +21,7 @@ class RepositoryUpdaterTest {
@Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext
Database.init(context)
RepositoryUpdater.init(CoroutineScope(Dispatchers.Default), FakeDownloader)
context = InstrumentationRegistry.getInstrumentation().context
repository = Repository(
id = 15,
address = "https://apt.izzysoft.de/fdroid/repo",
@@ -49,14 +41,13 @@ class RepositoryUpdaterTest {
@Test
fun processFile() {
val output = benchmark(1) {
testRepetition(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 {
@@ -74,4 +65,28 @@ 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

View File

@@ -1,11 +1,11 @@
package com.looker.droidify.sync
package de.felitendo.felostore.sync
import com.looker.droidify.network.Downloader
import com.looker.droidify.network.NetworkResponse
import com.looker.droidify.network.ProgressListener
import com.looker.droidify.network.header.HeadersBuilder
import com.looker.droidify.network.validation.FileValidator
import com.looker.droidify.sync.common.assets
import de.felitendo.felostore.network.Downloader
import de.felitendo.felostore.network.NetworkResponse
import de.felitendo.felostore.network.ProgressListener
import de.felitendo.felostore.network.header.HeadersBuilder
import de.felitendo.felostore.network.validation.FileValidator
import de.felitendo.felostore.sync.common.assets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext

View File

@@ -1,19 +1,19 @@
package com.looker.droidify.sync
package de.felitendo.felostore.sync
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
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.assets
import com.looker.droidify.sync.common.downloadIndex
import com.looker.droidify.sync.common.benchmark
import com.looker.droidify.sync.v2.EntryParser
import com.looker.droidify.sync.v2.EntrySyncable
import com.looker.droidify.sync.v2.model.Entry
import com.looker.droidify.sync.v2.model.IndexV2
import de.felitendo.felostore.domain.model.Repo
import de.felitendo.felostore.sync.common.IndexJarValidator
import de.felitendo.felostore.sync.common.Izzy
import de.felitendo.felostore.sync.common.JsonParser
import de.felitendo.felostore.sync.common.assets
import de.felitendo.felostore.sync.common.downloadIndex
import de.felitendo.felostore.sync.common.benchmark
import de.felitendo.felostore.sync.v2.EntryParser
import de.felitendo.felostore.sync.v2.EntrySyncable
import de.felitendo.felostore.sync.v2.model.Entry
import de.felitendo.felostore.sync.v2.model.IndexV2
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -44,7 +44,7 @@ class EntrySyncableTest {
@OptIn(ExperimentalSerializationApi::class)
@Before
fun before() {
context = InstrumentationRegistry.getInstrumentation().targetContext
context = InstrumentationRegistry.getInstrumentation().context
dispatcher = StandardTestDispatcher()
validator = IndexJarValidator(dispatcher)
parser = EntryParser(dispatcher, JsonParser, validator)

View File

@@ -1,6 +1,6 @@
package com.looker.droidify.sync
package de.felitendo.felostore.sync
import com.looker.droidify.domain.model.Fingerprint
import de.felitendo.felostore.domain.model.Fingerprint
import java.util.jar.JarEntry
val FakeIndexValidator = object : IndexValidator {

View File

@@ -1,24 +1,23 @@
package com.looker.droidify.sync
package de.felitendo.felostore.sync
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
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.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
import com.looker.droidify.sync.v1.model.IndexV1
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 de.felitendo.felostore.domain.model.Repo
import de.felitendo.felostore.sync.common.IndexJarValidator
import de.felitendo.felostore.sync.common.Izzy
import de.felitendo.felostore.sync.common.JsonParser
import de.felitendo.felostore.sync.common.downloadIndex
import de.felitendo.felostore.sync.common.benchmark
import de.felitendo.felostore.sync.common.toV2
import de.felitendo.felostore.sync.v1.V1Parser
import de.felitendo.felostore.sync.v1.V1Syncable
import de.felitendo.felostore.sync.v1.model.IndexV1
import de.felitendo.felostore.sync.v2.V2Parser
import de.felitendo.felostore.sync.v2.model.FileV2
import de.felitendo.felostore.sync.v2.model.IndexV2
import de.felitendo.felostore.sync.v2.model.MetadataV2
import de.felitendo.felostore.sync.v2.model.VersionV2
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -29,8 +28,6 @@ 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 {
@@ -45,7 +42,7 @@ class V1SyncableTest {
@Before
fun before() {
context = InstrumentationRegistry.getInstrumentation().targetContext
context = InstrumentationRegistry.getInstrumentation().context
dispatcher = StandardTestDispatcher()
validator = IndexJarValidator(dispatcher)
parser = V1Parser(dispatcher, JsonParser, validator)
@@ -105,38 +102,9 @@ class V1SyncableTest {
testIndexConversion("index-v1.jar", "index-v2-updated.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)
}
// @Test
fun v1tov2FDroidRepo() = runTest(dispatcher) {
testIndexConversion("fdroid-index-v1.jar", "fdroid-index-v2.json")
}
private suspend fun testIndexConversion(
@@ -284,8 +252,6 @@ 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)
@@ -295,13 +261,7 @@ 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 },

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.sync.common
package de.felitendo.felostore.sync.common
import kotlin.math.pow
import kotlin.math.sqrt
@@ -8,6 +8,11 @@ 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()
@@ -15,19 +20,11 @@ internal inline fun benchmark(
times[iteration] = block().toDouble()
}
val meanAndDeviation = times.culledMeanAndDeviation()
return buildString(200) {
return buildString {
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))

View File

@@ -1,12 +1,12 @@
package com.looker.droidify.sync.common
package de.felitendo.felostore.sync.common
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 de.felitendo.felostore.domain.model.Authentication
import de.felitendo.felostore.domain.model.Fingerprint
import de.felitendo.felostore.domain.model.Repo
import de.felitendo.felostore.domain.model.VersionInfo
val Izzy = Repo(
id = 1,
id = 1L,
enabled = true,
address = "https://apt.izzysoft.de/fdroid/repo",
name = "IzzyOnDroid F-Droid Repo",
@@ -15,4 +15,6 @@ val Izzy = Repo(
authentication = Authentication("", ""),
versionInfo = VersionInfo(0L, null),
mirrors = emptyList(),
antiFeatures = emptyList(),
categories = emptyList(),
)

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.sync.common
package de.felitendo.felostore.sync.common
import androidx.test.platform.app.InstrumentationRegistry
import java.io.InputStream

File diff suppressed because one or more lines are too long

View File

@@ -26,7 +26,7 @@
android:required="false" />
<application
android:name=".Droidify"
android:name=".FeloStore"
android:allowBackup="true"
android:banner="@drawable/tv_banner"
android:enableOnBackInvokedCallback="true"
@@ -38,7 +38,7 @@
tools:ignore="UnusedAttribute">
<receiver
android:name=".Droidify$BootReceiver"
android:name=".FeloStore$BootReceiver"
android:exported="true">
<intent-filter>
@@ -97,7 +97,7 @@
<data android:pathPattern="/.*/packages/.*" />
</intent-filter>
<intent-filter android:autoVerify="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -116,7 +116,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="droidify.eu.org" />
<data android:host="felostore.eu.org" />
<data android:pathPattern="/app/.*" />
</intent-filter>

View File

@@ -1,61 +0,0 @@
@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
}

View File

@@ -1,83 +0,0 @@
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()

View File

@@ -1,61 +0,0 @@
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)
}
}

View File

@@ -1,21 +0,0 @@
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)
}
}

View File

@@ -1,189 +0,0 @@
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)
}

View File

@@ -1,16 +0,0 @@
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?
}

View File

@@ -1,181 +0,0 @@
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>)
}

View File

@@ -1,20 +0,0 @@
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)
}

View File

@@ -1,71 +0,0 @@
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,
)
}
}

View File

@@ -1,110 +0,0 @@
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,
)

View File

@@ -1,26 +0,0 @@
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,
)

View File

@@ -1,33 +0,0 @@
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,
)

View File

@@ -1,71 +0,0 @@
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,
)
}
}

View File

@@ -1,100 +0,0 @@
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

View File

@@ -1,83 +0,0 @@
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

View File

@@ -1,13 +0,0 @@
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,
)

View File

@@ -1,56 +0,0 @@
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,
)

View File

@@ -1,36 +0,0 @@
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,
)

View File

@@ -1,54 +0,0 @@
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,
)

View File

@@ -1,99 +0,0 @@
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

View File

@@ -1,74 +0,0 @@
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()
}

View File

@@ -1,236 +0,0 @@
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")
}
}
}
}

View File

@@ -1,35 +0,0 @@
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)",
)
}
}

View File

@@ -1,26 +0,0 @@
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
)
}
}

View File

@@ -1,11 +0,0 @@
package com.looker.droidify.datastore.model
// todo: Add Support for sorting by size
enum class SortOrder {
UPDATED,
ADDED,
NAME,
SIZE,
}
fun supportedSortOrders(): List<SortOrder> = listOf(SortOrder.UPDATED, SortOrder.ADDED, SortOrder.NAME)

View File

@@ -1,51 +0,0 @@
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()
}

View File

@@ -1,23 +0,0 @@
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)
}

View File

@@ -1,57 +0,0 @@
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()
}

View File

@@ -1,13 +0,0 @@
package com.looker.droidify.installer.installers
import com.looker.droidify.domain.model.PackageName
import com.looker.droidify.installer.model.InstallItem
import com.looker.droidify.installer.model.InstallState
interface Installer : AutoCloseable {
suspend fun install(installItem: InstallItem): InstallState
suspend fun uninstall(packageName: PackageName)
}

View File

@@ -1,101 +0,0 @@
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,
private val settingsRepository: SettingsRepository
) : Installer {
companion object {
private const val APK_MIME = "application/vnd.android.package-archive"
}
override suspend fun install(
installItem: InstallItem,
): InstallState {
val installFlag = if (SdkCheck.isNougat) Intent.FLAG_GRANT_READ_URI_PERMISSION else 0
val fileUri = if (SdkCheck.isNougat) {
Cache.getReleaseUri(context, installItem.installFileName)
} else {
Cache.getReleaseFile(context, installItem.installFileName).toUri()
}
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) =
context.uninstallPackage(packageName)
override fun close() {}
}
suspend fun Context.uninstallPackage(packageName: PackageName) =
suspendCancellableCoroutine { cont ->
try {
startActivity(
intent(Intent.ACTION_UNINSTALL_PACKAGE) {
data = "package:${packageName.name}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
)
cont.resume(Unit)
} catch (e: Exception) {
e.printStackTrace()
cont.resume(Unit)
}
}

View File

@@ -1,410 +0,0 @@
package com.looker.droidify.model
import java.net.URL
data class Repository(
var id: Long,
val address: String,
val mirrors: List<String>,
val name: String,
val description: String,
val version: Int,
val enabled: Boolean,
val fingerprint: String,
val lastModified: String,
val entityTag: String,
val updated: Long,
val timestamp: Long,
val authentication: String,
) {
fun edit(address: String, fingerprint: String, authentication: String): Repository {
val isAddressChanged = this.address != address
val isFingerprintChanged = this.fingerprint != fingerprint
val shouldForceUpdate = isAddressChanged || isFingerprintChanged
return copy(
address = address,
fingerprint = fingerprint,
lastModified = if (shouldForceUpdate) "" else lastModified,
entityTag = if (shouldForceUpdate) "" else entityTag,
authentication = authentication
)
}
fun update(
mirrors: List<String>,
name: String,
description: String,
version: Int,
lastModified: String,
entityTag: String,
timestamp: Long,
): Repository {
return copy(
mirrors = mirrors,
name = name,
description = description,
version = if (version >= 0) version else this.version,
lastModified = lastModified,
entityTag = entityTag,
updated = System.currentTimeMillis(),
timestamp = timestamp
)
}
fun enable(enabled: Boolean): Repository {
return copy(enabled = enabled, lastModified = "", entityTag = "")
}
@Suppress("SpellCheckingInspection")
companion object {
fun newRepository(
address: String,
fingerprint: String,
authentication: String,
): Repository {
val name = try {
URL(address).let { "${it.host}${it.path}" }
} catch (e: Exception) {
address
}
return defaultRepository(address, name, "", 0, true, fingerprint, authentication)
}
fun defaultRepository(
address: String,
name: String,
description: String,
version: Int = 21,
enabled: Boolean = false,
fingerprint: String,
authentication: String = "",
): Repository {
return Repository(
-1, address, emptyList(), name, description, version, enabled,
fingerprint, "", "", 0L, 0L, authentication
)
}
val defaultRepositories = listOf(
defaultRepository(
address = "https://f-droid.org/repo",
name = "F-Droid",
description = "The official F-Droid Free Software repos" +
"itory. Everything in this repository is always buil" +
"t from the source code.",
enabled = true,
fingerprint = "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB"
),
defaultRepository(
address = "https://f-droid.org/archive",
name = "F-Droid Archive",
description = "The archive of the official F-Droid Free" +
" Software repository. Apps here are old and can co" +
"ntain known vulnerabilities and security issues!",
fingerprint = "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB"
),
defaultRepository(
address = "https://guardianproject.info/fdroid/repo",
name = "Guardian Project Official Releases",
description = "The official repository of The Guardian " +
"Project apps for use with the F-Droid client. Appl" +
"ications in this repository are official binaries " +
"built by the original application developers and " +
"signed by the same key as the APKs that are relea" +
"sed in the Google Play Store.",
fingerprint = "B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135"
),
defaultRepository(
address = "https://guardianproject.info/fdroid/archive",
name = "Guardian Project Archive",
description = "The official repository of The Guardian Pr" +
"oject apps for use with the F-Droid client. This con" +
"tains older versions of applications from the main repository.",
fingerprint = "B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135"
),
defaultRepository(
address = "https://apt.izzysoft.de/fdroid/repo",
name = "IzzyOnDroid F-Droid Repo",
description = "This is a repository of apps to be used with" +
" F-Droid the original application developers, taken" +
" from the resp. repositories (mostly GitHub). At thi" +
"s moment I cannot give guarantees on regular updates" +
" for all of them, though most are checked multiple times a week ",
enabled = true,
fingerprint = "3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A"
),
defaultRepository(
address = "https://microg.org/fdroid/repo",
name = "microG Project",
description = "The official repository for microG." +
" microG is a lightweight open source implementation" +
" of Google Play Services.",
fingerprint = "9BD06727E62796C0130EB6DAB39B73157451582CBD138E86C468ACC395D14165"
),
defaultRepository(
address = "https://repo.netsyms.com/fdroid/repo",
name = "Netsyms Technologies",
description = "Official collection of open-source apps created" +
" by Netsyms Technologies.",
fingerprint = "2581BA7B32D3AB443180C4087CAB6A7E8FB258D3A6E98870ECB3C675E4D64489"
),
defaultRepository(
address = "https://molly.im/fdroid/foss/fdroid/repo",
name = "Molly",
description = "The official repository for Molly. " +
"Molly is a fork of Signal focused on security.",
fingerprint = "5198DAEF37FC23C14D5EE32305B2AF45787BD7DF2034DE33AD302BDB3446DF74"
),
defaultRepository(
address = "https://archive.newpipe.net/fdroid/repo",
name = "NewPipe",
description = "The official repository for NewPipe." +
" NewPipe is a lightweight client for Youtube, PeerTube" +
", Soundcloud, etc.",
fingerprint = "E2402C78F9B97C6C89E97DB914A2751FDA1D02FE2039CC0897A462BDB57E7501"
),
defaultRepository(
address = "https://www.collaboraoffice.com/downloads/fdroid/repo",
name = "Collabora Office",
description = "Collabora Office is an office suite based on LibreOffice.",
fingerprint = "573258C84E149B5F4D9299E7434B2B69A8410372921D4AE586BA91EC767892CC"
),
defaultRepository(
address = "https://cdn.kde.org/android/fdroid/repo",
name = "KDE Android",
description = "The official nightly repository for KDE Android apps.",
fingerprint = "B3EBE10AFA6C5C400379B34473E843D686C61AE6AD33F423C98AF903F056523F"
),
defaultRepository(
address = "https://calyxos.gitlab.io/calyx-fdroid-repo/fdroid/repo",
name = "Calyx OS Repo",
description = "The official Calyx Labs F-Droid repository.",
fingerprint = "C44D58B4547DE5096138CB0B34A1CC99DAB3B4274412ED753FCCBFC11DC1B7B6"
),
defaultRepository(
address = "https://divestos.org/fdroid/official",
name = "Divest OS Repo",
description = "The official Divest OS F-Droid repository.",
fingerprint = "E4BE8D6ABFA4D9D4FEEF03CDDA7FF62A73FD64B75566F6DD4E5E577550BE8467"
),
defaultRepository(
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" +
" software social networks.",
fingerprint = "11F0A69910A4280E2CD3CCC3146337D006BE539B18E1A9FEACE15FF757A94FEB"
),
defaultRepository(
address = "https://store.nethunter.com/repo",
name = "Kali Nethunter",
description = "Kali Nethunter's official selection of original b" +
"inaries.",
fingerprint = "FE7A23DFC003A1CF2D2ADD2469B9C0C49B206BA5DC9EDD6563B3B7EB6A8F5FAB"
),
defaultRepository(
address = "https://thecapslock.gitlab.io/fdroid-patched-apps/fdroid/repo",
name = "Patched Apps",
description = "A collection of patched applications to provid" +
"e better compatibility, privacy etc..",
fingerprint = "313D9E6E789FF4E8E2D687AAE31EEF576050003ED67963301821AC6D3763E3AC"
),
defaultRepository(
address = "https://mobileapp.bitwarden.com/fdroid/repo",
name = "Bitwarden",
description = "The official repository for Bitwarden. Bitward" +
"en is a password manager.",
fingerprint = "BC54EA6FD1CD5175BCCCC47C561C5726E1C3ED7E686B6DB4B18BAC843A3EFE6C"
),
defaultRepository(
address = "https://briarproject.org/fdroid/repo",
name = "Briar",
description = "The official repository for Briar. Briar is a" +
" serverless/offline messenger that focused on privacy, s" +
"ecurity, and decentralization.",
fingerprint = "1FB874BEE7276D28ECB2C9B06E8A122EC4BCB4008161436CE474C257CBF49BD6"
),
defaultRepository(
address = "https://guardianproject-wind.s3.amazonaws.com/fdroid/repo",
name = "Wind Project",
description = "A collection of interesting offline/serverless apps.",
fingerprint = "182CF464D219D340DA443C62155198E399FEC1BC4379309B775DD9FC97ED97E1"
),
defaultRepository(
address = "https://nanolx.org/fdroid/repo",
name = "NanoDroid",
description = "A companion repository to microG's installer.",
fingerprint = "862ED9F13A3981432BF86FE93D14596B381D75BE83A1D616E2D44A12654AD015"
),
defaultRepository(
address = "https://releases.threema.ch/fdroid/repo",
name = "Threema Libre",
description = "The official repository for Threema Libre. R" +
"equires Threema Shop license. Threema Libre is an open" +
"-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 messenger focused on security and privacy.",
fingerprint = "DB0E5297EB65CC22D6BD93C869943BDCFCB6A07DC69A48A0DD8C7BA698EC04E6"
),
defaultRepository(
address = "https://www.cromite.org/fdroid/repo",
name = "Cromite",
description = "The official repository for Cromite. Cromite" +
" is a Chromium with ad blocking and enhanced privacy.",
fingerprint = "49F37E74DEE483DCA2B991334FB5A0200787430D0B5F9A783DD5F13695E9517B"
),
defaultRepository(
address = "https://fdroid.twinhelix.com/fdroid/repo",
name = "TwinHelix",
description = "TwinHelix F-Droid Repository, used for Signa" +
"l-FOSS, an open-source fork of Signal Private Messenger.",
fingerprint = "7b03b0232209b21b10a30a63897d3c6bca4f58fe29bc3477e8e3d8cf8e304028"
),
defaultRepository(
address = "https://fdroid.typeblog.net",
name = "PeterCxy's F-Droid",
description = "You have landed on PeterCxy's F-Droid repo. T" +
"o use this repository, please add the page's URL to your F-Droid client.",
fingerprint = "1a7e446c491c80bc2f83844a26387887990f97f2f379ae7b109679feae3dbc8c"
),
defaultRepository(
address = "https://s2.spiritcroc.de/fdroid/repo",
name = "SpiritCroc.de",
description = "While some of my apps are available from" +
" the official F-Droid repository, I also maintain my" +
" own repository for a small selection of apps. These" +
" might be forks of other apps with only minor change" +
"s, or apps that are not published on the Play Store f" +
"or other reasons. In contrast to the official F-Droid" +
" repos, these might also include proprietary librarie" +
"s, e.g. for push notifications.",
fingerprint = "6612ade7e93174a589cf5ba26ed3ab28231a789640546c8f30375ef045bc9242"
),
defaultRepository(
address = "https://s2.spiritcroc.de/testing/fdroid/repo",
name = "SpiritCroc.de Test Builds",
description = "SpiritCroc.de Test Builds",
fingerprint = "52d03f2fab785573bb295c7ab270695e3a1bdd2adc6a6de8713250b33f231225"
),
defaultRepository(
address = "https://static.cryptomator.org/android/fdroid/repo",
name = "Cryptomator",
description = "No Description",
fingerprint = "f7c3ec3b0d588d3cb52983e9eb1a7421c93d4339a286398e71d7b651e8d8ecdd"
),
defaultRepository(
address = "https://divestos.org/apks/unofficial/fdroid/repo",
name = "DivestOS Unofficial",
description = "This repository contains unofficial builds of open source apps" +
" that are not included in the other repos.",
fingerprint = "a18cdb92f40ebfbbf778a54fd12dbd74d90f1490cb9ef2cc6c7e682dd556855d"
),
defaultRepository(
address = "https://cdn.kde.org/android/stable-releases/fdroid/repo",
name = "KDE Stables",
description = "This repository contains unofficial builds of open source apps" +
" that are not included in the other repos.",
fingerprint = "13784ba6c80ff4e2181e55c56f961eed5844cea16870d3b38d58780b85e1158f"
),
defaultRepository(
address = "https://zimbelstern.eu/fdroid/repo",
name = "Zimbelstern's F-Droid repository",
description = "This is the official repository of apps from zimbelstern.eu," +
" to be used with F-Droid.",
fingerprint = "285158DECEF37CB8DE7C5AF14818ACBF4A9B1FBE63116758EFC267F971CA23AA"
),
defaultRepository(
address = "https://app.simplex.chat/fdroid/repo",
name = "SimpleX Chat F-Droid",
description = "SimpleX Chat official F-Droid repository.",
fingerprint = "9F358FF284D1F71656A2BFAF0E005DEAE6AA14143720E089F11FF2DDCFEB01BA"
),
defaultRepository(
address = "https://f-droid.monerujo.io/fdroid/repo",
name = "Monerujo Wallet",
description = "Monerujo Monero Wallet official F-Droid repository.",
fingerprint = "A82C68E14AF0AA6A2EC20E6B272EFF25E5A038F3F65884316E0F5E0D91E7B713"
),
defaultRepository(
address = "https://fdroid.cakelabs.com/fdroid/repo",
name = "Cake Labs",
description = "Cake Labs official F-Droid repository for Cake Wallet and Monero.com",
fingerprint = "EA44EFAEE0B641EE7A032D397D5D976F9C4E5E1ED26E11C75702D064E55F8755"
),
defaultRepository(
address = "https://app.futo.org/fdroid/repo",
name = "FUTO",
description = "FUTO official F-Droid repository.",
fingerprint = "39D47869D29CBFCE4691D9F7E6946A7B6D7E6FF4883497E6E675744ECDFA6D6D"
),
defaultRepository(
address = "https://fdroid.mm20.de/repo",
name = "MM20 Apps",
description = "Apps developed and distributed by MM20",
fingerprint = "156FBAB952F6996415F198F3F29628D24B30E725B0F07A2B49C3A9B5161EEE1A"
),
defaultRepository(
address = "https://breezy-weather.github.io/fdroid-repo/fdroid/repo",
name = "Breezy Weather",
description = "The F-Droid repository for Breezy Weather",
fingerprint = "3480A7BB2A296D8F98CB90D2309199B5B9519C1B31978DBCD877ADB102AF35EE"
),
defaultRepository(
address = "https://gh.artemchep.com/keyguard-repo-fdroid/repo",
name = "Keyguard Project",
description = "Mirrors artifacts available on https://github.com/AChep/keyguard-app/releases",
fingerprint = "03941CE79B081666609C8A48AB6E46774263F6FC0BBF1FA046CCFFC60EA643BC"
),
defaultRepository(
address = "https://f5a.torus.icu/fdroid/repo",
name = "Fcitx 5 For Android F-Droid Repo",
description = "Out-of-tree fcitx5-android plugins.",
fingerprint = "5D87CE1FAD3772425C2A7ED987A57595A20B07543B9595A7FD2CED25DFF3CF12"
),
defaultRepository(
address = "https://fdroid.i2pd.xyz/fdroid/repo/",
name = "PurpleI2P F-Droid repository",
description = "This is a repository of PurpleI2P. It contains applications developed and supported by our team.",
fingerprint = "5D87CE1FAD3772425C2A7ED987A57595A20B07543B9595A7FD2CED25DFF3CF12"
),
)
val newlyAdded: List<Repository> = listOf(
defaultRepository(
address = "https://fdroid.ironfoxoss.org/fdroid/repo",
name = "IronFox",
description = "The official repository for IronFox:" +
" A privacy and security-oriented Firefox-based browser for Android.",
fingerprint = "C5E291B5A571F9C8CD9A9799C2C94E02EC9703948893F2CA756D67B94204F904"
),
defaultRepository(
address = "https://raw.githubusercontent.com/chrisgch/tca/master/fdroid/repo",
name = "Total Commander",
description = "The official repository for Total Commander",
fingerprint = "3576596CECDD70488D61CFD90799A49B7FFD26A81A8FEF1BADEC88D069FA72C1"
),
defaultRepository(
address = "https://www.cromite.org/fdroid/repo",
name = "Cromite",
description = "The official repository for Cromite. " +
"Cromite is a Chromium fork based on Bromite with " +
"built-in support for ad blocking and an eye for privacy.",
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"
)
}
}

View File

@@ -1,26 +0,0 @@
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)
}
}

View File

@@ -1,15 +0,0 @@
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
interface Syncable<T> {
val parser: Parser<T>
suspend fun sync(
repo: Repo,
): Pair<Fingerprint, IndexV2?>
}

View File

@@ -1,44 +0,0 @@
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
}

View File

@@ -1,17 +0,0 @@
package com.looker.droidify.utility.extension
import android.content.pm.PackageInfo
import com.looker.droidify.utility.common.extension.calculateHash
import com.looker.droidify.utility.common.extension.singleSignature
import com.looker.droidify.utility.common.extension.versionCodeCompat
import com.looker.droidify.model.InstalledItem
fun PackageInfo.toInstalledItem(): InstalledItem {
val signatureString = singleSignature?.calculateHash().orEmpty()
return InstalledItem(
packageName,
versionName.orEmpty(),
versionCodeCompat,
signatureString
)
}

View File

@@ -1,4 +1,4 @@
package com.looker.droidify
package de.felitendo.felostore
import android.annotation.SuppressLint
import android.app.Application
@@ -21,30 +21,30 @@ import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.memory.MemoryCache
import coil3.request.crossfade
import com.looker.droidify.content.ProductPreferences
import com.looker.droidify.database.Database
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.datastore.get
import com.looker.droidify.datastore.model.AutoSync
import com.looker.droidify.datastore.model.ProxyPreference
import com.looker.droidify.datastore.model.ProxyType
import com.looker.droidify.index.RepositoryUpdater
import com.looker.droidify.installer.InstallManager
import com.looker.droidify.network.Downloader
import com.looker.droidify.receivers.InstalledAppReceiver
import com.looker.droidify.service.Connection
import com.looker.droidify.service.SyncService
import com.looker.droidify.sync.SyncPreference
import com.looker.droidify.sync.toJobNetworkType
import com.looker.droidify.utility.common.Constants
import com.looker.droidify.utility.common.SdkCheck
import com.looker.droidify.utility.common.cache.Cache
import com.looker.droidify.utility.common.extension.getDrawableCompat
import com.looker.droidify.utility.common.extension.getInstalledPackagesCompat
import com.looker.droidify.utility.common.extension.jobScheduler
import com.looker.droidify.utility.common.log
import com.looker.droidify.utility.extension.toInstalledItem
import com.looker.droidify.work.CleanUpWorker
import de.felitendo.felostore.content.ProductPreferences
import de.felitendo.felostore.database.Database
import de.felitendo.felostore.datastore.SettingsRepository
import de.felitendo.felostore.datastore.get
import de.felitendo.felostore.datastore.model.AutoSync
import de.felitendo.felostore.datastore.model.ProxyPreference
import de.felitendo.felostore.datastore.model.ProxyType
import de.felitendo.felostore.index.RepositoryUpdater
import de.felitendo.felostore.installer.InstallManager
import de.felitendo.felostore.network.Downloader
import de.felitendo.felostore.receivers.InstalledAppReceiver
import de.felitendo.felostore.service.Connection
import de.felitendo.felostore.service.SyncService
import de.felitendo.felostore.sync.SyncPreference
import de.felitendo.felostore.sync.toJobNetworkType
import de.felitendo.felostore.utility.common.Constants
import de.felitendo.felostore.utility.common.SdkCheck
import de.felitendo.felostore.utility.common.cache.Cache
import de.felitendo.felostore.utility.common.extension.getDrawableCompat
import de.felitendo.felostore.utility.common.extension.getInstalledPackagesCompat
import de.felitendo.felostore.utility.common.extension.jobScheduler
import de.felitendo.felostore.utility.common.log
import de.felitendo.felostore.utility.extension.toInstalledItem
import de.felitendo.felostore.work.CleanUpWorker
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -60,7 +60,7 @@ import kotlin.time.Duration.Companion.INFINITE
import kotlin.time.Duration.Companion.hours
@HiltAndroidApp
class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Provider {
class FeloStore : Application(), SingletonImageLoader.Factory, Configuration.Provider {
private val parentJob = SupervisorJob()
private val appScope = CoroutineScope(Dispatchers.Default + parentJob)
@@ -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,13 +212,10 @@ 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() {
@@ -259,12 +256,12 @@ fun strictThreadPolicy() {
.detectNetwork()
.detectUnbufferedIo()
.penaltyLog()
.build(),
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build(),
.build()
)
}

View File

@@ -1,4 +1,4 @@
package com.looker.droidify
package de.felitendo.felostore
import android.content.Intent
import android.os.Build
@@ -14,26 +14,26 @@ import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.looker.droidify.database.CursorOwner
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.datastore.extension.getThemeRes
import com.looker.droidify.datastore.get
import com.looker.droidify.installer.InstallManager
import com.looker.droidify.installer.model.installFrom
import com.looker.droidify.ui.appDetail.AppDetailFragment
import com.looker.droidify.ui.favourites.FavouritesFragment
import com.looker.droidify.ui.repository.EditRepositoryFragment
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 de.felitendo.felostore.utility.common.DeeplinkType
import de.felitendo.felostore.utility.common.SdkCheck
import de.felitendo.felostore.utility.common.deeplinkType
import de.felitendo.felostore.utility.common.extension.homeAsUp
import de.felitendo.felostore.utility.common.extension.inputManager
import de.felitendo.felostore.utility.common.getInstallPackageName
import de.felitendo.felostore.utility.common.requestNotificationPermission
import de.felitendo.felostore.database.CursorOwner
import de.felitendo.felostore.datastore.SettingsRepository
import de.felitendo.felostore.datastore.extension.getThemeRes
import de.felitendo.felostore.datastore.get
import de.felitendo.felostore.installer.InstallManager
import de.felitendo.felostore.installer.model.installFrom
import de.felitendo.felostore.ui.appDetail.AppDetailFragment
import de.felitendo.felostore.ui.favourites.FavouritesFragment
import de.felitendo.felostore.ui.repository.EditRepositoryFragment
import de.felitendo.felostore.ui.repository.RepositoriesFragment
import de.felitendo.felostore.ui.repository.RepositoryFragment
import de.felitendo.felostore.ui.settings.SettingsFragment
import de.felitendo.felostore.ui.tabsFragment.TabsFragment
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,25 +87,24 @@ 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()
}
@@ -117,11 +116,9 @@ 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)
@@ -191,7 +188,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)
@@ -205,8 +202,8 @@ class MainActivity : AppCompatActivity() {
FragmentStackItem(
it::class.java.name,
it.arguments,
supportFragmentManager.saveFragmentInstanceState(it),
),
supportFragmentManager.saveFragmentInstanceState(it)
)
)
}
replaceFragment(fragment, true)

View File

@@ -1,14 +1,14 @@
package com.looker.droidify.content
package de.felitendo.felostore.content
import android.content.Context
import android.content.SharedPreferences
import com.looker.droidify.utility.common.extension.Json
import com.looker.droidify.utility.common.extension.parseDictionary
import com.looker.droidify.utility.common.extension.writeDictionary
import com.looker.droidify.model.ProductPreference
import com.looker.droidify.database.Database
import com.looker.droidify.utility.serialization.productPreference
import com.looker.droidify.utility.serialization.serialize
import de.felitendo.felostore.utility.common.extension.Json
import de.felitendo.felostore.utility.common.extension.parseDictionary
import de.felitendo.felostore.utility.common.extension.writeDictionary
import de.felitendo.felostore.model.ProductPreference
import de.felitendo.felostore.database.Database
import de.felitendo.felostore.utility.serialization.productPreference
import de.felitendo.felostore.utility.serialization.serialize
import java.io.ByteArrayOutputStream
import java.nio.charset.Charset
import kotlinx.coroutines.CoroutineScope

View File

@@ -1,49 +1,48 @@
package com.looker.droidify.database
package de.felitendo.felostore.database
import android.database.Cursor
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
import de.felitendo.felostore.datastore.model.SortOrder
import de.felitendo.felostore.model.ProductItem
@AndroidEntryPoint
class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
@Inject
lateinit var settingsRepository: SettingsRepository
sealed interface Request {
val id: Int
sealed class Request {
internal abstract val id: Int
class Available(
val searchQuery: String,
val section: ProductItem.Section,
val order: SortOrder,
override val id: Int = 1,
) : Request
) : Request() {
override val id: Int
get() = 1
}
class Installed(
val searchQuery: String,
val section: ProductItem.Section,
val order: SortOrder,
override val id: Int = 2,
) : Request
) : Request() {
override val id: Int
get() = 2
}
class Updates(
val searchQuery: String,
val section: ProductItem.Section,
val order: SortOrder,
override val id: Int = 3,
) : Request
val skipSignatureCheck: Boolean,
) : Request() {
override val id: Int
get() = 3
}
object Repositories : Request {
override val id = 4
object Repositories : Request() {
override val id: Int
get() = 4
}
}
@@ -57,6 +56,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
val cursor: Cursor?,
)
init {
retainInstance = true
}
private val activeRequests = mutableMapOf<Int, ActiveRequest>()
fun attach(callback: Callback, request: Request) {
@@ -90,7 +93,6 @@ 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
@@ -101,7 +103,6 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
section = request.section,
order = request.order,
signal = it,
skipSignatureCheck = settings.ignoreSignature,
)
is Request.Installed ->
@@ -113,7 +114,6 @@ 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 = settings.ignoreSignature,
skipSignatureCheck = request.skipSignatureCheck,
)
is Request.Repositories -> Database.RepositoryAdapter.query(it)

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.database
package de.felitendo.felostore.database
import android.content.ContentValues
import android.content.Context
@@ -9,22 +9,22 @@ 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.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
import com.looker.droidify.model.ProductItem
import com.looker.droidify.model.Repository
import com.looker.droidify.utility.common.extension.Json
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.serialization.product
import com.looker.droidify.utility.serialization.productItem
import com.looker.droidify.utility.serialization.repository
import com.looker.droidify.utility.serialization.serialize
import de.felitendo.felostore.BuildConfig
import de.felitendo.felostore.datastore.model.SortOrder
import de.felitendo.felostore.model.InstalledItem
import de.felitendo.felostore.model.Product
import de.felitendo.felostore.model.ProductItem
import de.felitendo.felostore.model.Repository
import de.felitendo.felostore.utility.common.extension.Json
import de.felitendo.felostore.utility.common.extension.asSequence
import de.felitendo.felostore.utility.common.extension.firstOrNull
import de.felitendo.felostore.utility.common.extension.parseDictionary
import de.felitendo.felostore.utility.common.extension.writeDictionary
import de.felitendo.felostore.utility.common.log
import de.felitendo.felostore.utility.serialization.product
import de.felitendo.felostore.utility.serialization.productItem
import de.felitendo.felostore.utility.serialization.repository
import de.felitendo.felostore.utility.serialization.serialize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
@@ -44,15 +44,52 @@ import kotlin.collections.set
object Database {
fun init(context: Context): Boolean {
val helper = DatabaseHelper(context)
val helper = Helper(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
object Schema {
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 Repository : Table {
const val ROW_ID = "_id"
const val ROW_ENABLED = "enabled"
@@ -153,6 +190,126 @@ object Database {
}
}
private class Helper(context: Context) : SQLiteOpenHelper(context, "felostore", 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()
@@ -207,7 +364,7 @@ object Database {
}
}
fun SQLiteDatabase.query(
private fun SQLiteDatabase.query(
table: String,
columns: Array<String>? = null,
selection: Pair<String, Array<String>>? = null,
@@ -450,19 +607,6 @@ 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,
@@ -575,7 +719,7 @@ object Database {
when (order) {
SortOrder.UPDATED -> builder += "product.${Schema.Product.ROW_UPDATED} DESC,"
SortOrder.ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC,"
else -> Unit
SortOrder.NAME -> Unit
}::class
builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC"

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.database
package de.felitendo.felostore.database
import android.database.ContentObservable
import android.database.ContentObserver

View File

@@ -1,11 +1,11 @@
package com.looker.droidify.database
package de.felitendo.felostore.database
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.os.CancellationSignal
import com.looker.droidify.BuildConfig
import com.looker.droidify.utility.common.extension.asSequence
import com.looker.droidify.utility.common.log
import de.felitendo.felostore.BuildConfig
import de.felitendo.felostore.utility.common.extension.asSequence
import de.felitendo.felostore.utility.common.log
class QueryBuilder {

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.database
package de.felitendo.felostore.database
import android.content.Context
import android.database.Cursor

View File

@@ -1,20 +1,20 @@
package com.looker.droidify.database
package de.felitendo.felostore.database
import android.content.Context
import android.net.Uri
import com.fasterxml.jackson.core.JsonToken
import com.looker.droidify.utility.common.Exporter
import com.looker.droidify.utility.common.extension.Json
import com.looker.droidify.utility.common.extension.forEach
import com.looker.droidify.utility.common.extension.forEachKey
import com.looker.droidify.utility.common.extension.parseDictionary
import com.looker.droidify.utility.common.extension.writeArray
import com.looker.droidify.utility.common.extension.writeDictionary
import com.looker.droidify.di.ApplicationScope
import com.looker.droidify.di.IoDispatcher
import com.looker.droidify.model.Repository
import com.looker.droidify.utility.serialization.repository
import com.looker.droidify.utility.serialization.serialize
import de.felitendo.felostore.utility.common.Exporter
import de.felitendo.felostore.utility.common.extension.Json
import de.felitendo.felostore.utility.common.extension.forEach
import de.felitendo.felostore.utility.common.extension.forEachKey
import de.felitendo.felostore.utility.common.extension.parseDictionary
import de.felitendo.felostore.utility.common.extension.writeArray
import de.felitendo.felostore.utility.common.extension.writeDictionary
import de.felitendo.felostore.di.ApplicationScope
import de.felitendo.felostore.di.IoDispatcher
import de.felitendo.felostore.model.Repository
import de.felitendo.felostore.utility.serialization.repository
import de.felitendo.felostore.utility.serialization.serialize
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.datastore
package de.felitendo.felostore.datastore
import android.net.Uri
import android.util.Log
@@ -12,26 +12,23 @@ import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
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
import com.looker.droidify.datastore.model.Theme
import com.looker.droidify.utility.common.Exporter
import com.looker.droidify.utility.common.extension.updateAsMutable
import de.felitendo.felostore.datastore.model.AutoSync
import de.felitendo.felostore.datastore.model.InstallerType
import de.felitendo.felostore.datastore.model.ProxyPreference
import de.felitendo.felostore.datastore.model.ProxyType
import de.felitendo.felostore.datastore.model.SortOrder
import de.felitendo.felostore.datastore.model.Theme
import de.felitendo.felostore.utility.common.Exporter
import de.felitendo.felostore.utility.common.extension.updateAsMutable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlin.time.Clock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
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>,
@@ -39,7 +36,7 @@ class PreferenceSettingsRepository(
override val data: Flow<Settings> = dataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e("PreferencesSettingsRepository", "Error reading preferences.", exception)
Log.e("TAG", "Error reading preferences.", exception)
} else {
throw exception
}
@@ -88,31 +85,6 @@ 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)
@@ -153,18 +125,6 @@ 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
@@ -194,7 +154,6 @@ class PreferenceSettingsRepository(
theme = theme,
dynamicTheme = dynamicTheme,
installerType = installerType,
legacyInstallerComponent = legacyInstallerComponent,
autoUpdate = autoUpdate,
autoSync = autoSync,
sortOrder = sortOrder,
@@ -226,9 +185,6 @@ 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")
@@ -244,28 +200,6 @@ 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)

View File

@@ -1,39 +1,35 @@
package com.looker.droidify.datastore
package de.felitendo.felostore.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 de.felitendo.felostore.datastore.model.AutoSync
import de.felitendo.felostore.datastore.model.InstallerType
import de.felitendo.felostore.datastore.model.ProxyPreference
import de.felitendo.felostore.datastore.model.SortOrder
import de.felitendo.felostore.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,
val notifyUpdate: Boolean = true,
val unstableUpdate: Boolean = false,
val ignoreSignature: Boolean = false,
val ignoreSignature: Boolean = true,
val theme: Theme = Theme.SYSTEM,
val dynamicTheme: Boolean = false,
val dynamicTheme: Boolean = true,
val installerType: InstallerType = InstallerType.Default,
val legacyInstallerComponent: LegacyInstallerComponent? = null,
val autoUpdate: Boolean = false,
val autoUpdate: Boolean = true,
val autoSync: AutoSync = AutoSync.WIFI_ONLY,
val sortOrder: SortOrder = SortOrder.UPDATED,
val proxy: ProxyPreference = ProxyPreference(),
@@ -48,7 +44,6 @@ 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 {

View File

@@ -1,12 +1,11 @@
package com.looker.droidify.datastore
package de.felitendo.felostore.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
import de.felitendo.felostore.datastore.model.AutoSync
import de.felitendo.felostore.datastore.model.InstallerType
import de.felitendo.felostore.datastore.model.ProxyType
import de.felitendo.felostore.datastore.model.SortOrder
import de.felitendo.felostore.datastore.model.Theme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@@ -38,8 +37,6 @@ interface SettingsRepository {
suspend fun setInstallerType(installerType: InstallerType)
suspend fun setLegacyInstallerComponent(component: LegacyInstallerComponent?)
suspend fun setAutoUpdate(allow: Boolean)
suspend fun setAutoSync(autoSync: AutoSync)

View File

@@ -1,9 +1,9 @@
package com.looker.droidify.datastore.exporter
package de.felitendo.felostore.datastore.exporter
import android.content.Context
import android.net.Uri
import com.looker.droidify.utility.common.Exporter
import com.looker.droidify.datastore.Settings
import de.felitendo.felostore.utility.common.Exporter
import de.felitendo.felostore.datastore.Settings
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel

View File

@@ -1,16 +1,16 @@
package com.looker.droidify.datastore.extension
package de.felitendo.felostore.datastore.extension
import android.content.Context
import android.content.res.Configuration
import com.looker.droidify.R
import com.looker.droidify.R.string as stringRes
import com.looker.droidify.R.style as styleRes
import com.looker.droidify.utility.common.SdkCheck
import com.looker.droidify.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType
import com.looker.droidify.datastore.model.ProxyType
import com.looker.droidify.datastore.model.SortOrder
import com.looker.droidify.datastore.model.Theme
import de.felitendo.felostore.R
import de.felitendo.felostore.R.string as stringRes
import de.felitendo.felostore.R.style as styleRes
import de.felitendo.felostore.utility.common.SdkCheck
import de.felitendo.felostore.datastore.model.AutoSync
import de.felitendo.felostore.datastore.model.InstallerType
import de.felitendo.felostore.datastore.model.ProxyType
import de.felitendo.felostore.datastore.model.SortOrder
import de.felitendo.felostore.datastore.model.Theme
import kotlin.time.Duration
fun Configuration.getThemeRes(theme: Theme, dynamicTheme: Boolean) = when (theme) {
@@ -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)
}
} ?: ""

View File

@@ -1,7 +1,7 @@
package com.looker.droidify.datastore.migration
package de.felitendo.felostore.datastore.migration
import com.looker.droidify.datastore.PreferenceSettingsRepository.PreferencesKeys.setting
import com.looker.droidify.datastore.Settings
import de.felitendo.felostore.datastore.PreferenceSettingsRepository.PreferencesKeys.setting
import de.felitendo.felostore.datastore.Settings
import kotlinx.coroutines.flow.first
class ProtoToPreferenceMigration(

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.datastore.model
package de.felitendo.felostore.datastore.model
enum class AutoSync {
ALWAYS,

View File

@@ -1,6 +1,6 @@
package com.looker.droidify.datastore.model
package de.felitendo.felostore.datastore.model
import com.looker.droidify.utility.common.device.Miui
import de.felitendo.felostore.utility.common.device.Miui
enum class InstallerType {
LEGACY,

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.datastore.model
package de.felitendo.felostore.datastore.model
import kotlinx.serialization.Serializable

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.datastore.model
package de.felitendo.felostore.datastore.model
enum class ProxyType {
DIRECT,

View File

@@ -0,0 +1,8 @@
package de.felitendo.felostore.datastore.model
// todo: Add Support for sorting by size
enum class SortOrder {
UPDATED,
ADDED,
NAME
}

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.datastore.model
package de.felitendo.felostore.datastore.model
enum class Theme {
SYSTEM,

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.di
package de.felitendo.felostore.di
import dagger.Module
import dagger.Provides

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.di
package de.felitendo.felostore.di
import android.content.Context
import androidx.datastore.core.DataStore
@@ -7,13 +7,13 @@ import androidx.datastore.dataStoreFile
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import com.looker.droidify.utility.common.Exporter
import com.looker.droidify.datastore.PreferenceSettingsRepository
import com.looker.droidify.datastore.Settings
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.datastore.SettingsSerializer
import com.looker.droidify.datastore.exporter.SettingsExporter
import com.looker.droidify.datastore.migration.ProtoToPreferenceMigration
import de.felitendo.felostore.utility.common.Exporter
import de.felitendo.felostore.datastore.PreferenceSettingsRepository
import de.felitendo.felostore.datastore.Settings
import de.felitendo.felostore.datastore.SettingsRepository
import de.felitendo.felostore.datastore.SettingsSerializer
import de.felitendo.felostore.datastore.exporter.SettingsExporter
import de.felitendo.felostore.datastore.migration.ProtoToPreferenceMigration
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn

View File

@@ -1,8 +1,8 @@
package com.looker.droidify.di
package de.felitendo.felostore.di
import android.content.Context
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.installer.InstallManager
import de.felitendo.felostore.datastore.SettingsRepository
import de.felitendo.felostore.installer.InstallManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn

View File

@@ -1,7 +1,7 @@
package com.looker.droidify.di
package de.felitendo.felostore.di
import com.looker.droidify.network.Downloader
import com.looker.droidify.network.KtorDownloader
import de.felitendo.felostore.network.Downloader
import de.felitendo.felostore.network.KtorDownloader
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn

View File

@@ -1,10 +1,10 @@
package com.looker.droidify.domain
package de.felitendo.felostore.domain
import com.looker.droidify.domain.model.App
import com.looker.droidify.domain.model.AppMinimal
import com.looker.droidify.domain.model.Author
import com.looker.droidify.domain.model.Package
import com.looker.droidify.domain.model.PackageName
import de.felitendo.felostore.domain.model.App
import de.felitendo.felostore.domain.model.AppMinimal
import de.felitendo.felostore.domain.model.Author
import de.felitendo.felostore.domain.model.Package
import de.felitendo.felostore.domain.model.PackageName
import kotlinx.coroutines.flow.Flow
interface AppRepository {

View File

@@ -1,6 +1,6 @@
package com.looker.droidify.domain
package de.felitendo.felostore.domain
import com.looker.droidify.domain.model.Repo
import de.felitendo.felostore.domain.model.Repo
import kotlinx.coroutines.flow.Flow
interface RepoRepository {

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.domain.model
package de.felitendo.felostore.domain.model
data class App(
val repoId: Long,
@@ -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,35 +15,34 @@ data class App(
)
data class Author(
val id: Int,
val name: String?,
val email: String?,
val phone: String?,
val web: String?,
val id: Long,
val name: String,
val email: String,
val web: String
)
data class Donation(
val regularUrl: List<String>? = null,
val regularUrl: String? = null,
val bitcoinAddress: String? = null,
val flattrId: String? = null,
val litecoinAddress: String? = null,
val liteCoinAddress: String? = null,
val openCollectiveId: String? = null,
val liberapayId: String? = null,
val librePayId: String? = null,
)
data class Graphics(
val featureGraphic: String? = null,
val promoGraphic: String? = null,
val tvBanner: String? = null,
val video: String? = null,
val featureGraphic: String = "",
val promoGraphic: String = "",
val tvBanner: String = "",
val video: String = ""
)
data class Links(
val changelog: String? = null,
val issueTracker: String? = null,
val sourceCode: String? = null,
val translation: String? = null,
val webSite: String? = null,
val changelog: String = "",
val issueTracker: String = "",
val sourceCode: String = "",
val translation: String = "",
val webSite: String = ""
)
data class Metadata(

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.domain.model
package de.felitendo.felostore.domain.model
interface DataFile {
val name: String

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.domain.model
package de.felitendo.felostore.domain.model
import java.security.MessageDigest
import java.security.cert.Certificate
@@ -27,22 +27,6 @@ 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) {

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.domain.model
package de.felitendo.felostore.domain.model
data class Package(
val id: Long,

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.domain.model
package de.felitendo.felostore.domain.model
@JvmInline
value class PackageName(val name: String)

View File

@@ -1,23 +1,25 @@
package com.looker.droidify.domain.model
package de.felitendo.felostore.domain.model
data class Repo(
val id: Int,
val id: Long,
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 != null
val shouldAuthenticate =
authentication.username.isNotEmpty() && authentication.password.isNotEmpty()
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
)
}
}
@@ -26,22 +28,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?
)

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.graphics
package de.felitendo.felostore.graphics
import android.graphics.Canvas
import android.graphics.ColorFilter

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.graphics
package de.felitendo.felostore.graphics
import android.graphics.Rect
import android.graphics.drawable.Drawable

View File

@@ -1,17 +1,17 @@
package com.looker.droidify.index
package de.felitendo.felostore.index
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import com.fasterxml.jackson.core.JsonToken
import com.looker.droidify.utility.common.extension.Json
import com.looker.droidify.utility.common.extension.asSequence
import com.looker.droidify.utility.common.extension.collectNotNull
import com.looker.droidify.utility.common.extension.writeDictionary
import com.looker.droidify.model.Product
import com.looker.droidify.model.Release
import com.looker.droidify.utility.serialization.product
import com.looker.droidify.utility.serialization.release
import com.looker.droidify.utility.serialization.serialize
import de.felitendo.felostore.utility.common.extension.Json
import de.felitendo.felostore.utility.common.extension.asSequence
import de.felitendo.felostore.utility.common.extension.collectNotNull
import de.felitendo.felostore.utility.common.extension.writeDictionary
import de.felitendo.felostore.model.Product
import de.felitendo.felostore.model.Release
import de.felitendo.felostore.utility.serialization.product
import de.felitendo.felostore.utility.serialization.release
import de.felitendo.felostore.utility.serialization.serialize
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.File

View File

@@ -1,31 +1,31 @@
package com.looker.droidify.index
package de.felitendo.felostore.index
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.looker.droidify.utility.common.extension.Json
import com.looker.droidify.utility.common.extension.collectDistinctNotEmptyStrings
import com.looker.droidify.utility.common.extension.collectNotNull
import com.looker.droidify.utility.common.extension.forEach
import com.looker.droidify.utility.common.extension.forEachKey
import com.looker.droidify.utility.common.extension.illegal
import com.looker.droidify.model.Product
import com.looker.droidify.model.Product.Donate.Bitcoin
import com.looker.droidify.model.Product.Donate.Liberapay
import com.looker.droidify.model.Product.Donate.Litecoin
import com.looker.droidify.model.Product.Donate.OpenCollective
import com.looker.droidify.model.Product.Donate.Regular
import com.looker.droidify.model.Product.Screenshot.Type.LARGE_TABLET
import com.looker.droidify.model.Product.Screenshot.Type.PHONE
import com.looker.droidify.model.Product.Screenshot.Type.SMALL_TABLET
import com.looker.droidify.model.Product.Screenshot.Type.TV
import com.looker.droidify.model.Product.Screenshot.Type.VIDEO
import com.looker.droidify.model.Product.Screenshot.Type.WEAR
import com.looker.droidify.model.Release
import com.looker.droidify.utility.common.SdkCheck
import com.looker.droidify.utility.common.nullIfEmpty
import de.felitendo.felostore.utility.common.extension.Json
import de.felitendo.felostore.utility.common.extension.collectDistinctNotEmptyStrings
import de.felitendo.felostore.utility.common.extension.collectNotNull
import de.felitendo.felostore.utility.common.extension.forEach
import de.felitendo.felostore.utility.common.extension.forEachKey
import de.felitendo.felostore.utility.common.extension.illegal
import de.felitendo.felostore.model.Product
import de.felitendo.felostore.model.Product.Donate.Bitcoin
import de.felitendo.felostore.model.Product.Donate.Liberapay
import de.felitendo.felostore.model.Product.Donate.Litecoin
import de.felitendo.felostore.model.Product.Donate.OpenCollective
import de.felitendo.felostore.model.Product.Donate.Regular
import de.felitendo.felostore.model.Product.Screenshot.Type.LARGE_TABLET
import de.felitendo.felostore.model.Product.Screenshot.Type.PHONE
import de.felitendo.felostore.model.Product.Screenshot.Type.SMALL_TABLET
import de.felitendo.felostore.model.Product.Screenshot.Type.TV
import de.felitendo.felostore.model.Product.Screenshot.Type.VIDEO
import de.felitendo.felostore.model.Product.Screenshot.Type.WEAR
import de.felitendo.felostore.model.Release
import de.felitendo.felostore.utility.common.SdkCheck
import de.felitendo.felostore.utility.common.nullIfEmpty
import java.io.InputStream
object IndexV1Parser {

View File

@@ -1,27 +1,24 @@
package com.looker.droidify.index
package de.felitendo.felostore.index
import android.content.Context
import androidx.core.net.toUri
import com.looker.droidify.database.Database
import com.looker.droidify.domain.model.fingerprint
import com.looker.droidify.model.Product
import com.looker.droidify.model.Release
import com.looker.droidify.model.Repository
import com.looker.droidify.network.Downloader
import com.looker.droidify.network.NetworkResponse
import com.looker.droidify.utility.common.SdkCheck
import com.looker.droidify.utility.common.cache.Cache
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.CoroutineScope
import kotlinx.coroutines.Dispatchers
import android.net.Uri
import de.felitendo.felostore.database.Database
import de.felitendo.felostore.domain.model.fingerprint
import de.felitendo.felostore.model.Product
import de.felitendo.felostore.model.Release
import de.felitendo.felostore.model.Repository
import de.felitendo.felostore.network.Downloader
import de.felitendo.felostore.network.NetworkResponse
import de.felitendo.felostore.utility.common.SdkCheck
import de.felitendo.felostore.utility.common.cache.Cache
import de.felitendo.felostore.utility.common.extension.toFormattedString
import de.felitendo.felostore.utility.common.result.Result
import de.felitendo.felostore.utility.extension.android.Android
import de.felitendo.felostore.utility.getProgress
import kotlinx.coroutines.*
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
@@ -36,7 +33,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")
}
@@ -54,7 +51,7 @@ object RepositoryUpdater {
constructor(errorType: ErrorType, message: String, cause: Exception) : super(
message,
cause,
cause
) {
this.errorType = errorType
}
@@ -92,13 +89,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(
@@ -106,7 +103,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)) {
@@ -123,14 +120,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}"
)
)
}
}
@@ -149,7 +146,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) {
@@ -164,20 +161,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 = repository.address.toUri().buildUpon()
url = Uri.parse(repository.address).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)
callback(Stage.DOWNLOAD, read.value, total.value.takeIf { it != 0L })
}
when (result) {
@@ -188,8 +185,8 @@ object RepositoryUpdater {
lastModified = result.lastModified?.toFormattedString() ?: "",
entityTag = result.etag ?: "",
statusCode = result.statusCode,
file = file,
),
file = file
)
)
}
@@ -206,8 +203,8 @@ object RepositoryUpdater {
Result.Error(
UpdateException(
errorType = errorType,
message = "Failed with Status: ${result.statusCode}",
),
message = "Failed with Status: ${result.statusCode}"
)
)
}
@@ -231,7 +228,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) {
@@ -261,7 +258,7 @@ object RepositoryUpdater {
name: String,
description: String,
version: Int,
timestamp: Long,
timestamp: Long
) {
changedRepository = repository.update(
mirrors,
@@ -270,7 +267,7 @@ object RepositoryUpdater {
version,
lastModified,
entityTag,
timestamp,
timestamp
)
}
@@ -287,7 +284,7 @@ object RepositoryUpdater {
override fun onReleases(
packageName: String,
releases: List<Release>,
releases: List<Release>
) {
if (Thread.interrupted()) {
throw InterruptedException()
@@ -298,7 +295,7 @@ object RepositoryUpdater {
unmergedReleases.clear()
}
}
},
}
)
if (Thread.interrupted()) {
@@ -321,11 +318,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) }
)
}
}
@@ -339,7 +336,7 @@ object RepositoryUpdater {
throw UpdateException(
ErrorType.VALIDATION,
"New index is older than current index:" +
" ${workRepository.timestamp} < ${repository.timestamp}",
" ${workRepository.timestamp} < ${repository.timestamp}"
)
}
@@ -352,13 +349,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"
)
}
@@ -394,7 +391,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)
@@ -402,13 +399,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 }
@@ -448,7 +445,7 @@ object RepositoryUpdater {
selected = firstSelected?.let {
it.first.versionCode == release.versionCode &&
it.second == incompatibilities
} ?: false,
} ?: false
)
}
return product.copy(releases = releases)
@@ -460,5 +457,5 @@ data class IndexFile(
val lastModified: String,
val entityTag: String,
val statusCode: Int,
val file: File,
val file: File
)

View File

@@ -1,24 +1,24 @@
package com.looker.droidify.installer
package de.felitendo.felostore.installer
import android.content.Context
import com.looker.droidify.utility.common.extension.addAndCompute
import com.looker.droidify.utility.common.extension.filter
import com.looker.droidify.utility.common.extension.notificationManager
import com.looker.droidify.utility.common.extension.updateAsMutable
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.datastore.get
import com.looker.droidify.datastore.model.InstallerType
import com.looker.droidify.domain.model.PackageName
import com.looker.droidify.installer.installers.Installer
import com.looker.droidify.installer.installers.LegacyInstaller
import com.looker.droidify.installer.installers.root.RootInstaller
import com.looker.droidify.installer.installers.session.SessionInstaller
import com.looker.droidify.installer.installers.shizuku.ShizukuInstaller
import com.looker.droidify.installer.model.InstallItem
import com.looker.droidify.installer.model.InstallState
import com.looker.droidify.installer.notification.createInstallNotification
import com.looker.droidify.installer.notification.installNotification
import com.looker.droidify.installer.notification.removeInstallNotification
import de.felitendo.felostore.utility.common.extension.addAndCompute
import de.felitendo.felostore.utility.common.extension.filter
import de.felitendo.felostore.utility.common.extension.notificationManager
import de.felitendo.felostore.utility.common.extension.updateAsMutable
import de.felitendo.felostore.datastore.SettingsRepository
import de.felitendo.felostore.datastore.get
import de.felitendo.felostore.datastore.model.InstallerType
import de.felitendo.felostore.domain.model.PackageName
import de.felitendo.felostore.installer.installers.Installer
import de.felitendo.felostore.installer.installers.LegacyInstaller
import de.felitendo.felostore.installer.installers.root.RootInstaller
import de.felitendo.felostore.installer.installers.session.SessionInstaller
import de.felitendo.felostore.installer.installers.shizuku.ShizukuInstaller
import de.felitendo.felostore.installer.model.InstallItem
import de.felitendo.felostore.installer.model.InstallState
import de.felitendo.felostore.installer.notification.createInstallNotification
import de.felitendo.felostore.installer.notification.installNotification
import de.felitendo.felostore.installer.notification.removeInstallNotification
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
@@ -32,7 +32,7 @@ import kotlinx.coroutines.sync.withLock
class InstallManager(
private val context: Context,
private val settingsRepository: SettingsRepository
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, settingsRepository)
InstallerType.LEGACY -> LegacyInstaller(context)
InstallerType.SESSION -> SessionInstaller(context)
InstallerType.SHIZUKU -> ShizukuInstaller(context)
InstallerType.ROOT -> RootInstaller(context)

Some files were not shown because too many files have changed in this diff Show More