21 Commits
main ... v0.0.1

Author SHA1 Message Date
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 3152 additions and 7543 deletions

View File

@@ -7,8 +7,8 @@ trim_trailing_whitespace = true
[*.{kt,kts}] [*.{kt,kts}]
indent_size = 4 indent_size = 4
ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site=true ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 999 ij_kotlin_name_count_to_use_star_import = 999
ij_kotlin_name_count_to_use_star_import_for_members = 999 ij_kotlin_name_count_to_use_star_import_for_members = 999

View File

@@ -1,5 +1,4 @@
name: Build Debug APK name: Build Debug APK
on: on:
push: push:
branches: branches:
@@ -13,19 +12,15 @@ on:
- '**.md' - '**.md'
types: submitted types: submitted
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }} group: ${{ github.workflow }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -33,10 +28,7 @@ jobs:
submodules: true submodules: true
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/actions/setup-gradle@v4 uses: gradle/wrapper-validation-action@v1
- name: Setup Gradle
uses: gradle/wrapper-validation-action@v3
- name: Set up Java 17 - name: Set up Java 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@@ -45,30 +37,51 @@ jobs:
distribution: 'adopt' distribution: 'adopt'
cache: gradle cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Grant execution permission to Gradle Wrapper - name: Grant execution permission to Gradle Wrapper
run: chmod +x gradlew run: chmod +x gradlew
- name: Build Debug APK - name: Build Debug APK
run: ./gradlew assembleDebug run: ./gradlew assembleDebug
- name: Sign Apk - name: Display APK directory contents
continue-on-error: true run: |
id: sign_apk echo "Listing app/build/outputs/apk/debug contents:"
uses: r0adkll/sign-android-release@v1 ls -la app/build/outputs/apk/debug/
with:
releaseDir: app/build/outputs/apk/debug
signingKeyBase64: ${{ secrets.KEY_BASE64 }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEYSTORE_PASS }}
keyPassword: ${{ secrets.KEYSTORE_PASS }}
- 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 continue-on-error: true
run: | 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 - name: Upload the APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: debug name: debug
path: app/build/outputs/apk/debug/app-debug*.apk path: app/build/outputs/apk/debug/*.apk

View File

@@ -1,34 +1,34 @@
name: Build Release APK name: Build Release APK
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
tags: tags:
- '*' - '*'
concurrency: concurrency:
group: "release-build" group: "release-build"
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
packages: write packages: write
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
- name: Validate Gradle Wrapper - name: Debug Environment
uses: gradle/actions/setup-gradle@v4 run: |
echo "Working directory: $(pwd)"
echo "Repository: ${{ github.repository }}"
echo "Ref: ${{ github.ref }}"
echo "Ref name: ${{ github.ref_name }}"
ls -la
- name: Setup Gradle - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v3 uses: gradle/wrapper-validation-action@v1
- name: Set up Java 17 - name: Set up Java 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@@ -37,58 +37,130 @@ jobs:
distribution: 'adopt' distribution: 'adopt'
cache: gradle cache: gradle
- name: Debug Java and Gradle Setup
run: |
java -version
echo "JAVA_HOME: $JAVA_HOME"
# Setting up Android SDK manually
- name: Setup Android SDK
run: |
mkdir -p $HOME/Android/sdk
echo "sdk.dir=$HOME/Android/sdk" > local.properties
echo "ANDROID_HOME=$HOME/Android/sdk" >> $GITHUB_ENV
echo "PATH=$PATH:$HOME/Android/sdk/tools:$HOME/Android/sdk/tools/bin:$HOME/Android/sdk/platform-tools" >> $GITHUB_ENV
# Install cmdline-tools
mkdir -p $HOME/Android/sdk/cmdline-tools
wget -q https://dl.google.com/android/repository/commandlinetools-linux-8092744_latest.zip -O cmdline-tools.zip
unzip -q cmdline-tools.zip -d $HOME/Android/sdk/cmdline-tools
mv $HOME/Android/sdk/cmdline-tools/cmdline-tools $HOME/Android/sdk/cmdline-tools/latest
# Debug cmdline-tools installation
ls -la $HOME/Android/sdk/cmdline-tools/latest/bin/
# Accept licenses and install necessary components
echo "y" | $HOME/Android/sdk/cmdline-tools/latest/bin/sdkmanager --licenses || echo "License acceptance failed but continuing"
$HOME/Android/sdk/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-33" "build-tools;33.0.0"
# Verify installation
echo "Android SDK components installed:"
$HOME/Android/sdk/cmdline-tools/latest/bin/sdkmanager --list_installed
- name: Grant execution permission to Gradle Wrapper - name: Grant execution permission to Gradle Wrapper
run: chmod +x gradlew run: chmod +x gradlew
- name: Debug Gradle setup
run: |
./gradlew --version
cat local.properties
- name: Build Release APK - name: Build Release APK
run: ./gradlew assembleRelease run: |
./gradlew assembleRelease --stacktrace
- name: Checks - name: Find APK files
run: find . -type f -name "*.apk" run: |
echo "Looking for APK files:"
find . -type f -name "*.apk"
- uses: r0adkll/sign-android-release@v1 - name: Sign APK manually
name: Signing APK run: |
id: sign_app # Find APK file
with: APK_FILES=$(find app/build/outputs/apk/release/ -name "*.apk" | sort)
releaseDirectory: app/build/outputs/apk/release if [ -z "$APK_FILES" ]; then
signingKeyBase64: ${{ secrets.KEY_BASE64 }} echo "No APK files found!"
alias: ${{ secrets.KEY_ALIAS }} exit 1
keyStorePassword: ${{ secrets.KEYSTORE_PASS }} fi
keyPassword: ${{ secrets.KEYSTORE_PASS }}
env: echo "Found APK files:"
BUILD_TOOLS_VERSION: "35.0.0" echo "$APK_FILES"
# Use the first APK file found
APK_FILE=$(echo "$APK_FILES" | head -1)
echo "Using APK file: $APK_FILE"
# Create keystore from base64
echo "Creating keystore..."
echo "${{ secrets.KEY_BASE64 }}" | base64 -d > keystore.jks
# Sign APK
echo "Signing APK..."
SIGNED_FILE="${APK_FILE%.apk}-signed.apk"
# Find apksigner in build-tools
APKSIGNER_PATH=$(find $HOME/Android/sdk/build-tools -name "apksigner" | head -1)
echo "Using apksigner at: $APKSIGNER_PATH"
$APKSIGNER_PATH sign --ks keystore.jks \
--ks-key-alias "${{ secrets.KEY_ALIAS }}" \
--ks-pass pass:"${{ secrets.KEYSTORE_PASS }}" \
--key-pass pass:"${{ secrets.KEYSTORE_PASS }}" \
--out "$SIGNED_FILE" \
"$APK_FILE"
echo "SIGNED_RELEASE_FILE=$SIGNED_FILE" >> $GITHUB_ENV
# Verify signed APK
ls -la "$SIGNED_FILE" || echo "Failed to find signed APK"
- name: Extract Version Code - name: Extract Version Code
id: extract_version
run: | run: |
VERSION_CODE=$(grep -oP '(?<=versionCode=)\d+' app/build.gradle.kts) # Adjust path to your build.gradle VERSION_CODE=$(grep -oP '(?<=versionCode)\s*=?\s*\K\d+' app/build.gradle || grep -oP '(?<=versionCode)\s*=?\s*\K\d+' app/build.gradle.kts || echo "unknown")
echo "::set-output name=version_code::$VERSION_CODE" echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
echo "Version Code: $VERSION_CODE" echo "Version Code: $VERSION_CODE"
- name: Read Changelog - name: Read Changelog
id: read_changelog
run: | run: |
CHANGELOG_PATH="metadata/en-US/changelogs/${{ steps.extract_version.outputs.version_code }}.txt" CHANGELOG_PATH="metadata/en-US/changelogs/$VERSION_CODE.txt"
echo "Looking for changelog at: $CHANGELOG_PATH"
if [[ -f "$CHANGELOG_PATH" ]]; then if [[ -f "$CHANGELOG_PATH" ]]; then
CHANGELOG=$(cat "$CHANGELOG_PATH") echo "Changelog found:"
echo "::set-output name=changelog::$CHANGELOG" cat "$CHANGELOG_PATH"
echo ""
# Set environment variable for the changelog
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
cat "$CHANGELOG_PATH" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else else
echo "::set-output name=changelog::No changelog found for this version."
echo "No changelog found at: $CHANGELOG_PATH" echo "No changelog found at: $CHANGELOG_PATH"
echo "CHANGELOG=Release version $VERSION_CODE" >> $GITHUB_ENV
# Try to find any changelog files
echo "Looking for any changelog files:"
find metadata -type f -name "*.txt" || echo "No changelog files found"
fi fi
- uses: softprops/action-gh-release@v2 - name: Upload Artifact
name: Create Release uses: actions/upload-artifact@v2
id: publish_release
with: with:
body: ${{ steps.read_changelog.outputs.changelog }} name: Signed-APK-${{ env.VERSION_CODE }}
tag_name: ${{ github.ref }} path: ${{ env.SIGNED_RELEASE_FILE }}
name: Release ${{ github.ref }}
files: ${{steps.sign_app.outputs.signedReleaseFile}}
draft: true
prerelease: false
- uses: actions/upload-artifact@v4 - name: Debug Final Output
with: run: |
name: Signed APK echo "Signed APK path: ${{ env.SIGNED_RELEASE_FILE }}"
path: ${{steps.sign_app.outputs.signedReleaseFile}} echo "Version code: ${{ env.VERSION_CODE }}"
echo "Changelog summary: $(echo "${{ env.CHANGELOG }}" | head -1)"

View File

@@ -1,18 +1,18 @@
<div align="center"> <div align="center">
<img width="" src="metadata/en-US/images/featureGraphic.png" alt="Droid-ify" align="center"> <img width="" src="metadata/en-US/images/featureGraphic.png" alt="FeloStore" 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 Stars](https://img.shields.io/github/stars/Felitendo/FeloStore?color=%2364f573&style=for-the-badge)](https://github.com/Felitendo/FeloStore/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 License](https://img.shields.io/github/license/Felitendo/FeloStore?color=%2364f573&style=for-the-badge)](https://github.com/Felitendo/FeloStore/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 Downloads](https://img.shields.io/github/downloads/Felitendo/FeloStore/total.svg?color=%23f5ad64&style=for-the-badge)](https://github.com/Felitendo/FeloStore/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) [![Github Latest](https://img.shields.io/github/v/release/Felitendo/FeloStore?display_name=tag&color=%23f5ad64&style=for-the-badge)](https://github.com/Felitendo/FeloStore/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) [![FDroid Latest](https://img.shields.io/f-droid/v/com.felitendo.felostore?color=%23f5ad64&style=for-the-badge)](https://f-droid.org/packages/com.felitendo.felostore)
</div> </div>
<div align="left"> <div align="left">
## Features ## Features
* Clean Material 3 design * Material & Clean design
* Fast repository syncing * Fast repository syncing
* Smooth user experience * Smooth user experience
* Feature-rich * Feature-rich
@@ -21,13 +21,13 @@
<img src="metadata/en-US/images/phoneScreenshots/1.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/2.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/3.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/4.png" width="25%" /> <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**: 1. **Install Android Studio**:
- Download and install [Android Studio](https://developer.android.com/studio) on your computer - Download and install [Android Studio](https://developer.android.com/studio) on your computer
if you haven't already. if you haven't already.
2. **Clone the repository**: 2. **Clone the Repository**:
- Open Android Studio and select "Project from Version Control." - Open Android Studio and select "Project from Version Control."
- Paste the link to this repository to clone it to your local machine. - Paste the link to this repository to clone it to your local machine.
@@ -39,23 +39,24 @@
## TODO ## TODO
- [ ] Add support for `index-v2` - [ ] Add support for `index-v2`
- [ ] Add detekt code analysis - [ ] Add detekt code-analysis
- [ ] Add GitHub Repo feature
## Contributing ## Contribution
- Pick any issue you would like to resolve - Pick any issue you would like to resolve
- Fork the project - Fork the project
- Open a pull request - Open a Pull Request
- Your pull request will undergo review - Your PR will undergo review
## Translations ## Translations
[![Translation status](https://hosted.weblate.org/widgets/droidify/-/horizontal-auto.svg)](https://hosted.weblate.org/engage/droidify/?utm_source=widget) [![Translation status](https://hosted.weblate.org/widgets/felostore/-/horizontal-auto.svg)](https://hosted.weblate.org/engage/felostore/?utm_source=widget)
## License ## License
``` ```
Droid-ify FeloStore
Copyright (C) 2023 LooKeR Copyright (C) 2023 LooKeR
This program is free software: you can redistribute it and/or modify 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? ## What now?
- Next release will be on **19-Aug**, and will contain many bug fixes and stability improvements. - 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. - We will be missing on the new index format introduced by fdroid for some future releases.

8
UPDATING.md Normal file
View File

@@ -0,0 +1,8 @@
1. Delete everything in the "FeloStore/Releases" Repo
2. Put new version of FeloStore from the GitHub's "Release"-Tab in the "FeloStore/Releases" Repo
3. 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 com.android.build.gradle.internal.tasks.factory.dependsOn
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.ktlint)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.hilt) alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
@@ -12,59 +12,68 @@ plugins {
} }
android { android {
val latestVersionName = "0.6.6" val latestVersionName = "0.0.1"
namespace = "com.looker.droidify" namespace = "com.felitendo.felostore"
buildToolsVersion = "35.0.0" buildToolsVersion = "35.0.0"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
minSdk = 23 minSdk = 23
targetSdk = 35 targetSdk = 35
applicationId = "com.looker.droidify" applicationId = "com.felitendo.felostore"
versionCode = 660 versionCode = 650
versionName = latestVersionName versionName = latestVersionName
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = false
testInstrumentationRunner = "com.looker.droidify.TestRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
compileOptions.isCoreLibraryDesugaringEnabled = true compileOptions {
kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-parameters") sourceCompatibility = JavaVersion.VERSION_17
androidResources.generateLocaleConfig = true targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlin { kotlinOptions {
jvmToolchain(17) jvmTarget = "17"
compilerOptions { freeCompilerArgs = listOf(
languageVersion.set(KotlinVersion.KOTLIN_2_2) "-opt-in=kotlin.RequiresOptIn",
apiVersion.set(KotlinVersion.KOTLIN_2_2) "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
jvmTarget.set(JvmTarget.JVM_17) "-opt-in=kotlinx.coroutines.FlowPreview",
"-Xcontext-receivers"
)
}
androidResources {
generateLocaleConfig = true
}
sourceSets.forEach { source ->
val javaDir = source.java.srcDirs.find { it.name == "java" }
source.java {
srcDir(File(javaDir?.parentFile, "kotlin"))
} }
} }
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.generateKotlin", "true")
}
buildTypes { buildTypes {
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
resValue("string", "application_name", "Droid-ify-Debug") resValue("string", "application_name", "Felo Store")
} }
release { release {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
resValue("string", "application_name", "Droid-ify") resValue("string", "application_name", "Felo Store")
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard.pro", "proguard.pro"
) )
} }
create("alpha") { create("alpha") {
initWith(getByName("debug")) initWith(getByName("debug"))
applicationIdSuffix = ".alpha" applicationIdSuffix = ".alpha"
resValue("string", "application_name", "Droid-ify Alpha") resValue("string", "application_name", "Felo Store")
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard.pro", "proguard.pro"
) )
isDebuggable = true isDebuggable = true
isMinifyEnabled = true isMinifyEnabled = true
@@ -73,7 +82,7 @@ android {
buildConfigField( buildConfigField(
type = "String", type = "String",
name = "VERSION_NAME", 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 { dependencies {
coreLibraryDesugaring(libs.desugaring) coreLibraryDesugaring(libs.desugaring)
@@ -123,17 +144,19 @@ dependencies {
implementation(libs.kotlin.stdlib) implementation(libs.kotlin.stdlib)
implementation(libs.datetime) 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.libsu.core)
implementation(libs.bundles.shizuku) implementation(libs.shizuku.api)
api(libs.shizuku.provider)
implementation(libs.jackson.core) implementation(libs.jackson.core)
implementation(libs.serialization) implementation(libs.serialization)
implementation(libs.bundles.ktor) implementation(libs.ktor.core)
implementation(libs.bundles.room) implementation(libs.ktor.okhttp)
ksp(libs.room.compiler)
implementation(libs.work.ktx) implementation(libs.work.ktx)
@@ -146,16 +169,13 @@ dependencies {
testImplementation(platform(libs.junit.bom)) testImplementation(platform(libs.junit.bom))
testImplementation(libs.bundles.test.unit) testImplementation(libs.bundles.test.unit)
testRuntimeOnly(libs.junit.platform) testRuntimeOnly(libs.junit.platform)
androidTestImplementation(libs.hilt.test) androidTestImplementation(platform(libs.junit.bom))
androidTestImplementation(libs.room.test)
androidTestImplementation(libs.bundles.test.android) androidTestImplementation(libs.bundles.test.android)
kspAndroidTest(libs.hilt.compiler)
// debugImplementation(libs.leakcanary) // debugImplementation(libs.leakcanary)
} }
// using a task as a preBuild dependency instead of a function that takes some time insures that it runs // 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") { task("detectAndroidLocals") {
val langsList: MutableSet<String> = HashSet() 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 com.felitendo.felostore.index
import android.content.Context import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.looker.droidify.database.Database import com.felitendo.felostore.model.Repository
import com.looker.droidify.index.RepositoryUpdater.IndexType
import com.looker.droidify.model.Repository
import com.looker.droidify.sync.FakeDownloader
import com.looker.droidify.sync.common.assets
import com.looker.droidify.sync.common.benchmark
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.File import java.io.File
import kotlin.math.sqrt
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@@ -27,9 +21,7 @@ class RepositoryUpdaterTest {
@Before @Before
fun setup() { fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext context = InstrumentationRegistry.getInstrumentation().context
Database.init(context)
RepositoryUpdater.init(CoroutineScope(Dispatchers.Default), FakeDownloader)
repository = Repository( repository = Repository(
id = 15, id = 15,
address = "https://apt.izzysoft.de/fdroid/repo", address = "https://apt.izzysoft.de/fdroid/repo",
@@ -49,14 +41,13 @@ class RepositoryUpdaterTest {
@Test @Test
fun processFile() { fun processFile() {
val output = benchmark(1) { testRepetition(1) {
val createFile = File.createTempFile("index", "entry") val createFile = File.createTempFile("index", "entry")
val mergerFile = File.createTempFile("index", "merger") val mergerFile = File.createTempFile("index", "merger")
val jarStream = context.resources.assets.open("index-v1.jar") val jarStream = context.resources.assets.open("index-v1.jar")
jarStream.copyTo(createFile.outputStream()) jarStream.copyTo(createFile.outputStream())
process(createFile, mergerFile) process(createFile, mergerFile)
} }
println(output)
} }
private fun process(file: File, merger: File) = measureTimeMillis { 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 com.felitendo.felostore.sync
import com.looker.droidify.network.Downloader import com.felitendo.felostore.network.Downloader
import com.looker.droidify.network.NetworkResponse import com.felitendo.felostore.network.NetworkResponse
import com.looker.droidify.network.ProgressListener import com.felitendo.felostore.network.ProgressListener
import com.looker.droidify.network.header.HeadersBuilder import com.felitendo.felostore.network.header.HeadersBuilder
import com.looker.droidify.network.validation.FileValidator import com.felitendo.felostore.network.validation.FileValidator
import com.looker.droidify.sync.common.assets import com.felitendo.felostore.sync.common.assets
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

View File

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

View File

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

View File

@@ -1,24 +1,23 @@
package com.looker.droidify.sync package com.felitendo.felostore.sync
import android.content.Context import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.looker.droidify.domain.model.Repo import com.felitendo.felostore.domain.model.Repo
import com.looker.droidify.sync.common.IndexJarValidator import com.felitendo.felostore.sync.common.IndexJarValidator
import com.looker.droidify.sync.common.Izzy import com.felitendo.felostore.sync.common.Izzy
import com.looker.droidify.sync.common.JsonParser import com.felitendo.felostore.sync.common.JsonParser
import com.looker.droidify.sync.common.benchmark import com.felitendo.felostore.sync.common.downloadIndex
import com.looker.droidify.sync.common.downloadIndex import com.felitendo.felostore.sync.common.benchmark
import com.looker.droidify.sync.common.toV2 import com.felitendo.felostore.sync.common.toV2
import com.looker.droidify.sync.v1.V1Parser import com.felitendo.felostore.sync.v1.V1Parser
import com.looker.droidify.sync.v1.V1Syncable import com.felitendo.felostore.sync.v1.V1Syncable
import com.looker.droidify.sync.v1.model.IndexV1 import com.felitendo.felostore.sync.v1.model.IndexV1
import com.looker.droidify.sync.v2.V2Parser import com.felitendo.felostore.sync.v2.V2Parser
import com.looker.droidify.sync.v2.model.FileV2 import com.felitendo.felostore.sync.v2.model.FileV2
import com.looker.droidify.sync.v2.model.IndexV2 import com.felitendo.felostore.sync.v2.model.IndexV2
import com.looker.droidify.sync.v2.model.MetadataV2 import com.felitendo.felostore.sync.v2.model.MetadataV2
import com.looker.droidify.sync.v2.model.PackageV2 import com.felitendo.felostore.sync.v2.model.VersionV2
import com.looker.droidify.sync.v2.model.VersionV2
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -29,8 +28,6 @@ import kotlin.test.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class V1SyncableTest { class V1SyncableTest {
@@ -45,7 +42,7 @@ class V1SyncableTest {
@Before @Before
fun before() { fun before() {
context = InstrumentationRegistry.getInstrumentation().targetContext context = InstrumentationRegistry.getInstrumentation().context
dispatcher = StandardTestDispatcher() dispatcher = StandardTestDispatcher()
validator = IndexJarValidator(dispatcher) validator = IndexJarValidator(dispatcher)
parser = V1Parser(dispatcher, JsonParser, validator) parser = V1Parser(dispatcher, JsonParser, validator)
@@ -105,38 +102,9 @@ class V1SyncableTest {
testIndexConversion("index-v1.jar", "index-v2-updated.json") testIndexConversion("index-v1.jar", "index-v2-updated.json")
} }
@Test // @Test
fun targetPropertyTest() = runTest(dispatcher) { fun v1tov2FDroidRepo() = runTest(dispatcher) {
val v2IzzyFile = testIndexConversion("fdroid-index-v1.jar", "fdroid-index-v2.json")
FakeDownloader.downloadIndex(context, repo, "izzy-v2", "index-v2-updated.json")
val v2FdroidFile =
FakeDownloader.downloadIndex(context, repo, "fdroid-v2", "fdroid-index-v2.json")
val (_, v2Izzy) = v2Parser.parse(v2IzzyFile, repo)
val (_, v2Fdroid) = v2Parser.parse(v2FdroidFile, repo)
val performTest: (PackageV2) -> Unit = { data ->
print("lib: ")
println(data.metadata.liberapay)
print("donate: ")
println(data.metadata.donate)
print("bit: ")
println(data.metadata.bitcoin)
print("flattr: ")
println(data.metadata.flattrID)
print("Open: ")
println(data.metadata.openCollective)
print("LiteCoin: ")
println(data.metadata.litecoin)
}
v2Izzy.packages.forEach { (packageName, data) ->
println("Testing on Izzy $packageName")
performTest(data)
}
v2Fdroid.packages.forEach { (packageName, data) ->
println("Testing on FDroid $packageName")
performTest(data)
}
} }
private suspend fun testIndexConversion( private suspend fun testIndexConversion(
@@ -284,8 +252,6 @@ private fun assertVersion(
assertNotNull(foundVersion) assertNotNull(foundVersion)
assertEquals(expectedVersion.added, foundVersion.added) 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.file.name, foundVersion.file.name)
assertEquals(expectedVersion.src?.name, foundVersion.src?.name) assertEquals(expectedVersion.src?.name, foundVersion.src?.name)
@@ -295,13 +261,7 @@ private fun assertVersion(
assertEquals(expectedMan.versionCode, foundMan.versionCode) assertEquals(expectedMan.versionCode, foundMan.versionCode)
assertEquals(expectedMan.versionName, foundMan.versionName) assertEquals(expectedMan.versionName, foundMan.versionName)
assertEquals(expectedMan.maxSdkVersion, foundMan.maxSdkVersion) assertEquals(expectedMan.maxSdkVersion, foundMan.maxSdkVersion)
assertNotNull(expectedMan.usesSdk)
assertNotNull(foundMan.usesSdk)
assertEquals(expectedMan.usesSdk, 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( assertContentEquals(
expectedMan.features.sortedBy { it.name }, expectedMan.features.sortedBy { it.name },

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.sync.common package com.felitendo.felostore.sync.common
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
@@ -8,6 +8,11 @@ internal inline fun benchmark(
extraMessage: String? = null, extraMessage: String? = null,
block: () -> Long, block: () -> Long,
): String { ): String {
if (extraMessage != null) {
println("=".repeat(50))
println(extraMessage)
println("=".repeat(50))
}
val times = DoubleArray(repetition) val times = DoubleArray(repetition)
repeat(repetition) { iteration -> repeat(repetition) { iteration ->
System.gc() System.gc()
@@ -15,19 +20,11 @@ internal inline fun benchmark(
times[iteration] = block().toDouble() times[iteration] = block().toDouble()
} }
val meanAndDeviation = times.culledMeanAndDeviation() val meanAndDeviation = times.culledMeanAndDeviation()
return buildString(200) { return buildString {
append("=".repeat(50)) append("=".repeat(50))
append("\n") append("\n")
if (extraMessage != null) { append(times.joinToString(" | "))
append(extraMessage) append("\n")
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("${meanAndDeviation.first} ms ± ${meanAndDeviation.second.toFloat()} ms")
append("\n") append("\n")
append("=".repeat(50)) append("=".repeat(50))

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -26,7 +26,7 @@
android:required="false" /> android:required="false" />
<application <application
android:name=".Droidify" android:name=".FeloStore"
android:allowBackup="true" android:allowBackup="true"
android:banner="@drawable/tv_banner" android:banner="@drawable/tv_banner"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
@@ -38,7 +38,7 @@
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<receiver <receiver
android:name=".Droidify$BootReceiver" android:name=".FeloStore$BootReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@@ -97,7 +97,7 @@
<data android:pathPattern="/.*/packages/.*" /> <data android:pathPattern="/.*/packages/.*" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -116,7 +116,7 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="droidify.eu.org" /> <data android:host="felostore.eu.org" />
<data android:pathPattern="/app/.*" /> <data android:pathPattern="/app/.*" />
</intent-filter> </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 com.felitendo.felostore
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
@@ -21,30 +21,30 @@ import coil3.disk.DiskCache
import coil3.disk.directory import coil3.disk.directory
import coil3.memory.MemoryCache import coil3.memory.MemoryCache
import coil3.request.crossfade import coil3.request.crossfade
import com.looker.droidify.content.ProductPreferences import com.felitendo.felostore.content.ProductPreferences
import com.looker.droidify.database.Database import com.felitendo.felostore.database.Database
import com.looker.droidify.datastore.SettingsRepository import com.felitendo.felostore.datastore.SettingsRepository
import com.looker.droidify.datastore.get import com.felitendo.felostore.datastore.get
import com.looker.droidify.datastore.model.AutoSync import com.felitendo.felostore.datastore.model.AutoSync
import com.looker.droidify.datastore.model.ProxyPreference import com.felitendo.felostore.datastore.model.ProxyPreference
import com.looker.droidify.datastore.model.ProxyType import com.felitendo.felostore.datastore.model.ProxyType
import com.looker.droidify.index.RepositoryUpdater import com.felitendo.felostore.index.RepositoryUpdater
import com.looker.droidify.installer.InstallManager import com.felitendo.felostore.installer.InstallManager
import com.looker.droidify.network.Downloader import com.felitendo.felostore.network.Downloader
import com.looker.droidify.receivers.InstalledAppReceiver import com.felitendo.felostore.receivers.InstalledAppReceiver
import com.looker.droidify.service.Connection import com.felitendo.felostore.service.Connection
import com.looker.droidify.service.SyncService import com.felitendo.felostore.service.SyncService
import com.looker.droidify.sync.SyncPreference import com.felitendo.felostore.sync.SyncPreference
import com.looker.droidify.sync.toJobNetworkType import com.felitendo.felostore.sync.toJobNetworkType
import com.looker.droidify.utility.common.Constants import com.felitendo.felostore.utility.common.Constants
import com.looker.droidify.utility.common.SdkCheck import com.felitendo.felostore.utility.common.SdkCheck
import com.looker.droidify.utility.common.cache.Cache import com.felitendo.felostore.utility.common.cache.Cache
import com.looker.droidify.utility.common.extension.getDrawableCompat import com.felitendo.felostore.utility.common.extension.getDrawableCompat
import com.looker.droidify.utility.common.extension.getInstalledPackagesCompat import com.felitendo.felostore.utility.common.extension.getInstalledPackagesCompat
import com.looker.droidify.utility.common.extension.jobScheduler import com.felitendo.felostore.utility.common.extension.jobScheduler
import com.looker.droidify.utility.common.log import com.felitendo.felostore.utility.common.log
import com.looker.droidify.utility.extension.toInstalledItem import com.felitendo.felostore.utility.extension.toInstalledItem
import com.looker.droidify.work.CleanUpWorker import com.felitendo.felostore.work.CleanUpWorker
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -60,7 +60,7 @@ import kotlin.time.Duration.Companion.INFINITE
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
@HiltAndroidApp @HiltAndroidApp
class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Provider { class FeloStore : Application(), SingletonImageLoader.Factory, Configuration.Provider {
private val parentJob = SupervisorJob() private val parentJob = SupervisorJob()
private val appScope = CoroutineScope(Dispatchers.Default + parentJob) private val appScope = CoroutineScope(Dispatchers.Default + parentJob)
@@ -80,7 +80,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy() if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
val databaseUpdated = Database.init(this) val databaseUpdated = Database.init(this)
ProductPreferences.init(this, appScope) ProductPreferences.init(this, appScope)
@@ -107,7 +107,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
addAction(Intent.ACTION_PACKAGE_ADDED) addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED) addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package") addDataScheme("package")
}, }
) )
val installedItems = val installedItems =
packageManager.getInstalledPackagesCompat() packageManager.getInstalledPackagesCompat()
@@ -200,7 +200,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
periodMillis = period, periodMillis = period,
networkType = syncConditions.toJobNetworkType(), networkType = syncConditions.toJobNetworkType(),
isCharging = syncConditions.pluggedIn, isCharging = syncConditions.pluggedIn,
isBatteryLow = syncConditions.batteryNotLow, isBatteryLow = syncConditions.batteryNotLow
) )
jobScheduler?.schedule(job) jobScheduler?.schedule(job)
} }
@@ -212,13 +212,10 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = "")) Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = ""))
} }
} }
Connection( Connection(SyncService::class.java, onBind = { connection, binder ->
SyncService::class.java, binder.sync(SyncService.SyncRequest.FORCE)
onBind = { connection, binder -> connection.unbind(this)
binder.sync(SyncService.SyncRequest.FORCE) }).bind(this)
connection.unbind(this)
},
).bind(this)
} }
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
@@ -259,12 +256,12 @@ fun strictThreadPolicy() {
.detectNetwork() .detectNetwork()
.detectUnbufferedIo() .detectUnbufferedIo()
.penaltyLog() .penaltyLog()
.build(), .build()
) )
StrictMode.setVmPolicy( StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder() StrictMode.VmPolicy.Builder()
.detectAll() .detectAll()
.penaltyLog() .penaltyLog()
.build(), .build()
) )
} }

View File

@@ -1,4 +1,4 @@
package com.looker.droidify package com.felitendo.felostore
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
@@ -14,26 +14,26 @@ import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.looker.droidify.database.CursorOwner import com.felitendo.felostore.utility.common.DeeplinkType
import com.looker.droidify.datastore.SettingsRepository import com.felitendo.felostore.utility.common.SdkCheck
import com.looker.droidify.datastore.extension.getThemeRes import com.felitendo.felostore.utility.common.deeplinkType
import com.looker.droidify.datastore.get import com.felitendo.felostore.utility.common.extension.homeAsUp
import com.looker.droidify.installer.InstallManager import com.felitendo.felostore.utility.common.extension.inputManager
import com.looker.droidify.installer.model.installFrom import com.felitendo.felostore.utility.common.getInstallPackageName
import com.looker.droidify.ui.appDetail.AppDetailFragment import com.felitendo.felostore.utility.common.requestNotificationPermission
import com.looker.droidify.ui.favourites.FavouritesFragment import com.felitendo.felostore.database.CursorOwner
import com.looker.droidify.ui.repository.EditRepositoryFragment import com.felitendo.felostore.datastore.SettingsRepository
import com.looker.droidify.ui.repository.RepositoriesFragment import com.felitendo.felostore.datastore.extension.getThemeRes
import com.looker.droidify.ui.repository.RepositoryFragment import com.felitendo.felostore.datastore.get
import com.looker.droidify.ui.settings.SettingsFragment import com.felitendo.felostore.installer.InstallManager
import com.looker.droidify.ui.tabsFragment.TabsFragment import com.felitendo.felostore.installer.model.installFrom
import com.looker.droidify.utility.common.DeeplinkType import com.felitendo.felostore.ui.appDetail.AppDetailFragment
import com.looker.droidify.utility.common.SdkCheck import com.felitendo.felostore.ui.favourites.FavouritesFragment
import com.looker.droidify.utility.common.deeplinkType import com.felitendo.felostore.ui.repository.EditRepositoryFragment
import com.looker.droidify.utility.common.extension.homeAsUp import com.felitendo.felostore.ui.repository.RepositoriesFragment
import com.looker.droidify.utility.common.extension.inputManager import com.felitendo.felostore.ui.repository.RepositoryFragment
import com.looker.droidify.utility.common.getInstallPackageName import com.felitendo.felostore.ui.settings.SettingsFragment
import com.looker.droidify.utility.common.requestNotificationPermission import com.felitendo.felostore.ui.tabsFragment.TabsFragment
import dagger.hilt.EntryPoint import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -64,7 +64,7 @@ class MainActivity : AppCompatActivity() {
@Parcelize @Parcelize
private class FragmentStackItem( private class FragmentStackItem(
val className: String, val arguments: Bundle?, val savedState: Fragment.SavedState?, val className: String, val arguments: Bundle?, val savedState: Fragment.SavedState?
) : Parcelable ) : Parcelable
lateinit var cursorOwner: CursorOwner lateinit var cursorOwner: CursorOwner
@@ -87,25 +87,24 @@ class MainActivity : AppCompatActivity() {
} }
private fun collectChange() { private fun collectChange() {
val hiltEntryPoint = val hiltEntryPoint = EntryPointAccessors.fromApplication(
EntryPointAccessors.fromApplication(this, CustomUserRepositoryInjector::class.java) this, CustomUserRepositoryInjector::class.java
)
val newSettings = hiltEntryPoint.settingsRepository().get { theme to dynamicTheme } val newSettings = hiltEntryPoint.settingsRepository().get { theme to dynamicTheme }
runBlocking { runBlocking {
val theme = newSettings.first() val theme = newSettings.first()
setTheme( setTheme(
resources.configuration.getThemeRes( resources.configuration.getThemeRes(
theme = theme.first, theme = theme.first, dynamicTheme = theme.second
dynamicTheme = theme.second, )
),
) )
} }
lifecycleScope.launch { lifecycleScope.launch {
newSettings.drop(1).collect { themeAndDynamic -> newSettings.drop(1).collect { themeAndDynamic ->
setTheme( setTheme(
resources.configuration.getThemeRes( resources.configuration.getThemeRes(
theme = themeAndDynamic.first, theme = themeAndDynamic.first, dynamicTheme = themeAndDynamic.second
dynamicTheme = themeAndDynamic.second, )
),
) )
recreate() recreate()
} }
@@ -117,11 +116,9 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val rootView = FrameLayout(this).apply { id = R.id.main_content } val rootView = FrameLayout(this).apply { id = R.id.main_content }
addContentView( addContentView(
rootView, rootView, ViewGroup.LayoutParams(
ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
ViewGroup.LayoutParams.MATCH_PARENT, )
ViewGroup.LayoutParams.MATCH_PARENT,
),
) )
requestNotificationPermission(request = notificationPermission::launch) requestNotificationPermission(request = notificationPermission::launch)
@@ -191,7 +188,7 @@ class MainActivity : AppCompatActivity() {
if (open != null) { if (open != null) {
setCustomAnimations( setCustomAnimations(
if (open) R.animator.slide_in else 0, 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) setReorderingAllowed(true)
@@ -205,8 +202,8 @@ class MainActivity : AppCompatActivity() {
FragmentStackItem( FragmentStackItem(
it::class.java.name, it::class.java.name,
it.arguments, it.arguments,
supportFragmentManager.saveFragmentInstanceState(it), supportFragmentManager.saveFragmentInstanceState(it)
), )
) )
} }
replaceFragment(fragment, true) replaceFragment(fragment, true)

View File

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

View File

@@ -1,49 +1,48 @@
package com.looker.droidify.database package com.felitendo.felostore.database
import android.database.Cursor import android.database.Cursor
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import com.looker.droidify.datastore.SettingsRepository import com.felitendo.felostore.datastore.model.SortOrder
import com.looker.droidify.datastore.model.SortOrder import com.felitendo.felostore.model.ProductItem
import com.looker.droidify.model.ProductItem
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
@AndroidEntryPoint
class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> { class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
sealed class Request {
@Inject internal abstract val id: Int
lateinit var settingsRepository: SettingsRepository
sealed interface Request {
val id: Int
class Available( class Available(
val searchQuery: String, val searchQuery: String,
val section: ProductItem.Section, val section: ProductItem.Section,
val order: SortOrder, val order: SortOrder,
override val id: Int = 1, ) : Request() {
) : Request override val id: Int
get() = 1
}
class Installed( class Installed(
val searchQuery: String, val searchQuery: String,
val section: ProductItem.Section, val section: ProductItem.Section,
val order: SortOrder, val order: SortOrder,
override val id: Int = 2, ) : Request() {
) : Request override val id: Int
get() = 2
}
class Updates( class Updates(
val searchQuery: String, val searchQuery: String,
val section: ProductItem.Section, val section: ProductItem.Section,
val order: SortOrder, val order: SortOrder,
override val id: Int = 3, val skipSignatureCheck: Boolean,
) : Request ) : Request() {
override val id: Int
get() = 3
}
object Repositories : Request { object Repositories : Request() {
override val id = 4 override val id: Int
get() = 4
} }
} }
@@ -57,6 +56,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
val cursor: Cursor?, val cursor: Cursor?,
) )
init {
retainInstance = true
}
private val activeRequests = mutableMapOf<Int, ActiveRequest>() private val activeRequests = mutableMapOf<Int, ActiveRequest>()
fun attach(callback: Callback, request: Request) { 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> { override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
val request = activeRequests[id]!!.request val request = activeRequests[id]!!.request
return QueryLoader(requireContext()) { return QueryLoader(requireContext()) {
val settings = runBlocking { settingsRepository.getInitial() }
when (request) { when (request) {
is Request.Available -> is Request.Available ->
Database.ProductAdapter Database.ProductAdapter
@@ -101,7 +103,6 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
section = request.section, section = request.section,
order = request.order, order = request.order,
signal = it, signal = it,
skipSignatureCheck = settings.ignoreSignature,
) )
is Request.Installed -> is Request.Installed ->
@@ -113,7 +114,6 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
section = request.section, section = request.section,
order = request.order, order = request.order,
signal = it, signal = it,
skipSignatureCheck = settings.ignoreSignature,
) )
is Request.Updates -> is Request.Updates ->
@@ -125,7 +125,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
section = request.section, section = request.section,
order = request.order, order = request.order,
signal = it, signal = it,
skipSignatureCheck = settings.ignoreSignature, skipSignatureCheck = request.skipSignatureCheck,
) )
is Request.Repositories -> Database.RepositoryAdapter.query(it) is Request.Repositories -> Database.RepositoryAdapter.query(it)

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.database package com.felitendo.felostore.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
@@ -9,22 +9,22 @@ import android.os.CancellationSignal
import androidx.core.database.sqlite.transaction import androidx.core.database.sqlite.transaction
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonParser
import com.looker.droidify.database.table.DatabaseHelper import com.felitendo.felostore.BuildConfig
import com.looker.droidify.database.table.Table import com.felitendo.felostore.datastore.model.SortOrder
import com.looker.droidify.datastore.model.SortOrder import com.felitendo.felostore.model.InstalledItem
import com.looker.droidify.model.InstalledItem import com.felitendo.felostore.model.Product
import com.looker.droidify.model.Product import com.felitendo.felostore.model.ProductItem
import com.looker.droidify.model.ProductItem import com.felitendo.felostore.model.Repository
import com.looker.droidify.model.Repository import com.felitendo.felostore.utility.common.extension.Json
import com.looker.droidify.utility.common.extension.Json import com.felitendo.felostore.utility.common.extension.asSequence
import com.looker.droidify.utility.common.extension.asSequence import com.felitendo.felostore.utility.common.extension.firstOrNull
import com.looker.droidify.utility.common.extension.firstOrNull import com.felitendo.felostore.utility.common.extension.parseDictionary
import com.looker.droidify.utility.common.extension.parseDictionary import com.felitendo.felostore.utility.common.extension.writeDictionary
import com.looker.droidify.utility.common.extension.writeDictionary import com.felitendo.felostore.utility.common.log
import com.looker.droidify.utility.serialization.product import com.felitendo.felostore.utility.serialization.product
import com.looker.droidify.utility.serialization.productItem import com.felitendo.felostore.utility.serialization.productItem
import com.looker.droidify.utility.serialization.repository import com.felitendo.felostore.utility.serialization.repository
import com.looker.droidify.utility.serialization.serialize import com.felitendo.felostore.utility.serialization.serialize
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -44,15 +44,52 @@ import kotlin.collections.set
object Database { object Database {
fun init(context: Context): Boolean { fun init(context: Context): Boolean {
val helper = DatabaseHelper(context) val helper = Helper(context)
db = helper.writableDatabase db = helper.writableDatabase
if (helper.created) {
for (repository in Repository.defaultRepositories.sortedBy { it.name }) {
RepositoryAdapter.put(repository)
}
}
RepositoryAdapter.removeDuplicates() RepositoryAdapter.removeDuplicates()
return helper.created || helper.updated return helper.created || helper.updated
} }
private lateinit var db: SQLiteDatabase 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 { object Repository : Table {
const val ROW_ID = "_id" const val ROW_ID = "_id"
const val ROW_ENABLED = "enabled" 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 { sealed class Subject {
data object Repositories : Subject() data object Repositories : Subject()
data class Repository(val id: Long) : Subject() data class Repository(val id: Long) : Subject()
@@ -207,7 +364,7 @@ object Database {
} }
} }
fun SQLiteDatabase.query( private fun SQLiteDatabase.query(
table: String, table: String,
columns: Array<String>? = null, columns: Array<String>? = null,
selection: Pair<String, Array<String>>? = null, selection: Pair<String, Array<String>>? = null,
@@ -450,19 +607,6 @@ object Database {
.map { getUpdates(skipSignatureCheck) } .map { getUpdates(skipSignatureCheck) }
.flowOn(Dispatchers.IO) .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> { fun get(packageName: String, signal: CancellationSignal?): List<Product> {
return db.query( return db.query(
Schema.Product.name, Schema.Product.name,
@@ -575,7 +719,7 @@ object Database {
when (order) { when (order) {
SortOrder.UPDATED -> builder += "product.${Schema.Product.ROW_UPDATED} DESC," SortOrder.UPDATED -> builder += "product.${Schema.Product.ROW_UPDATED} DESC,"
SortOrder.ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC," SortOrder.ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC,"
else -> Unit SortOrder.NAME -> Unit
}::class }::class
builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC" builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.datastore package com.felitendo.felostore.datastore
import android.net.Uri import android.net.Uri
import android.util.Log 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.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey
import com.looker.droidify.datastore.model.AutoSync import com.felitendo.felostore.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType import com.felitendo.felostore.datastore.model.InstallerType
import com.looker.droidify.datastore.model.LegacyInstallerComponent import com.felitendo.felostore.datastore.model.ProxyPreference
import com.looker.droidify.datastore.model.ProxyPreference import com.felitendo.felostore.datastore.model.ProxyType
import com.looker.droidify.datastore.model.ProxyType import com.felitendo.felostore.datastore.model.SortOrder
import com.looker.droidify.datastore.model.SortOrder import com.felitendo.felostore.datastore.model.Theme
import com.looker.droidify.datastore.model.Theme import com.felitendo.felostore.utility.common.Exporter
import com.looker.droidify.utility.common.Exporter import com.felitendo.felostore.utility.common.extension.updateAsMutable
import com.looker.droidify.utility.common.extension.updateAsMutable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map 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
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
@OptIn(ExperimentalTime::class)
class PreferenceSettingsRepository( class PreferenceSettingsRepository(
private val dataStore: DataStore<Preferences>, private val dataStore: DataStore<Preferences>,
private val exporter: Exporter<Settings>, private val exporter: Exporter<Settings>,
@@ -39,7 +36,7 @@ class PreferenceSettingsRepository(
override val data: Flow<Settings> = dataStore.data override val data: Flow<Settings> = dataStore.data
.catch { exception -> .catch { exception ->
if (exception is IOException) { if (exception is IOException) {
Log.e("PreferencesSettingsRepository", "Error reading preferences.", exception) Log.e("TAG", "Error reading preferences.", exception)
} else { } else {
throw exception throw exception
} }
@@ -88,31 +85,6 @@ class PreferenceSettingsRepository(
override suspend fun setInstallerType(installerType: InstallerType) = override suspend fun setInstallerType(installerType: InstallerType) =
INSTALLER_TYPE.update(installerType.name) 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) = override suspend fun setAutoUpdate(allow: Boolean) =
AUTO_UPDATE.update(allow) AUTO_UPDATE.update(allow)
@@ -153,18 +125,6 @@ class PreferenceSettingsRepository(
private fun mapSettings(preferences: Preferences): Settings { private fun mapSettings(preferences: Preferences): Settings {
val installerType = val installerType =
InstallerType.valueOf(preferences[INSTALLER_TYPE] ?: InstallerType.Default.name) 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 language = preferences[LANGUAGE] ?: "system"
val incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false val incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false
@@ -194,7 +154,6 @@ class PreferenceSettingsRepository(
theme = theme, theme = theme,
dynamicTheme = dynamicTheme, dynamicTheme = dynamicTheme,
installerType = installerType, installerType = installerType,
legacyInstallerComponent = legacyInstallerComponent,
autoUpdate = autoUpdate, autoUpdate = autoUpdate,
autoSync = autoSync, autoSync = autoSync,
sortOrder = sortOrder, sortOrder = sortOrder,
@@ -226,9 +185,6 @@ class PreferenceSettingsRepository(
val LAST_CLEAN_UP = longPreferencesKey("key_last_clean_up_time") val LAST_CLEAN_UP = longPreferencesKey("key_last_clean_up_time")
val FAVOURITE_APPS = stringSetPreferencesKey("key_favourite_apps") val FAVOURITE_APPS = stringSetPreferencesKey("key_favourite_apps")
val HOME_SCREEN_SWIPING = booleanPreferencesKey("key_home_swiping") 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 // Enums
val THEME = stringPreferencesKey("key_theme") val THEME = stringPreferencesKey("key_theme")
@@ -244,28 +200,6 @@ class PreferenceSettingsRepository(
set(UNSTABLE_UPDATES, settings.unstableUpdate) set(UNSTABLE_UPDATES, settings.unstableUpdate)
set(THEME, settings.theme.name) set(THEME, settings.theme.name)
set(DYNAMIC_THEME, settings.dynamicTheme) 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(INSTALLER_TYPE, settings.installerType.name)
set(AUTO_UPDATE, settings.autoUpdate) set(AUTO_UPDATE, settings.autoUpdate)
set(AUTO_SYNC, settings.autoSync.name) set(AUTO_SYNC, settings.autoSync.name)

View File

@@ -1,28 +1,25 @@
package com.looker.droidify.datastore package com.felitendo.felostore.datastore
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
import com.looker.droidify.datastore.model.AutoSync import com.felitendo.felostore.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType import com.felitendo.felostore.datastore.model.InstallerType
import com.looker.droidify.datastore.model.LegacyInstallerComponent import com.felitendo.felostore.datastore.model.ProxyPreference
import com.looker.droidify.datastore.model.ProxyPreference import com.felitendo.felostore.datastore.model.SortOrder
import com.looker.droidify.datastore.model.SortOrder import com.felitendo.felostore.datastore.model.Theme
import com.looker.droidify.datastore.model.Theme import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlinx.datetime.Instant
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream 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 @Serializable
@OptIn(ExperimentalTime::class)
data class Settings( data class Settings(
val language: String = "system", val language: String = "system",
val incompatibleVersions: Boolean = false, val incompatibleVersions: Boolean = false,
@@ -30,12 +27,11 @@ data class Settings(
val unstableUpdate: Boolean = false, val unstableUpdate: Boolean = false,
val ignoreSignature: Boolean = false, val ignoreSignature: Boolean = false,
val theme: Theme = Theme.SYSTEM, val theme: Theme = Theme.SYSTEM,
val dynamicTheme: Boolean = false, val dynamicTheme: Boolean = true,
val installerType: InstallerType = InstallerType.Default, val installerType: InstallerType = InstallerType.Default,
val legacyInstallerComponent: LegacyInstallerComponent? = null, val autoUpdate: Boolean = true,
val autoUpdate: Boolean = false,
val autoSync: AutoSync = AutoSync.WIFI_ONLY, val autoSync: AutoSync = AutoSync.WIFI_ONLY,
val sortOrder: SortOrder = SortOrder.UPDATED, val sortOrder: SortOrder = SortOrder.NAME,
val proxy: ProxyPreference = ProxyPreference(), val proxy: ProxyPreference = ProxyPreference(),
val cleanUpInterval: Duration = 12.hours, val cleanUpInterval: Duration = 12.hours,
val lastCleanup: Instant? = null, val lastCleanup: Instant? = null,
@@ -48,7 +44,6 @@ object SettingsSerializer : Serializer<Settings> {
private val json = Json { encodeDefaults = true } private val json = Json { encodeDefaults = true }
@OptIn(ExperimentalTime::class)
override val defaultValue: Settings = Settings() override val defaultValue: Settings = Settings()
override suspend fun readFrom(input: InputStream): Settings { override suspend fun readFrom(input: InputStream): Settings {

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
package com.looker.droidify.datastore.extension package com.felitendo.felostore.datastore.extension
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import com.looker.droidify.R import com.felitendo.felostore.R
import com.looker.droidify.R.string as stringRes import com.felitendo.felostore.R.string as stringRes
import com.looker.droidify.R.style as styleRes import com.felitendo.felostore.R.style as styleRes
import com.looker.droidify.utility.common.SdkCheck import com.felitendo.felostore.utility.common.SdkCheck
import com.looker.droidify.datastore.model.AutoSync import com.felitendo.felostore.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType import com.felitendo.felostore.datastore.model.InstallerType
import com.looker.droidify.datastore.model.ProxyType import com.felitendo.felostore.datastore.model.ProxyType
import com.looker.droidify.datastore.model.SortOrder import com.felitendo.felostore.datastore.model.SortOrder
import com.looker.droidify.datastore.model.Theme import com.felitendo.felostore.datastore.model.Theme
import kotlin.time.Duration import kotlin.time.Duration
fun Configuration.getThemeRes(theme: Theme, dynamicTheme: Boolean) = when (theme) { 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.UPDATED -> getString(stringRes.recently_updated)
SortOrder.ADDED -> getString(stringRes.whats_new) SortOrder.ADDED -> getString(stringRes.whats_new)
SortOrder.NAME -> getString(stringRes.name) 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 com.felitendo.felostore.datastore.migration
import com.looker.droidify.datastore.PreferenceSettingsRepository.PreferencesKeys.setting import com.felitendo.felostore.datastore.PreferenceSettingsRepository.PreferencesKeys.setting
import com.looker.droidify.datastore.Settings import com.felitendo.felostore.datastore.Settings
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
class ProtoToPreferenceMigration( class ProtoToPreferenceMigration(

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package com.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 com.felitendo.felostore.datastore.model
enum class Theme { enum class Theme {
SYSTEM, SYSTEM,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.looker.droidify.domain.model package com.felitendo.felostore.domain.model
import java.security.MessageDigest import java.security.MessageDigest
import java.security.cert.Certificate 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) fun Fingerprint.formattedString(): String = value.windowed(2, 2, false)
.take(FINGERPRINT_LENGTH / 2).joinToString(separator = " ") { it.uppercase(Locale.US) } .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 { fun Certificate.fingerprint(): Fingerprint {
val bytes = encoded val bytes = encoded
return if (bytes.size >= 256) { return if (bytes.size >= 256) {

View File

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

View File

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

View File

@@ -1,23 +1,25 @@
package com.looker.droidify.domain.model package com.felitendo.felostore.domain.model
data class Repo( data class Repo(
val id: Int, val id: Long,
val enabled: Boolean, val enabled: Boolean,
val address: String, val address: String,
val name: String, val name: String,
val description: String, val description: String,
val fingerprint: Fingerprint?, val fingerprint: Fingerprint?,
val authentication: Authentication?, val authentication: Authentication,
val versionInfo: VersionInfo, val versionInfo: VersionInfo,
val mirrors: List<String>, 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 { fun update(fingerprint: Fingerprint, timestamp: Long? = null, etag: String? = null): Repo {
return copy( return copy(
fingerprint = fingerprint, fingerprint = fingerprint,
versionInfo = timestamp?.let { VersionInfo(timestamp = it, etag = etag) } versionInfo = timestamp?.let { VersionInfo(timestamp = it, etag = etag) } ?: versionInfo
?: versionInfo,
) )
} }
} }
@@ -26,22 +28,22 @@ data class AntiFeature(
val id: Long, val id: Long,
val name: String, val name: String,
val icon: String = "", val icon: String = "",
val description: String = "", val description: String = ""
) )
data class Category( data class Category(
val id: Long, val id: Long,
val name: String, val name: String,
val icon: String = "", val icon: String = "",
val description: String = "", val description: String = ""
) )
data class Authentication( data class Authentication(
val username: String, val username: String,
val password: String, val password: String
) )
data class VersionInfo( data class VersionInfo(
val timestamp: Long, val timestamp: Long,
val etag: String?, val etag: String?
) )

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,24 @@
package com.looker.droidify.index package com.felitendo.felostore.index
import android.content.Context import android.content.Context
import androidx.core.net.toUri import android.net.Uri
import com.looker.droidify.database.Database import com.felitendo.felostore.database.Database
import com.looker.droidify.domain.model.fingerprint import com.felitendo.felostore.domain.model.fingerprint
import com.looker.droidify.model.Product import com.felitendo.felostore.model.Product
import com.looker.droidify.model.Release import com.felitendo.felostore.model.Release
import com.looker.droidify.model.Repository import com.felitendo.felostore.model.Repository
import com.looker.droidify.network.Downloader import com.felitendo.felostore.network.Downloader
import com.looker.droidify.network.NetworkResponse import com.felitendo.felostore.network.NetworkResponse
import com.looker.droidify.utility.common.SdkCheck import com.felitendo.felostore.utility.common.SdkCheck
import com.looker.droidify.utility.common.cache.Cache import com.felitendo.felostore.utility.common.cache.Cache
import com.looker.droidify.utility.common.extension.toFormattedString import com.felitendo.felostore.utility.common.extension.toFormattedString
import com.looker.droidify.utility.common.result.Result import com.felitendo.felostore.utility.common.result.Result
import com.looker.droidify.utility.extension.android.Android import com.felitendo.felostore.utility.extension.android.Android
import com.looker.droidify.utility.getProgress import com.felitendo.felostore.utility.getProgress
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.security.CodeSigner import java.security.CodeSigner
import java.security.cert.Certificate import java.security.cert.Certificate
@@ -36,7 +33,7 @@ object RepositoryUpdater {
// TODO Add support for Index-V2 and also cleanup everything here // TODO Add support for Index-V2 and also cleanup everything here
enum class IndexType( enum class IndexType(
val jarName: String, val jarName: String,
val contentName: String, val contentName: String
) { ) {
INDEX_V1("index-v1.jar", "index-v1.json") INDEX_V1("index-v1.jar", "index-v1.json")
} }
@@ -54,7 +51,7 @@ object RepositoryUpdater {
constructor(errorType: ErrorType, message: String, cause: Exception) : super( constructor(errorType: ErrorType, message: String, cause: Exception) : super(
message, message,
cause, cause
) { ) {
this.errorType = errorType this.errorType = errorType
} }
@@ -92,13 +89,13 @@ object RepositoryUpdater {
context: Context, context: Context,
repository: Repository, repository: Repository,
unstable: Boolean, unstable: Boolean,
callback: (Stage, Long, Long?) -> Unit, callback: (Stage, Long, Long?) -> Unit
) = update( ) = update(
context = context, context = context,
repository = repository, repository = repository,
unstable = unstable, unstable = unstable,
indexTypes = listOf(IndexType.INDEX_V1), indexTypes = listOf(IndexType.INDEX_V1),
callback = callback, callback = callback
) )
private suspend fun update( private suspend fun update(
@@ -106,7 +103,7 @@ object RepositoryUpdater {
repository: Repository, repository: Repository,
unstable: Boolean, unstable: Boolean,
indexTypes: List<IndexType>, indexTypes: List<IndexType>,
callback: (Stage, Long, Long?) -> Unit, callback: (Stage, Long, Long?) -> Unit
): Result<Boolean> = withContext(Dispatchers.IO) { ): Result<Boolean> = withContext(Dispatchers.IO) {
val indexType = indexTypes[0] val indexType = indexTypes[0]
when (val request = downloadIndex(context, repository, indexType, callback)) { when (val request = downloadIndex(context, repository, indexType, callback)) {
@@ -123,14 +120,14 @@ object RepositoryUpdater {
repository = repository, repository = repository,
indexTypes = indexTypes.subList(1, indexTypes.size), indexTypes = indexTypes.subList(1, indexTypes.size),
unstable = unstable, unstable = unstable,
callback = callback, callback = callback
) )
} else { } else {
Result.Error( Result.Error(
UpdateException( UpdateException(
ErrorType.HTTP, ErrorType.HTTP,
"Invalid response: HTTP ${result.statusCode}", "Invalid response: HTTP ${result.statusCode}"
), )
) )
} }
} }
@@ -149,7 +146,7 @@ object RepositoryUpdater {
file = request.data.file, file = request.data.file,
lastModified = request.data.lastModified, lastModified = request.data.lastModified,
entityTag = request.data.entityTag, entityTag = request.data.entityTag,
callback = callback, callback = callback
) )
Result.Success(isFileParsedSuccessfully) Result.Success(isFileParsedSuccessfully)
} catch (e: UpdateException) { } catch (e: UpdateException) {
@@ -164,20 +161,20 @@ object RepositoryUpdater {
context: Context, context: Context,
repository: Repository, repository: Repository,
indexType: IndexType, indexType: IndexType,
callback: (Stage, Long, Long?) -> Unit, callback: (Stage, Long, Long?) -> Unit
): Result<IndexFile> = withContext(Dispatchers.IO) { ): Result<IndexFile> = withContext(Dispatchers.IO) {
val file = Cache.getTemporaryFile(context) val file = Cache.getTemporaryFile(context)
val result = downloader.downloadToFile( val result = downloader.downloadToFile(
url = repository.address.toUri().buildUpon() url = Uri.parse(repository.address).buildUpon()
.appendPath(indexType.jarName).build().toString(), .appendPath(indexType.jarName).build().toString(),
target = file, target = file,
headers = { headers = {
ifModifiedSince(repository.lastModified) ifModifiedSince(repository.lastModified)
etag(repository.entityTag) etag(repository.entityTag)
authentication(repository.authentication) authentication(repository.authentication)
}, }
) { read, total -> ) { read, total ->
callback(Stage.DOWNLOAD, read.value, total?.value) callback(Stage.DOWNLOAD, read.value, total.value.takeIf { it != 0L })
} }
when (result) { when (result) {
@@ -188,8 +185,8 @@ object RepositoryUpdater {
lastModified = result.lastModified?.toFormattedString() ?: "", lastModified = result.lastModified?.toFormattedString() ?: "",
entityTag = result.etag ?: "", entityTag = result.etag ?: "",
statusCode = result.statusCode, statusCode = result.statusCode,
file = file, file = file
), )
) )
} }
@@ -206,8 +203,8 @@ object RepositoryUpdater {
Result.Error( Result.Error(
UpdateException( UpdateException(
errorType = errorType, 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), mergerFile: File = Cache.getTemporaryFile(context),
lastModified: String, lastModified: String,
entityTag: String, entityTag: String,
callback: (Stage, Long, Long?) -> Unit, callback: (Stage, Long, Long?) -> Unit
): Boolean { ): Boolean {
var rollback = true var rollback = true
return synchronized(updaterLock) { return synchronized(updaterLock) {
@@ -261,7 +258,7 @@ object RepositoryUpdater {
name: String, name: String,
description: String, description: String,
version: Int, version: Int,
timestamp: Long, timestamp: Long
) { ) {
changedRepository = repository.update( changedRepository = repository.update(
mirrors, mirrors,
@@ -270,7 +267,7 @@ object RepositoryUpdater {
version, version,
lastModified, lastModified,
entityTag, entityTag,
timestamp, timestamp
) )
} }
@@ -287,7 +284,7 @@ object RepositoryUpdater {
override fun onReleases( override fun onReleases(
packageName: String, packageName: String,
releases: List<Release>, releases: List<Release>
) { ) {
if (Thread.interrupted()) { if (Thread.interrupted()) {
throw InterruptedException() throw InterruptedException()
@@ -298,7 +295,7 @@ object RepositoryUpdater {
unmergedReleases.clear() unmergedReleases.clear()
} }
} }
}, }
) )
if (Thread.interrupted()) { if (Thread.interrupted()) {
@@ -321,11 +318,11 @@ object RepositoryUpdater {
callback( callback(
Stage.MERGE, Stage.MERGE,
progress.toLong(), progress.toLong(),
totalCount.toLong(), totalCount.toLong()
) )
Database.UpdaterAdapter.putTemporary( Database.UpdaterAdapter.putTemporary(
products products
.map { transformProduct(it, features, unstable) }, .map { transformProduct(it, features, unstable) }
) )
} }
} }
@@ -339,7 +336,7 @@ object RepositoryUpdater {
throw UpdateException( throw UpdateException(
ErrorType.VALIDATION, ErrorType.VALIDATION,
"New index is older than current index:" + "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( val commitRepository = if (!workRepository.fingerprint.equals(
fingerprint, fingerprint,
ignoreCase = true, ignoreCase = true
) )
) { ) {
if (workRepository.fingerprint.isNotEmpty()) { if (workRepository.fingerprint.isNotEmpty()) {
throw UpdateException( throw UpdateException(
ErrorType.VALIDATION, ErrorType.VALIDATION,
"Certificate fingerprints do not match", "Certificate fingerprints do not match"
) )
} }
@@ -394,7 +391,7 @@ object RepositoryUpdater {
get() = codeSigners?.singleOrNull() get() = codeSigners?.singleOrNull()
?: throw UpdateException( ?: throw UpdateException(
ErrorType.VALIDATION, 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) @get:Throws(UpdateException::class)
@@ -402,13 +399,13 @@ object RepositoryUpdater {
get() = signerCertPath?.certificates?.singleOrNull() get() = signerCertPath?.certificates?.singleOrNull()
?: throw UpdateException( ?: throw UpdateException(
ErrorType.VALIDATION, ErrorType.VALIDATION,
"index.jar code signer should have only one certificate", "index.jar code signer should have only one certificate"
) )
private fun transformProduct( private fun transformProduct(
product: Product, product: Product,
features: Set<String>, features: Set<String>,
unstable: Boolean, unstable: Boolean
): Product { ): Product {
val releasePairs = product.releases val releasePairs = product.releases
.distinctBy { it.identifier } .distinctBy { it.identifier }
@@ -448,7 +445,7 @@ object RepositoryUpdater {
selected = firstSelected?.let { selected = firstSelected?.let {
it.first.versionCode == release.versionCode && it.first.versionCode == release.versionCode &&
it.second == incompatibilities it.second == incompatibilities
} ?: false, } ?: false
) )
} }
return product.copy(releases = releases) return product.copy(releases = releases)
@@ -460,5 +457,5 @@ data class IndexFile(
val lastModified: String, val lastModified: String,
val entityTag: String, val entityTag: String,
val statusCode: Int, val statusCode: Int,
val file: File, val file: File
) )

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