From 3dac4867fa42119b3e877b1912c948a24bdb78db Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 10 Mar 2026 09:26:41 +0100 Subject: [PATCH 01/14] build: introduce data, domain and ui modules --- .gitignore | 3 + app/build.gradle | 57 +++++-- data/build.gradle | 146 +++++++++++++++++ .../java/foundation/e/apps/OpenForTesting.kt | 46 ++++++ data/src/main/AndroidManifest.xml | 14 ++ domain/build.gradle | 53 +++++++ gradle/libs.versions.toml | 2 + lint.xml | 2 +- settings.gradle | 3 + ui/build.gradle | 149 ++++++++++++++++++ ui/src/main/AndroidManifest.xml | 2 + 11 files changed, 463 insertions(+), 14 deletions(-) create mode 100644 data/build.gradle create mode 100644 data/src/debug/java/foundation/e/apps/OpenForTesting.kt create mode 100644 data/src/main/AndroidManifest.xml create mode 100644 domain/build.gradle create mode 100644 ui/build.gradle create mode 100644 ui/src/main/AndroidManifest.xml diff --git a/.gitignore b/.gitignore index 10cfdbfaf..ea5746ade 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ .externalNativeBuild .cxx local.properties +data/build +domain/build +ui/build diff --git a/app/build.gradle b/app/build.gradle index 8d65015a3..824fa9420 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,26 +161,53 @@ android.applicationVariants.configureEach { variant -> html.required = true } - def javaClasses = fileTree("${buildDir}/intermediates/javac/${variant.name}/classes") { - exclude jacocoFileFilter + def moduleProjects = [ + project(":app"), + project(":data"), + project(":domain"), + project(":ui") + ] + + moduleProjects.each { moduleProject -> + def moduleTestTask = moduleProject.tasks.findByName(unitTestTaskName) + if (moduleTestTask != null) { + dependsOn(moduleTestTask) + } } - def kotlinClasses = fileTree("${buildDir}/tmp/kotlin-classes/${variant.name}") { - exclude jacocoFileFilter + + def classTrees = moduleProjects.collectMany { moduleProject -> + def javaClasses = fileTree("${moduleProject.buildDir}/intermediates/javac/${variant.name}/classes") { + exclude jacocoFileFilter + } + def kotlinClasses = fileTree("${moduleProject.buildDir}/tmp/kotlin-classes/${variant.name}") { + exclude jacocoFileFilter + } + [javaClasses, kotlinClasses] } - classDirectories.from = files(javaClasses, kotlinClasses) + classDirectories.from = files(classTrees) - def sourceDirs = variant.sourceSets.collect { sourceSet -> - def dirs = [] - dirs.addAll(sourceSet.java.srcDirs) - if (sourceSet.hasProperty('kotlin')) { - dirs.addAll(sourceSet.kotlin.srcDirs) + def sourceDirs = moduleProjects.collectMany { moduleProject -> + def androidExtension = moduleProject.extensions.findByName("android") + if (androidExtension == null) { + return [] } - return dirs - }.flatten() + androidExtension.sourceSets.collectMany { sourceSet -> + def dirs = [] + dirs.addAll(sourceSet.java.srcDirs) + if (sourceSet.hasProperty('kotlin')) { + dirs.addAll(sourceSet.kotlin.srcDirs) + } + dirs + } + } sourceDirectories.from = files(sourceDirs) - executionData.from = file("${buildDir}/jacoco/${unitTestTaskName}.exec") + + def execFiles = moduleProjects.collect { moduleProject -> + file("${moduleProject.buildDir}/jacoco/${unitTestTaskName}.exec") + } + executionData.from = files(execFiles) } } @@ -195,6 +222,8 @@ dependencies { // Project dependencies implementation(project(":auth-data-lib")) implementation(project(":parental-control-data")) + implementation(project(":data")) + implementation(project(":ui")) // eFoundation libraries implementation(libs.telemetry) @@ -253,6 +282,8 @@ dependencies { // Testing dependencies testImplementation(libs.truth) testImplementation(libs.junit) + testImplementation(project(":domain")) + testImplementation(project(":parental-control-data")) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) testImplementation(libs.core) diff --git a/data/build.gradle b/data/build.gradle new file mode 100644 index 000000000..ef3636b37 --- /dev/null +++ b/data/build.gradle @@ -0,0 +1,146 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'com.google.devtools.ksp' + id 'com.google.dagger.hilt.android' + id 'kotlin-allopen' + id 'jacoco' + alias libs.plugins.kotlin.serialization +} + +jacoco { + toolVersion = libs.versions.jacoco.get() +} + +tasks.withType(Test).configureEach { + jacoco { + includeNoLocationClasses = true + excludes = ['jdk.internal.*'] + } +} + +def versionMajor = 2 + +def versionMinor = 16 + +def versionPatch = 0 + +def parentalControlPkgName = "foundation.e.parentalcontrol" + +def getGitHashProvider = providers.exec { + commandLine 'git', 'log', '--pretty=format:%h', '-n', '1' +} + +def getGitHash = { + return getGitHashProvider.standardOutput.asText.get().trim() +} + +def getDate = { -> + return new Date().format('yyyyMMddHHmmss') +} + +android { + compileSdk = 36 + + defaultConfig { + minSdk = 30 + targetSdk = 34 + + buildConfigField "String", "APPLICATION_ID", "\"foundation.e.apps\"" + buildConfigField "String", "VERSION_NAME", "\"${versionMajor}.${versionMinor}.${versionPatch}\"" + buildConfigField "String", "BUILD_ID", "\"${getGitHash() + "." + getDate()}\"" + buildConfigField "String", "USER_AGENT", "\"${retrieveKey("user_agent", "Dalvik/2.1.0 (Linux; U; Android %s)")}\"" + buildConfigField "String", "PACKAGE_NAME_PARENTAL_CONTROL", "\"${parentalControlPkgName}\"" + } + + buildFeatures { + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + kotlinOptions { + jvmTarget = '21' + } + + namespace = 'foundation.e.apps.data' + + kotlin.sourceSets.configureEach { + languageSettings.optIn("kotlin.RequiresOptIn") + } +} + +allOpen { + annotation 'foundation.e.apps.OpenClass' + annotation 'foundation.e.apps.OpenForTesting' +} + +dependencies { + api(project(":domain")) + implementation(project(":auth-data-lib")) + implementation(project(":parental-control-data")) + + implementation(libs.gplayapi) + implementation(libs.elib) + + implementation(libs.core.ktx) + implementation(libs.preference.ktx) + implementation(libs.datastore.preferences) + implementation(libs.activity.ktx) + implementation(libs.paging.runtime.ktx) + + implementation(libs.lifecycle.livedata.ktx) + implementation(libs.lifecycle.runtime.ktx) + + implementation(libs.work.runtime.ktx) + testImplementation(libs.work.testing) + + ksp(libs.room.compiler) + implementation(libs.room.ktx) + implementation(libs.room.runtime) + + ksp(libs.hilt.compile) + implementation(libs.hilt.android) + implementation(libs.hilt.work) + ksp(libs.hilt.compiler) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.kotlin.test) + implementation(libs.kotlinx.serialization.json) + + implementation(libs.gson) + implementation(libs.protobuf.javalite) + implementation(libs.retrofit) + implementation(libs.converter.moshi) + implementation(libs.converter.jackson) + implementation(libs.converter.gson) + implementation(libs.moshi.kotlin) + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + implementation(libs.jackson.dataformat.yaml) + + implementation(libs.bcpg.jdk15on) + implementation(libs.timber) + + testImplementation(libs.truth) + testImplementation(libs.junit) + testImplementation(libs.core) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) + testImplementation(libs.core.testing) + testImplementation(libs.mockk) + testImplementation(libs.robolectric) +} + +def retrieveKey(String keyName, String defaultValue) { + Properties properties = new Properties() + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + + return properties.getProperty(keyName, defaultValue) +} diff --git a/data/src/debug/java/foundation/e/apps/OpenForTesting.kt b/data/src/debug/java/foundation/e/apps/OpenForTesting.kt new file mode 100644 index 000000000..43a38762d --- /dev/null +++ b/data/src/debug/java/foundation/e/apps/OpenForTesting.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 ECORP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps + +/** + * This annotation allows us to open some classes for mocking purposes while they are final in + * release builds. + */ +@Target( + allowedTargets = [ + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.CLASS, + ] +) +annotation class OpenClass + +/** + * Annotate a class with [OpenForTesting] if you want it to be extendable in debug builds. + */ +@OpenClass +@Target( + allowedTargets = [ + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.CLASS, + ] +) +annotation class OpenForTesting diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e97bf40a9 --- /dev/null +++ b/data/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/domain/build.gradle b/domain/build.gradle new file mode 100644 index 000000000..444de3a24 --- /dev/null +++ b/domain/build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'jacoco' +} + +jacoco { + toolVersion = libs.versions.jacoco.get() +} + +tasks.withType(Test).configureEach { + jacoco { + includeNoLocationClasses = true + excludes = ['jdk.internal.*'] + } +} + +android { + compileSdk = 36 + + defaultConfig { + minSdk = 30 + targetSdk = 34 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + kotlinOptions { + jvmTarget = '21' + } + + namespace = 'foundation.e.apps.domain' + + kotlin.sourceSets.configureEach { + languageSettings.optIn("kotlin.RequiresOptIn") + } +} + +dependencies { + implementation(libs.javax.inject) + implementation(libs.kotlinx.coroutines.core) + + testImplementation(libs.truth) + testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockk) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec1b556bc..0751a471f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,6 +71,7 @@ coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +compose-ui = { module = "androidx.compose.ui:ui" } compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } @@ -96,6 +97,7 @@ gplayapi = { module = "foundation.e:gplayapi", version.ref = "gplayapi" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt"} hilt-compile = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt"} +javax-inject = { module = "javax.inject:javax.inject", version = "1" } jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonDataformatYaml" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { module = "junit:junit", version.ref = "junit" } diff --git a/lint.xml b/lint.xml index d4ebad233..0de6a9b7d 100644 --- a/lint.xml +++ b/lint.xml @@ -51,10 +51,10 @@ + - diff --git a/settings.gradle b/settings.gradle index 0b3590379..07796292c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -64,3 +64,6 @@ rootProject.name = "App Lounge" include ':app' include ':parental-control-data' include ':auth-data-lib' +include ':data' +include ':domain' +include ':ui' diff --git a/ui/build.gradle b/ui/build.gradle new file mode 100644 index 000000000..17e41fd85 --- /dev/null +++ b/ui/build.gradle @@ -0,0 +1,149 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'com.google.devtools.ksp' + id 'androidx.navigation.safeargs.kotlin' + id 'com.google.dagger.hilt.android' + id 'jacoco' + alias libs.plugins.compose.compiler +} + +jacoco { + toolVersion = libs.versions.jacoco.get() +} + +tasks.withType(Test).configureEach { + jacoco { + includeNoLocationClasses = true + excludes = ['jdk.internal.*'] + } +} + +def versionMajor = 2 + +def versionMinor = 16 + +def versionPatch = 0 + +def parentalControlPkgName = "foundation.e.parentalcontrol" + +android { + compileSdk = 36 + + defaultConfig { + minSdk = 30 + targetSdk = 34 + + buildConfigField "String", "VERSION_NAME", "\"${versionMajor}.${versionMinor}.${versionPatch}\"" + buildConfigField "String", "PACKAGE_NAME_PARENTAL_CONTROL", "\"${parentalControlPkgName}\"" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + buildConfig = true + compose = true + viewBinding = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + kotlinOptions { + jvmTarget = '21' + } + + namespace = 'foundation.e.apps.ui' + + kotlin.sourceSets.configureEach { + languageSettings.optIn("kotlin.RequiresOptIn") + } +} + +dependencies { + implementation(project(":data")) + implementation(project(":domain")) + implementation(project(":parental-control-data")) + + implementation(libs.core.ktx) + implementation(libs.appcompat) + implementation(libs.fragment.ktx) + implementation(libs.preference.ktx) + implementation(libs.constraintlayout) + implementation(libs.legacy.support.v4) + implementation(libs.datastore.preferences) + implementation(libs.viewpager2) + implementation(libs.recyclerview) + implementation(libs.navigation.fragment.ktx) + implementation(libs.navigation.ui.ktx) + implementation(libs.activity.ktx) + implementation(libs.paging.runtime.ktx) + + implementation(libs.material) + + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.livedata.ktx) + implementation(libs.lifecycle.runtime.ktx) + implementation(libs.lifecycle.extensions) + + implementation(libs.work.runtime.ktx) + + ksp(libs.hilt.compile) + implementation(libs.hilt.android) + implementation(libs.hilt.work) + ksp(libs.hilt.compiler) + + implementation(libs.shimmer) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.kotlin.test) + implementation(libs.kotlinx.serialization.json) + + implementation(libs.coil) + implementation(libs.coil.compose) + implementation(libs.photoview) + + implementation(libs.gplayapi) + implementation(libs.elib) + implementation(libs.gson) + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + + implementation(libs.jsoup) + + implementation(libs.timber) + + def composeBom = platform(libs.compose.bom) + implementation composeBom + androidTestImplementation composeBom + + implementation libs.compose.material3 + implementation libs.compose.material.icons.extended + + implementation libs.activity.compose + implementation libs.lifecycle.viewmodel.compose + implementation libs.runtime.livedata + implementation libs.paging.compose + + implementation libs.compose.ui.tooling.preview + debugImplementation libs.compose.ui.tooling + + androidTestImplementation libs.compose.ui.test.junit4 + debugImplementation libs.compose.ui.test.manifest + + testImplementation(libs.truth) + testImplementation(libs.junit) + testImplementation(libs.core) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) + testImplementation(libs.core.testing) + testImplementation(libs.mockk) + testImplementation(libs.robolectric) + + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) +} diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8072ee00d --- /dev/null +++ b/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + -- GitLab From 0c6733aa4489c91724101e12e27f38d3792dc24d Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 10 Mar 2026 13:54:20 +0100 Subject: [PATCH 02/14] refactor(core): add domain foundation types --- .../foundation/e/apps/domain/ResultSupreme.kt | 194 ++++++++++++++++++ .../apps/domain/cleanapk/CleanApkConstants.kt | 30 +++ .../foundation/e/apps/domain/enums/AppTag.kt | 44 ++++ .../e/apps/domain/enums/FilterLevel.kt | 44 ++++ .../e/apps/domain/enums/ResultStatus.kt | 9 + .../foundation/e/apps/domain/enums/Source.kt | 25 +++ .../foundation/e/apps/domain/enums/Status.kt | 42 ++++ .../foundation/e/apps/domain/enums/Type.kt | 6 + .../foundation/e/apps/domain/enums/User.kt | 7 + .../e/apps/domain/system/BuildInfoProvider.kt | 5 + .../e/apps/domain/enums/AppTagTest.kt | 14 ++ .../e/apps/domain/enums/FilterLevelTest.kt | 23 +++ 12 files changed, 443 insertions(+) create mode 100644 domain/src/main/java/foundation/e/apps/domain/ResultSupreme.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/cleanapk/CleanApkConstants.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/enums/AppTag.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/enums/FilterLevel.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/enums/ResultStatus.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/enums/Source.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/enums/Status.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/enums/Type.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/enums/User.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/system/BuildInfoProvider.kt create mode 100644 domain/src/test/java/foundation/e/apps/domain/enums/AppTagTest.kt create mode 100644 domain/src/test/java/foundation/e/apps/domain/enums/FilterLevelTest.kt diff --git a/domain/src/main/java/foundation/e/apps/domain/ResultSupreme.kt b/domain/src/main/java/foundation/e/apps/domain/ResultSupreme.kt new file mode 100644 index 000000000..6c9d394a1 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/ResultSupreme.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2022 ECORP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain + +import foundation.e.apps.domain.enums.ResultStatus +import java.util.concurrent.TimeoutException + +private const val UNKNOWN_ERROR = "Unknown error!" + +/** + * Another implementation of Result class. + * This removes the use of [ResultStatus] class for different status. + * This class also follows the standard code patterns. However, we still have the same + * flaw that [data] is nullable. As such we may have to add extra null checks or just + * brute force with !! + * + * Also since for each case we now use an inner class with slightly different name, + * we need some refactoring. + * + * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/313 + */ +sealed class ResultSupreme { + + /** + * Success case. + * Use [isSuccess] to check. + * + * @param data End result of processing. + */ + class Success(data: T) : ResultSupreme() { + init { setData(data) } + } + + /** + * Timed out during network related job. + * Use [isTimeout] to check. + * + * @param data The process is expected to output some blank data, but it cannot be null. + * Example can be an empty list. + * @param exception Optional exception from try-catch block. + */ + class Timeout(data: T? = null, exception: Exception = TimeoutException()) : + ResultSupreme() { + init { + data?.let { + setData(it) + } + this.exception = exception + } + } + + /** + * Miscellaneous error case. + * No valid data from processing. + * Use [isUnknownError] to check. + */ + open class Error() : ResultSupreme() { + /** + * @param message A String message to log or display to the user. + * @param exception Optional exception from try-catch block. + */ + constructor(message: String, exception: Exception? = null) : this() { + this.message = message + this.exception = exception + } + + /** + * @param data Non-null data. Example a String which could not be parsed into a JSON. + * @param message A optional String message to log or display to the user. + */ + constructor(data: T, message: String = "") : this() { + setData(data) + this.message = message + } + } + + class WorkError constructor(data: T, payload: Any? = null) : Error(data) { + init { + this.otherPayload = payload + } + } + + /** + * Data from processing. May be null. + */ + var data: T? = null + private set + + /** + * A custom string message for logging or displaying to the user. + */ + var message: String = "" + + var otherPayload: Any? = null + + /** + * Exception from try-catch block for error cases. + */ + var exception: Exception? = null + + fun isValidData() = data != null + + fun isSuccess() = this is Success && isValidData() + fun isTimeout() = this is Timeout + fun isUnknownError() = this is Error + + fun setData(data: T) { + this.data = data + } + + fun getResultStatus(): ResultStatus { + return when (this) { + is Success -> ResultStatus.OK + is Timeout -> ResultStatus.TIMEOUT + else -> ResultStatus.UNKNOWN.apply { + message = this@ResultSupreme.exception?.localizedMessage ?: UNKNOWN_ERROR + } + } + } + + companion object { + + /** + * Function to create an instance of ResultSupreme from a [ResultStatus] status, + * and other available info - [data], [message], [exception]. + */ + fun create( + status: ResultStatus, + data: T? = null, + message: String = "", + exception: Exception? = null, + ): ResultSupreme { + val resultObject = when { + status == ResultStatus.OK && data != null -> Success(data) + status == ResultStatus.TIMEOUT && data != null -> Timeout(data) + else -> Error(message.ifBlank { status.message }, exception) + } + resultObject.apply { + if (isUnknownError()) { + this.data = data + } else { + this.message = message.ifBlank { status.message } + this.exception = exception + } + } + return resultObject + } + + /** + * Create a similar [ResultSupreme] instance i.e. of type [Success], [Timeout]... + * using a supplied [result] object but with a different generic type and new data. + * + * @param result Class of [ResultSupreme] whose replica is to be made. + * @param newData Nullable new data for this replica. + * @param message Optional new message for this replica. If not provided, + * the new object will get the message from [result]. + * @param exception Optional new exception for this replica. If not provided, + * the new object will get the exception from [result]. + */ + fun replicate( + result: ResultSupreme<*>, + newData: T?, + message: String? = null, + exception: Exception? = null, + ): ResultSupreme { + val status = when (result) { + is Success -> ResultStatus.OK + is Timeout -> ResultStatus.TIMEOUT + is Error -> ResultStatus.UNKNOWN + } + return create( + status, + newData, + message ?: result.message, + exception ?: result.exception + ) + } + } +} diff --git a/domain/src/main/java/foundation/e/apps/domain/cleanapk/CleanApkConstants.kt b/domain/src/main/java/foundation/e/apps/domain/cleanapk/CleanApkConstants.kt new file mode 100644 index 000000000..1d80a7cdb --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/cleanapk/CleanApkConstants.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.cleanapk + +object CleanApkConstants { + const val BASE_URL = "https://api.cleanapk.org/v2/" + const val ASSET_URL = "https://api.cleanapk.org/v2/media/" + + const val APP_SOURCE_FOSS = "open" + const val APP_SOURCE_ANY = "any" + + const val APP_TYPE_NATIVE = "native" + const val APP_TYPE_PWA = "pwa" + const val APP_TYPE_ANY = "any" +} diff --git a/domain/src/main/java/foundation/e/apps/domain/enums/AppTag.kt b/domain/src/main/java/foundation/e/apps/domain/enums/AppTag.kt new file mode 100644 index 000000000..a4fcbca04 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/enums/AppTag.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019-2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.enums + +/** + * This sealed class is used for the tags shown in the categories screen, + * the [displayTag] holds the tag in the user device specific locale. + * (Example: [OpenSource.displayTag] for Deutsch language = "Quelloffen") + * + * Previously this was hard coded, which led to crashes due to changes in different locales. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5364 + */ +sealed class AppTag(val displayTag: String) { + class OpenSource(displayTag: String) : AppTag(displayTag) + class PWA(displayTag: String) : AppTag(displayTag) + class GPlay(displayTag: String = "") : AppTag(displayTag) + + /** + * In many places in the code, checks are for hard coded string "Open Source". + * This method allows for all those check to work without modification. + */ + fun getOperationalTag(): String { + return if (this is OpenSource) { + "Open Source" + } else { + this::class.java.simpleName + } + } +} diff --git a/domain/src/main/java/foundation/e/apps/domain/enums/FilterLevel.kt b/domain/src/main/java/foundation/e/apps/domain/enums/FilterLevel.kt new file mode 100644 index 000000000..01a37f358 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/enums/FilterLevel.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019-2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.enums + +/** + * Use this class for various levels of filtering. + * + * Example 1: Searching for "Wild rift" should display the app, but show "N/A" for most cases. + * This is because in some countries, the app is downloadable and in some countries it is not, + * hence completely filtering it out of the search results is not the best thing to do. + * Instead if we detect that the app is not downloadable for a region, we use [UI] level + * filter; if it is downloadable for a different region, we then use [NONE] filter. + * + * Similar app: de.tlllr.tlllrfan + * + * Example 2: Some apps like "com.skype.m2" can not only be not downloaded, even its details + * page cannot be opened. Such apps cannot be shown on lists. Hence we use the [DATA] filter. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720 + */ +enum class FilterLevel { + UI, // Show the app in lists, but show "N/A" in the install button. + DATA, // Filter the app out from lists and search results, don't show the app at all. + NONE, // No restrictions + UNKNOWN, // Not initialised yet +} + +fun FilterLevel.isUnFiltered(): Boolean = this == FilterLevel.NONE +fun FilterLevel.isInitialized(): Boolean = this != FilterLevel.UNKNOWN diff --git a/domain/src/main/java/foundation/e/apps/domain/enums/ResultStatus.kt b/domain/src/main/java/foundation/e/apps/domain/enums/ResultStatus.kt new file mode 100644 index 000000000..30a8fa966 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/enums/ResultStatus.kt @@ -0,0 +1,9 @@ +package foundation.e.apps.domain.enums + +enum class ResultStatus { + OK, + TIMEOUT, + UNKNOWN, + RETRY; + var message: String = "" +} diff --git a/domain/src/main/java/foundation/e/apps/domain/enums/Source.kt b/domain/src/main/java/foundation/e/apps/domain/enums/Source.kt new file mode 100644 index 000000000..63aad5037 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/enums/Source.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019-2022 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.enums + +enum class Source { + OPEN_SOURCE, + PWA, + SYSTEM_APP, + PLAY_STORE +} diff --git a/domain/src/main/java/foundation/e/apps/domain/enums/Status.kt b/domain/src/main/java/foundation/e/apps/domain/enums/Status.kt new file mode 100644 index 000000000..37abd2a51 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/enums/Status.kt @@ -0,0 +1,42 @@ +/* + * Copyright ECORP SAS 2022 + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.enums + +enum class Status { + INSTALLED, + UPDATABLE, + INSTALLING, + DOWNLOADING, + DOWNLOADED, + UNAVAILABLE, + QUEUED, + BLOCKED, + INSTALLATION_ISSUE, + AWAITING, + PURCHASE_NEEDED; + + companion object { + val downloadStatuses = setOf( + QUEUED, + AWAITING, + DOWNLOADING, + DOWNLOADED + ) + } +} diff --git a/domain/src/main/java/foundation/e/apps/domain/enums/Type.kt b/domain/src/main/java/foundation/e/apps/domain/enums/Type.kt new file mode 100644 index 000000000..07c864c80 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/enums/Type.kt @@ -0,0 +1,6 @@ +package foundation.e.apps.domain.enums + +enum class Type { + NATIVE, + PWA +} diff --git a/domain/src/main/java/foundation/e/apps/domain/enums/User.kt b/domain/src/main/java/foundation/e/apps/domain/enums/User.kt new file mode 100644 index 000000000..990e43ddd --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/enums/User.kt @@ -0,0 +1,7 @@ +package foundation.e.apps.domain.enums + +enum class User { + NO_GOOGLE, + ANONYMOUS, + GOOGLE +} diff --git a/domain/src/main/java/foundation/e/apps/domain/system/BuildInfoProvider.kt b/domain/src/main/java/foundation/e/apps/domain/system/BuildInfoProvider.kt new file mode 100644 index 000000000..0725b0b5c --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/system/BuildInfoProvider.kt @@ -0,0 +1,5 @@ +package foundation.e.apps.domain.system + +interface BuildInfoProvider { + fun getAppBuildInfo(): String +} diff --git a/domain/src/test/java/foundation/e/apps/domain/enums/AppTagTest.kt b/domain/src/test/java/foundation/e/apps/domain/enums/AppTagTest.kt new file mode 100644 index 000000000..0d22ae30a --- /dev/null +++ b/domain/src/test/java/foundation/e/apps/domain/enums/AppTagTest.kt @@ -0,0 +1,14 @@ +package foundation.e.apps.domain.enums + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class AppTagTest { + + @Test + fun getOperationalTagMatchesLegacyStrings() { + assertThat(AppTag.OpenSource("Libre").getOperationalTag()).isEqualTo("Open Source") + assertThat(AppTag.PWA("Web").getOperationalTag()).isEqualTo("PWA") + assertThat(AppTag.GPlay().getOperationalTag()).isEqualTo("GPlay") + } +} diff --git a/domain/src/test/java/foundation/e/apps/domain/enums/FilterLevelTest.kt b/domain/src/test/java/foundation/e/apps/domain/enums/FilterLevelTest.kt new file mode 100644 index 000000000..ac7c0124a --- /dev/null +++ b/domain/src/test/java/foundation/e/apps/domain/enums/FilterLevelTest.kt @@ -0,0 +1,23 @@ +package foundation.e.apps.domain.enums + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class FilterLevelTest { + + @Test + fun isUnFilteredOnlyForNone() { + assertThat(FilterLevel.NONE.isUnFiltered()).isTrue() + assertThat(FilterLevel.UI.isUnFiltered()).isFalse() + assertThat(FilterLevel.DATA.isUnFiltered()).isFalse() + assertThat(FilterLevel.UNKNOWN.isUnFiltered()).isFalse() + } + + @Test + fun isInitializedExcludesUnknown() { + assertThat(FilterLevel.UNKNOWN.isInitialized()).isFalse() + assertThat(FilterLevel.NONE.isInitialized()).isTrue() + assertThat(FilterLevel.UI.isInitialized()).isTrue() + assertThat(FilterLevel.DATA.isInitialized()).isTrue() + } +} -- GitLab From 46b10a8f2c8930ce4c49b949aa3c4f2979cb0ebd Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 10 Mar 2026 14:08:34 +0100 Subject: [PATCH 03/14] refactor(core): adopt domain source and store contracts --- .../java/foundation/e/apps/data/Stores.kt | 16 +++---- .../data/di/bindings/DomainBindingsModule.kt | 16 +++++++ .../foundation/e/apps/data/enums/Source.kt | 18 +------- .../ui/application/ApplicationFragment.kt | 3 +- .../ApplicationListFragment.kt | 3 +- .../ApplicationListRVAdapter.kt | 3 +- .../ApplicationListViewModel.kt | 3 +- .../categories/model/CategoriesRVAdapter.kt | 7 +-- .../apps/ui/extensions/SourceUiExtensions.kt | 45 +++++++++++++++++++ .../e/apps/data/enums/SourceTest.kt | 30 ------------- .../ui/extensions/SourceUiExtensionsTest.kt | 31 +++++++++++++ .../java/foundation/e/apps/domain/Stores.kt | 28 ++++++++++++ 12 files changed, 141 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt create mode 100644 app/src/main/java/foundation/e/apps/ui/extensions/SourceUiExtensions.kt delete mode 100644 app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt create mode 100644 app/src/test/java/foundation/e/apps/ui/extensions/SourceUiExtensionsTest.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/Stores.kt diff --git a/app/src/main/java/foundation/e/apps/data/Stores.kt b/app/src/main/java/foundation/e/apps/data/Stores.kt index ca076e86c..c0c082b54 100644 --- a/app/src/main/java/foundation/e/apps/data/Stores.kt +++ b/app/src/main/java/foundation/e/apps/data/Stores.kt @@ -22,11 +22,11 @@ package foundation.e.apps.data import foundation.e.apps.data.cleanapk.repositories.CleanApkAppsRepository import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.enums.Source.OPEN_SOURCE -import foundation.e.apps.data.enums.Source.PLAY_STORE -import foundation.e.apps.data.enums.Source.PWA import foundation.e.apps.data.playstore.PlayStoreRepository import foundation.e.apps.data.preference.AppLoungePreference +import foundation.e.apps.domain.enums.Source.OPEN_SOURCE +import foundation.e.apps.domain.enums.Source.PLAY_STORE +import foundation.e.apps.domain.enums.Source.PWA import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -40,7 +40,7 @@ class Stores @Inject constructor( cleanApkAppsRepository: CleanApkAppsRepository, cleanApkPwaRepository: CleanApkPwaRepository, appLoungePreference: AppLoungePreference -) { +) : foundation.e.apps.domain.Stores { private val storeConfigs: Map = buildStoreConfigs( playStoreRepository, @@ -66,28 +66,28 @@ class Stores @Inject constructor( .mapValues { it.value.repository } } - fun getEnabledSearchSources(): List = + override fun getEnabledSearchSources(): List = storeConfigs .filter { (source, config) -> source in searchEligibleSources && config.isEnabled() } .map { (source, _) -> source } fun getStore(source: Source): StoreRepository? = getStores()[source] - fun enableStore(source: Source) { + override fun enableStore(source: Source) { storeConfigs[source]?.enable?.invoke() ?: error("No matching Store found for $source.") _enabledStoresFlow.update { provideEnabledStores() } } - fun disableStore(source: Source) { + override fun disableStore(source: Source) { storeConfigs[source]?.disable?.invoke() ?: error("No matching Store found for $source.") _enabledStoresFlow.update { provideEnabledStores() } } - fun isStoreEnabled(source: Source): Boolean = + override fun isStoreEnabled(source: Source): Boolean = storeConfigs[source]?.isEnabled?.invoke() == true private fun provideEnabledStores(): Set = diff --git a/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt b/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt new file mode 100644 index 000000000..ac4bd4f1a --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt @@ -0,0 +1,16 @@ +package foundation.e.apps.data.di.bindings + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.Stores +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface DomainBindingsModule { + @Binds + @Singleton + fun bindStores(impl: Stores): foundation.e.apps.domain.Stores +} diff --git a/app/src/main/java/foundation/e/apps/data/enums/Source.kt b/app/src/main/java/foundation/e/apps/data/enums/Source.kt index fb1d51a1b..469ed27dc 100644 --- a/app/src/main/java/foundation/e/apps/data/enums/Source.kt +++ b/app/src/main/java/foundation/e/apps/data/enums/Source.kt @@ -17,20 +17,4 @@ package foundation.e.apps.data.enums -import androidx.annotation.StringRes -import foundation.e.apps.R - -enum class Source(@param:StringRes val stringResId: Int?) { - OPEN_SOURCE(R.string.open_source), - PWA(R.string.pwa), - SYSTEM_APP(R.string.system_app), - PLAY_STORE(null); - - fun toString(getString: (Int) -> String) = stringResId?.let(getString) ?: "" - - companion object { - fun fromStringResId(@StringRes source: Int): Source { - return entries.find { it.stringResId == source } ?: PLAY_STORE - } - } -} +typealias Source = foundation.e.apps.domain.enums.Source diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index 3148ea144..655fe8858 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt @@ -76,6 +76,7 @@ import foundation.e.apps.ui.application.ShareButtonVisibilityState.Hidden import foundation.e.apps.ui.application.ShareButtonVisibilityState.Visible import foundation.e.apps.ui.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment +import foundation.e.apps.ui.extensions.toDisplayString import foundation.e.apps.ui.parentFragment.TimeoutFragment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest @@ -447,7 +448,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { val source = if (isFdroidDeepLink) Source.OPEN_SOURCE else args.source if (source == Source.OPEN_SOURCE || source == Source.PWA) { sourceTag.visibility = View.VISIBLE - sourceTag.text = it.source.toString(::getString) + sourceTag.text = it.source.toDisplayString(::getString) } appIcon.load(it.iconUrl) } diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt index 786ea7760..b69beaf02 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt @@ -47,6 +47,7 @@ import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment +import foundation.e.apps.ui.extensions.sourceFromStringResId import foundation.e.apps.ui.parentFragment.TimeoutFragment import kotlinx.coroutines.launch import java.util.Locale @@ -229,7 +230,7 @@ class ApplicationListFragment : * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/478 */ showLoadingUI() - val sourceType = Source.fromStringResId(args.source) + val sourceType = sourceFromStringResId(args.source) viewModel.loadList(args.category, args.source) if (sourceType != Source.OPEN_SOURCE && sourceType != Source.PWA) { /* diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index 596a463d0..85b71b133 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -52,6 +52,7 @@ import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.applicationlist.diffUtils.ConciseAppDiffUtils +import foundation.e.apps.ui.extensions.toDisplayString import foundation.e.apps.ui.search.SearchFragmentDirections import foundation.e.apps.ui.updates.UpdatesFragmentDirections import foundation.e.apps.ui.utils.disableInstallButton @@ -217,7 +218,7 @@ class ApplicationListRVAdapter( private fun ApplicationListItemBinding.updateSourceTag(searchApp: Application) { sourceTag.visibility = View.INVISIBLE - val tag = searchApp.source.toString(root.context::getString) + val tag = searchApp.source.toDisplayString(root.context::getString) if (tag.isNotBlank()) { sourceTag.text = tag sourceTag.visibility = View.VISIBLE diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt index c20c6cc1a..8f1b13d48 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt @@ -30,6 +30,7 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException +import foundation.e.apps.ui.extensions.sourceFromStringResId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -50,7 +51,7 @@ class ApplicationListViewModel @Inject constructor( if (isLoading) { return } - val sourceType = Source.fromStringResId(sourceResourceId) + val sourceType = sourceFromStringResId(sourceResourceId) val isCleanApk = sourceType != Source.PLAY_STORE viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/foundation/e/apps/ui/categories/model/CategoriesRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/categories/model/CategoriesRVAdapter.kt index f4c304efb..c735fe3c4 100644 --- a/app/src/main/java/foundation/e/apps/ui/categories/model/CategoriesRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/categories/model/CategoriesRVAdapter.kt @@ -30,6 +30,7 @@ import foundation.e.apps.data.enums.AppTag import foundation.e.apps.data.enums.Source import foundation.e.apps.databinding.CategoriesListItemBinding import foundation.e.apps.ui.categories.CategoriesFragmentDirections +import foundation.e.apps.ui.extensions.stringResIdOrNull class CategoriesRVAdapter : RecyclerView.Adapter() { @@ -88,9 +89,9 @@ class CategoriesRVAdapter : private fun getSourceStringResId(tag: AppTag): Int? { return when (tag) { - is AppTag.OpenSource -> Source.OPEN_SOURCE.stringResId - is AppTag.PWA -> Source.PWA.stringResId - is AppTag.GPlay -> Source.PLAY_STORE.stringResId + is AppTag.OpenSource -> Source.OPEN_SOURCE.stringResIdOrNull() + is AppTag.PWA -> Source.PWA.stringResIdOrNull() + is AppTag.GPlay -> Source.PLAY_STORE.stringResIdOrNull() } } diff --git a/app/src/main/java/foundation/e/apps/ui/extensions/SourceUiExtensions.kt b/app/src/main/java/foundation/e/apps/ui/extensions/SourceUiExtensions.kt new file mode 100644 index 000000000..f0a9785a7 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/extensions/SourceUiExtensions.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2026 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.ui.extensions + +import androidx.annotation.StringRes +import foundation.e.apps.R +import foundation.e.apps.domain.enums.Source + +@StringRes +fun Source.stringResIdOrNull(): Int? { + return when (this) { + Source.OPEN_SOURCE -> R.string.open_source + Source.PWA -> R.string.pwa + Source.SYSTEM_APP -> R.string.system_app + Source.PLAY_STORE -> null + } +} + +fun Source.toDisplayString(getString: (Int) -> String): String { + return stringResIdOrNull()?.let(getString).orEmpty() +} + +fun sourceFromStringResId(@StringRes source: Int): Source { + return when (source) { + R.string.open_source -> Source.OPEN_SOURCE + R.string.pwa -> Source.PWA + R.string.system_app -> Source.SYSTEM_APP + else -> Source.PLAY_STORE + } +} diff --git a/app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt b/app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt deleted file mode 100644 index 33156a79e..000000000 --- a/app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package foundation.e.apps.data.enums - -import com.google.common.truth.Truth.assertThat -import foundation.e.apps.R -import org.junit.Test - -class SourceTest { - - @Test - fun toStringUsesLocalizedResource() { - val strings = mapOf( - R.string.open_source to "Open Source", - R.string.pwa to "PWA", - R.string.system_app to "System app" - ) - - assertThat(Source.OPEN_SOURCE.toString(strings::getValue)).isEqualTo("Open Source") - assertThat(Source.PWA.toString(strings::getValue)).isEqualTo("PWA") - assertThat(Source.SYSTEM_APP.toString(strings::getValue)).isEqualTo("System app") - assertThat(Source.PLAY_STORE.toString(strings::getValue)).isEqualTo("") - } - - @Test - fun fromStringParsesKnownResourceIds() { - assertThat(Source.fromStringResId(R.string.open_source)).isEqualTo(Source.OPEN_SOURCE) - assertThat(Source.fromStringResId(R.string.pwa)).isEqualTo(Source.PWA) - assertThat(Source.fromStringResId(R.string.system_app)).isEqualTo(Source.SYSTEM_APP) - assertThat(Source.fromStringResId(R.string.app_name)).isEqualTo(Source.PLAY_STORE) - } -} diff --git a/app/src/test/java/foundation/e/apps/ui/extensions/SourceUiExtensionsTest.kt b/app/src/test/java/foundation/e/apps/ui/extensions/SourceUiExtensionsTest.kt new file mode 100644 index 000000000..fba0cca9e --- /dev/null +++ b/app/src/test/java/foundation/e/apps/ui/extensions/SourceUiExtensionsTest.kt @@ -0,0 +1,31 @@ +package foundation.e.apps.ui.extensions + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.R +import foundation.e.apps.domain.enums.Source +import org.junit.Test + +class SourceUiExtensionsTest { + + @Test + fun toDisplayStringUsesLocalizedResource() { + val strings = mapOf( + R.string.open_source to "Open Source", + R.string.pwa to "PWA", + R.string.system_app to "System app" + ) + + assertThat(Source.OPEN_SOURCE.toDisplayString(strings::getValue)).isEqualTo("Open Source") + assertThat(Source.PWA.toDisplayString(strings::getValue)).isEqualTo("PWA") + assertThat(Source.SYSTEM_APP.toDisplayString(strings::getValue)).isEqualTo("System app") + assertThat(Source.PLAY_STORE.toDisplayString(strings::getValue)).isEqualTo("") + } + + @Test + fun sourceFromStringResIdParsesKnownResourceIds() { + assertThat(sourceFromStringResId(R.string.open_source)).isEqualTo(Source.OPEN_SOURCE) + assertThat(sourceFromStringResId(R.string.pwa)).isEqualTo(Source.PWA) + assertThat(sourceFromStringResId(R.string.system_app)).isEqualTo(Source.SYSTEM_APP) + assertThat(sourceFromStringResId(R.string.app_name)).isEqualTo(Source.PLAY_STORE) + } +} diff --git a/domain/src/main/java/foundation/e/apps/domain/Stores.kt b/domain/src/main/java/foundation/e/apps/domain/Stores.kt new file mode 100644 index 000000000..6b867524e --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/Stores.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.domain + +import foundation.e.apps.domain.enums.Source + +interface Stores { + fun enableStore(source: Source) + fun disableStore(source: Source) + fun getEnabledSearchSources(): List + fun isStoreEnabled(source: Source): Boolean +} -- GitLab From 0866e453696602d1d23cec9bc27836be76e8cb08 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 10 Mar 2026 14:14:41 +0100 Subject: [PATCH 04/14] refactor(search): extract domain search contracts --- .../data/di/bindings/DomainBindingsModule.kt | 7 +++++++ .../data/di/bindings/SearchSuggestionModule.kt | 2 +- .../search/PlayStoreSuggestionSource.kt | 2 +- .../apps/data/preference/AppLoungePreference.kt | 5 +++-- .../e/apps/data/search/FakeSuggestionSource.kt | 2 ++ .../search/FetchSearchSuggestionsUseCase.kt | 8 ++++---- .../search/PrepareSearchSubmissionUseCase.kt | 4 ++-- .../domain/search/SearchPreferenceProvider.kt | 5 +++++ .../e/apps/domain/search/SearchRequest.kt | 2 +- .../apps/domain/search/SearchSubmissionResult.kt | 2 +- .../e/apps/domain}/search/SuggestionSource.kt | 2 +- .../search/FetchSearchSuggestionsUseCaseTest.kt | 16 ++++++++-------- .../search/PrepareSearchSubmissionUseCaseTest.kt | 4 ++-- 13 files changed, 38 insertions(+), 23 deletions(-) rename {app => domain}/src/main/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCase.kt (79%) rename {app => domain}/src/main/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCase.kt (96%) create mode 100644 domain/src/main/java/foundation/e/apps/domain/search/SearchPreferenceProvider.kt rename {app => domain}/src/main/java/foundation/e/apps/domain/search/SearchRequest.kt (94%) rename {app => domain}/src/main/java/foundation/e/apps/domain/search/SearchSubmissionResult.kt (95%) rename {app/src/main/java/foundation/e/apps/data => domain/src/main/java/foundation/e/apps/domain}/search/SuggestionSource.kt (95%) rename {app => domain}/src/test/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCaseTest.kt (80%) rename {app => domain}/src/test/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCaseTest.kt (97%) diff --git a/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt b/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt index ac4bd4f1a..43465bbc6 100644 --- a/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt +++ b/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt @@ -5,6 +5,7 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.Stores +import foundation.e.apps.data.preference.AppLoungePreference import javax.inject.Singleton @Module @@ -13,4 +14,10 @@ interface DomainBindingsModule { @Binds @Singleton fun bindStores(impl: Stores): foundation.e.apps.domain.Stores + + @Binds + @Singleton + fun bindSearchPreferenceProvider( + impl: AppLoungePreference + ): foundation.e.apps.domain.search.SearchPreferenceProvider } diff --git a/app/src/main/java/foundation/e/apps/data/di/bindings/SearchSuggestionModule.kt b/app/src/main/java/foundation/e/apps/data/di/bindings/SearchSuggestionModule.kt index eb4164dda..b161230c1 100644 --- a/app/src/main/java/foundation/e/apps/data/di/bindings/SearchSuggestionModule.kt +++ b/app/src/main/java/foundation/e/apps/data/di/bindings/SearchSuggestionModule.kt @@ -23,7 +23,7 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.playstore.search.PlayStoreSuggestionSource -import foundation.e.apps.data.search.SuggestionSource +import foundation.e.apps.domain.search.SuggestionSource import javax.inject.Singleton @Module diff --git a/app/src/main/java/foundation/e/apps/data/playstore/search/PlayStoreSuggestionSource.kt b/app/src/main/java/foundation/e/apps/data/playstore/search/PlayStoreSuggestionSource.kt index fbd2f6e69..b4695e658 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/search/PlayStoreSuggestionSource.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/search/PlayStoreSuggestionSource.kt @@ -19,7 +19,7 @@ package foundation.e.apps.data.playstore.search import foundation.e.apps.data.playstore.PlayStoreSearchHelper -import foundation.e.apps.data.search.SuggestionSource +import foundation.e.apps.domain.search.SuggestionSource import java.util.Locale import javax.inject.Inject diff --git a/app/src/main/java/foundation/e/apps/data/preference/AppLoungePreference.kt b/app/src/main/java/foundation/e/apps/data/preference/AppLoungePreference.kt index 98a50ef64..c4c25cf6e 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/AppLoungePreference.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/AppLoungePreference.kt @@ -29,6 +29,7 @@ import foundation.e.apps.data.Constants.PREFERENCE_SHOW_FOSS import foundation.e.apps.data.Constants.PREFERENCE_SHOW_GPLAY import foundation.e.apps.data.Constants.PREFERENCE_SHOW_PWA import foundation.e.apps.data.enums.User +import foundation.e.apps.domain.search.SearchPreferenceProvider import javax.inject.Inject import javax.inject.Singleton @@ -38,7 +39,7 @@ import javax.inject.Singleton class AppLoungePreference @Inject constructor( @ApplicationContext private val context: Context, private val appLoungeDataStore: AppLoungeDataStore -) { +) : SearchPreferenceProvider { private val preferenceManager = PreferenceManager.getDefaultSharedPreferences(context) @@ -55,7 +56,7 @@ class AppLoungePreference @Inject constructor( fun isOpenSourceSelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_FOSS, true) fun isPWASelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_PWA, true) - fun isPlayStoreSelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_GPLAY, true) + override fun isPlayStoreSelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_GPLAY, true) fun disablePlayStore() = preferenceManager.edit { putBoolean(PREFERENCE_SHOW_GPLAY, false) } fun disableOpenSource() = preferenceManager.edit { putBoolean(PREFERENCE_SHOW_FOSS, false) } diff --git a/app/src/test/java/foundation/e/apps/data/search/FakeSuggestionSource.kt b/app/src/test/java/foundation/e/apps/data/search/FakeSuggestionSource.kt index 911ce3701..302844995 100644 --- a/app/src/test/java/foundation/e/apps/data/search/FakeSuggestionSource.kt +++ b/app/src/test/java/foundation/e/apps/data/search/FakeSuggestionSource.kt @@ -18,6 +18,8 @@ package foundation.e.apps.data.search +import foundation.e.apps.domain.search.SuggestionSource + /* * Copyright (C) 2025 e Foundation * diff --git a/app/src/main/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCase.kt b/domain/src/main/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCase.kt similarity index 79% rename from app/src/main/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCase.kt rename to domain/src/main/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCase.kt index 49ed7ff37..84f634bb6 100644 --- a/app/src/main/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCase.kt +++ b/domain/src/main/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCase.kt @@ -18,16 +18,16 @@ package foundation.e.apps.domain.search -import foundation.e.apps.data.preference.AppLoungePreference -import foundation.e.apps.data.search.SuggestionSource +import foundation.e.apps.domain.search.SuggestionSource +import foundation.e.apps.domain.search.SearchPreferenceProvider import javax.inject.Inject class FetchSearchSuggestionsUseCase @Inject constructor( private val suggestionSource: SuggestionSource, - private val appLoungePreference: AppLoungePreference, + private val searchPreferenceProvider: SearchPreferenceProvider, ) { suspend operator fun invoke(query: String): List { - if (query.isBlank() || !appLoungePreference.isPlayStoreSelected()) { + if (query.isBlank() || !searchPreferenceProvider.isPlayStoreSelected()) { return emptyList() } return suggestionSource.suggest(query) diff --git a/app/src/main/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCase.kt b/domain/src/main/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCase.kt similarity index 96% rename from app/src/main/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCase.kt rename to domain/src/main/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCase.kt index d798460a7..f2948fbd0 100644 --- a/app/src/main/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCase.kt +++ b/domain/src/main/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCase.kt @@ -18,8 +18,8 @@ package foundation.e.apps.domain.search -import foundation.e.apps.data.Stores -import foundation.e.apps.data.enums.Source +import foundation.e.apps.domain.enums.Source +import foundation.e.apps.domain.Stores import javax.inject.Inject class PrepareSearchSubmissionUseCase @Inject constructor( diff --git a/domain/src/main/java/foundation/e/apps/domain/search/SearchPreferenceProvider.kt b/domain/src/main/java/foundation/e/apps/domain/search/SearchPreferenceProvider.kt new file mode 100644 index 000000000..cf9fa89e1 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/search/SearchPreferenceProvider.kt @@ -0,0 +1,5 @@ +package foundation.e.apps.domain.search + +interface SearchPreferenceProvider { + fun isPlayStoreSelected(): Boolean +} diff --git a/app/src/main/java/foundation/e/apps/domain/search/SearchRequest.kt b/domain/src/main/java/foundation/e/apps/domain/search/SearchRequest.kt similarity index 94% rename from app/src/main/java/foundation/e/apps/domain/search/SearchRequest.kt rename to domain/src/main/java/foundation/e/apps/domain/search/SearchRequest.kt index 0615e84e0..19497ac2c 100644 --- a/app/src/main/java/foundation/e/apps/domain/search/SearchRequest.kt +++ b/domain/src/main/java/foundation/e/apps/domain/search/SearchRequest.kt @@ -18,7 +18,7 @@ package foundation.e.apps.domain.search -import foundation.e.apps.data.enums.Source +import foundation.e.apps.domain.enums.Source data class SearchRequest( val query: String, diff --git a/app/src/main/java/foundation/e/apps/domain/search/SearchSubmissionResult.kt b/domain/src/main/java/foundation/e/apps/domain/search/SearchSubmissionResult.kt similarity index 95% rename from app/src/main/java/foundation/e/apps/domain/search/SearchSubmissionResult.kt rename to domain/src/main/java/foundation/e/apps/domain/search/SearchSubmissionResult.kt index 81b13870b..792be8a2d 100644 --- a/app/src/main/java/foundation/e/apps/domain/search/SearchSubmissionResult.kt +++ b/domain/src/main/java/foundation/e/apps/domain/search/SearchSubmissionResult.kt @@ -18,7 +18,7 @@ package foundation.e.apps.domain.search -import foundation.e.apps.data.enums.Source +import foundation.e.apps.domain.enums.Source data class SearchSubmissionResult( val trimmedQuery: String, diff --git a/app/src/main/java/foundation/e/apps/data/search/SuggestionSource.kt b/domain/src/main/java/foundation/e/apps/domain/search/SuggestionSource.kt similarity index 95% rename from app/src/main/java/foundation/e/apps/data/search/SuggestionSource.kt rename to domain/src/main/java/foundation/e/apps/domain/search/SuggestionSource.kt index fe0c30738..0924c51ea 100644 --- a/app/src/main/java/foundation/e/apps/data/search/SuggestionSource.kt +++ b/domain/src/main/java/foundation/e/apps/domain/search/SuggestionSource.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.search +package foundation.e.apps.domain.search interface SuggestionSource { suspend fun suggest(query: String): List diff --git a/app/src/test/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCaseTest.kt b/domain/src/test/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCaseTest.kt similarity index 80% rename from app/src/test/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCaseTest.kt rename to domain/src/test/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCaseTest.kt index c5c2d1cea..6d8f78629 100644 --- a/app/src/test/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCaseTest.kt +++ b/domain/src/test/java/foundation/e/apps/domain/search/FetchSearchSuggestionsUseCaseTest.kt @@ -19,8 +19,8 @@ package foundation.e.apps.domain.search import com.google.common.truth.Truth.assertThat -import foundation.e.apps.data.preference.AppLoungePreference -import foundation.e.apps.data.search.SuggestionSource +import foundation.e.apps.domain.search.SuggestionSource +import foundation.e.apps.domain.search.SearchPreferenceProvider import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -34,19 +34,19 @@ import org.junit.Test class FetchSearchSuggestionsUseCaseTest { private lateinit var suggestionSource: SuggestionSource - private lateinit var appLoungePreference: AppLoungePreference + private lateinit var searchPreferenceProvider: SearchPreferenceProvider private lateinit var useCase: FetchSearchSuggestionsUseCase @Before fun setUp() { suggestionSource = mockk() - appLoungePreference = mockk() - useCase = FetchSearchSuggestionsUseCase(suggestionSource, appLoungePreference) + searchPreferenceProvider = mockk() + useCase = FetchSearchSuggestionsUseCase(suggestionSource, searchPreferenceProvider) } @Test fun `blank query yields empty suggestions`() = runTest { - every { appLoungePreference.isPlayStoreSelected() } returns true + every { searchPreferenceProvider.isPlayStoreSelected() } returns true val result = useCase(" ") @@ -56,7 +56,7 @@ class FetchSearchSuggestionsUseCaseTest { @Test fun `play store disabled yields empty suggestions`() = runTest { - every { appLoungePreference.isPlayStoreSelected() } returns false + every { searchPreferenceProvider.isPlayStoreSelected() } returns false val result = useCase("notes") @@ -66,7 +66,7 @@ class FetchSearchSuggestionsUseCaseTest { @Test fun `eligible query returns suggestion results`() = runTest { - every { appLoungePreference.isPlayStoreSelected() } returns true + every { searchPreferenceProvider.isPlayStoreSelected() } returns true coEvery { suggestionSource.suggest("notes") } returns listOf("notes app") val result = useCase("notes") diff --git a/app/src/test/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCaseTest.kt b/domain/src/test/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCaseTest.kt similarity index 97% rename from app/src/test/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCaseTest.kt rename to domain/src/test/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCaseTest.kt index f8bba4590..8f1248c9c 100644 --- a/app/src/test/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCaseTest.kt +++ b/domain/src/test/java/foundation/e/apps/domain/search/PrepareSearchSubmissionUseCaseTest.kt @@ -19,8 +19,8 @@ package foundation.e.apps.domain.search import com.google.common.truth.Truth.assertThat -import foundation.e.apps.data.Stores -import foundation.e.apps.data.enums.Source +import foundation.e.apps.domain.enums.Source +import foundation.e.apps.domain.Stores import io.mockk.every import io.mockk.mockk import org.junit.Before -- GitLab From 51dcb0d086b7f8299f49b7411a348ac38a858892 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 10 Mar 2026 14:36:27 +0100 Subject: [PATCH 05/14] refactor(core): add domain application models --- .../application/model/AppRestriction.kt | 6 + .../domain/application/model/Application.kt | 135 ++++++++++++++++++ .../apps/domain/application/model/Category.kt | 35 +++++ .../domain/application/model/ContentRating.kt | 8 ++ .../e/apps/domain/application/model/Home.kt | 28 ++++ .../apps/domain/application/model/Ratings.kt | 24 ++++ 6 files changed, 236 insertions(+) create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/model/AppRestriction.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/model/Application.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/model/Category.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/model/ContentRating.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/model/Home.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/model/Ratings.kt diff --git a/domain/src/main/java/foundation/e/apps/domain/application/model/AppRestriction.kt b/domain/src/main/java/foundation/e/apps/domain/application/model/AppRestriction.kt new file mode 100644 index 000000000..82f4b6b50 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/model/AppRestriction.kt @@ -0,0 +1,6 @@ +package foundation.e.apps.domain.application.model + +enum class AppRestriction { + NOT_RESTRICTED, + RESTRICTED, +} diff --git a/domain/src/main/java/foundation/e/apps/domain/application/model/Application.kt b/domain/src/main/java/foundation/e/apps/domain/application/model/Application.kt new file mode 100644 index 000000000..0c1402ba5 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/model/Application.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2021-2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.domain.application.model + +import foundation.e.apps.domain.cleanapk.CleanApkConstants +import foundation.e.apps.domain.enums.FilterLevel +import foundation.e.apps.domain.enums.Source +import foundation.e.apps.domain.enums.Status +import foundation.e.apps.domain.enums.Type +import foundation.e.apps.domain.enums.Type.NATIVE +import foundation.e.apps.domain.enums.Type.PWA + +data class Application( + val _id: String = String(), + val author: String = String(), + val category: String = String(), + val description: String = String(), + var perms: List = emptyList(), + var reportId: Long = -1L, + val icon_image_path: String = String(), + val icon_url: String = String(), + val last_modified: String = String(), + var latest_version_code: Long = -1, + val latest_version_number: String = String(), + val latest_downloaded_version: String = String(), + val licence: String = String(), + val name: String = String(), + val other_images_path: List = emptyList(), + val package_name: String = String(), + val ratings: Ratings = Ratings(), + val offer_type: Int = -1, + var status: Status = Status.UNAVAILABLE, + val shareUrl: String = String(), + val originalSize: Long = 0, + var appSize: String = String(), + var source: Source = Source.PLAY_STORE, + val price: String = String(), + val isFree: Boolean = true, + val is_pwa: Boolean = false, + var pwaPlayerDbId: Long = -1, + val url: String = String(), + var type: Type = NATIVE, + var privacyScore: Int = -1, + var isPurchased: Boolean = false, + var updatedOn: String = String(), + + /* + * Number of permissions and trackers from Exodus Api used for privacy score calculation. + */ + var numberOfPermission: Int = 0, + var numberOfTracker: Int = 0, + + /* + * Store restriction from App. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + var restriction: AppRestriction = AppRestriction.NOT_RESTRICTED, + + /* + * Show a blank app at the end during loading. + * Used when loading apps of a category. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + var isPlaceHolder: Boolean = false, + + /* + * Store the filter/restriction level. + * If it is not NONE, then the app cannot be downloaded. + * If it is FilterLevel.UI, then we should show "N/A" on install button. + * If it is FilterLevel.DATA, then this app should not be displayed. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720 + */ + var filterLevel: FilterLevel = FilterLevel.UNKNOWN, + var isGplayReplaced: Boolean = false, + val isFDroidApp: Boolean = false, + var contentRating: ContentRating = ContentRating(), + val antiFeatures: List> = emptyList(), + var isSystemApp: Boolean = false, +) { + val iconUrl: String? + get() { + if (icon_url.isNotBlank()) { + return icon_url + } + if (icon_image_path.isBlank()) { + return null + } + return when (source) { + Source.OPEN_SOURCE, Source.PWA -> { + if (icon_image_path.startsWith("http")) { + icon_image_path + } else { + CleanApkConstants.ASSET_URL + icon_image_path + } + } + Source.SYSTEM_APP, Source.PLAY_STORE -> icon_image_path + } + } + + fun updateType() { + this.type = if (this.is_pwa) PWA else NATIVE + } + + fun hasExodusPrivacyRating(): Boolean { + return this.reportId.toInt() != -1 + } +} + +val Application.shareUri: String + get() = when (type) { + PWA -> url + NATIVE -> when { + isFDroidApp -> buildFDroidUri(package_name) + else -> shareUrl + } + } + +private fun buildFDroidUri(packageName: String) = "https://f-droid.org/packages/$packageName" diff --git a/domain/src/main/java/foundation/e/apps/domain/application/model/Category.kt b/domain/src/main/java/foundation/e/apps/domain/application/model/Category.kt new file mode 100644 index 000000000..5e0a36796 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/model/Category.kt @@ -0,0 +1,35 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.application.model + +import foundation.e.apps.domain.enums.AppTag +import java.util.UUID + +data class Category( + val id: String = UUID.randomUUID().toString(), + val title: String = String(), + val browseUrl: String = String(), + val imageUrl: String = String(), + var drawable: Int = -1, + /* + * Change tag to standard AppTag class. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5364 + */ + var tag: AppTag = AppTag.GPlay() +) diff --git a/domain/src/main/java/foundation/e/apps/domain/application/model/ContentRating.kt b/domain/src/main/java/foundation/e/apps/domain/application/model/ContentRating.kt new file mode 100644 index 000000000..8b6448764 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/model/ContentRating.kt @@ -0,0 +1,8 @@ +package foundation.e.apps.domain.application.model + +data class ContentRating( + val id: String = String(), + val title: String = String(), + val description: String = String(), + val artworkUrl: String = String(), +) diff --git a/domain/src/main/java/foundation/e/apps/domain/application/model/Home.kt b/domain/src/main/java/foundation/e/apps/domain/application/model/Home.kt new file mode 100644 index 000000000..a2d38041c --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/model/Home.kt @@ -0,0 +1,28 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.application.model + +import java.util.UUID + +data class Home( + val title: String = String(), + val list: List = emptyList(), + var source: String = String(), + var id: String = UUID.randomUUID().toString() +) diff --git a/domain/src/main/java/foundation/e/apps/domain/application/model/Ratings.kt b/domain/src/main/java/foundation/e/apps/domain/application/model/Ratings.kt new file mode 100644 index 000000000..ee3ccfa32 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/model/Ratings.kt @@ -0,0 +1,24 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.application.model + +data class Ratings( + val privacyScore: Double = -1.0, + val usageQualityScore: Double = -1.0 +) -- GitLab From 0f6e767008b8f9deb8cdbeeecef74576458827d1 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 10 Mar 2026 14:43:45 +0100 Subject: [PATCH 06/14] refactor(core): add domain home and application contracts --- .../e/apps/domain/application/apps/AppsApi.kt | 66 +++++++++++++++++++ .../exceptions/AppNotFoundException.kt | 3 + .../e/apps/domain/home/HomeRepository.kt | 9 +++ 3 files changed, 78 insertions(+) create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/apps/AppsApi.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/exceptions/AppNotFoundException.kt create mode 100644 domain/src/main/java/foundation/e/apps/domain/home/HomeRepository.kt diff --git a/domain/src/main/java/foundation/e/apps/domain/application/apps/AppsApi.kt b/domain/src/main/java/foundation/e/apps/domain/application/apps/AppsApi.kt new file mode 100644 index 000000000..81a6cf007 --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/apps/AppsApi.kt @@ -0,0 +1,66 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.application.apps + +import foundation.e.apps.domain.application.model.Application +import foundation.e.apps.domain.enums.FilterLevel +import foundation.e.apps.domain.enums.ResultStatus +import foundation.e.apps.domain.enums.Source +import foundation.e.apps.domain.enums.Status + +interface AppsApi { + + /* + * Function to search cleanapk using package name. + * Will be used to handle f-droid deeplink. + */ + suspend fun getCleanapkAppDetails(packageName: String): Pair + + suspend fun getApplicationDetails( + packageNameList: List, + source: Source + ): Pair, ResultStatus> + + suspend fun getApplicationDetails( + id: String, + packageName: String, + source: Source + ): Pair + + /** + * Get fused app installation status. + * Applicable for both native apps and PWAs. + * + * Recommended to use this instead of [PkgManagerModule.getPackageStatus]. + */ + fun getFusedAppInstallationStatus(application: Application): Status + + suspend fun getAppFilterLevel(application: Application): FilterLevel + + /** + * @return returns true if there is changes in data, otherwise false + */ + fun isAnyFusedAppUpdated( + newApplications: List, + oldApplications: List + ): Boolean + + fun isAnyAppInstallStatusChanged(currentList: List): Boolean + fun isOpenSourceSelected(): Boolean +} diff --git a/domain/src/main/java/foundation/e/apps/domain/application/exceptions/AppNotFoundException.kt b/domain/src/main/java/foundation/e/apps/domain/application/exceptions/AppNotFoundException.kt new file mode 100644 index 000000000..8b71ebd9d --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/exceptions/AppNotFoundException.kt @@ -0,0 +1,3 @@ +package foundation.e.apps.domain.application.exceptions + +class AppNotFoundException : Exception() diff --git a/domain/src/main/java/foundation/e/apps/domain/home/HomeRepository.kt b/domain/src/main/java/foundation/e/apps/domain/home/HomeRepository.kt new file mode 100644 index 000000000..c3ed1b9bc --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/home/HomeRepository.kt @@ -0,0 +1,9 @@ +package foundation.e.apps.domain.home + +import foundation.e.apps.domain.ResultSupreme +import foundation.e.apps.domain.application.model.Home +import kotlinx.coroutines.flow.Flow + +interface HomeRepository { + fun getHomeScreenData(): Flow>> +} -- GitLab From 5efc0f2720e7984c944a9d17541221a4cc4e4232 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 10 Mar 2026 21:55:36 +0100 Subject: [PATCH 07/14] refactor(application): add domain application type --- .../domain/application/ApplicationDomain.kt | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 domain/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt diff --git a/domain/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt b/domain/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt new file mode 100644 index 000000000..689f5295d --- /dev/null +++ b/domain/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.domain.application + +import foundation.e.apps.domain.application.model.AppRestriction +import foundation.e.apps.domain.application.model.ContentRating +import foundation.e.apps.domain.application.model.Ratings +import foundation.e.apps.domain.enums.FilterLevel +import foundation.e.apps.domain.enums.Source +import foundation.e.apps.domain.enums.Status +import foundation.e.apps.domain.enums.Type + +data class ApplicationDomain( + val id: String = String(), + val author: String = String(), + val category: String = String(), + val description: String = String(), + val perms: List = emptyList(), + val reportId: Long = -1L, + val packageName: String = String(), + val name: String = String(), + val iconImagePath: String = String(), + val iconUrl: String? = null, + val otherImagesPath: List = emptyList(), + val lastModified: String = String(), + val latestVersionCode: Long = -1, + val latestVersionNumber: String = String(), + val latestDownloadedVersion: String = String(), + val licence: String = String(), + val source: Source = Source.PLAY_STORE, + val status: Status = Status.UNAVAILABLE, + val ratings: Ratings = Ratings(), + val offerType: Int = -1, + val isFree: Boolean = true, + val price: String = String(), + val isPwa: Boolean = false, + val url: String = String(), + val pwaPlayerDbId: Long = -1L, + val type: Type = Type.NATIVE, + val originalSize: Long = 0, + val appSize: String = String(), + val shareUrl: String = String(), + val privacyScore: Int = -1, + val isPurchased: Boolean = false, + val updatedOn: String = String(), + val numberOfPermission: Int = 0, + val numberOfTracker: Int = 0, + val contentRating: ContentRating = ContentRating(), + val restriction: AppRestriction = AppRestriction.NOT_RESTRICTED, + val filterLevel: FilterLevel = FilterLevel.UNKNOWN, + val isGplayReplaced: Boolean = false, + val isFDroidApp: Boolean = false, + val antiFeatures: List> = emptyList(), + val isPlaceHolder: Boolean = false, + val isSystemApp: Boolean = false, +) -- GitLab From 1275f171e6df162d1aac6a8621c4206f2636f7a5 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 10 Mar 2026 22:08:07 +0100 Subject: [PATCH 08/14] refactor(home): move home domain flow to domain module --- .../data/application/HomeRepositoryAdapter.kt | 116 ++++++++++++++++++ .../data/di/bindings/DomainBindingsModule.kt | 7 ++ .../foundation/e/apps/ui/home/HomeFragment.kt | 2 +- .../e/apps/ui/home/HomeViewModel.kt | 8 +- .../domain/home/FetchHomeScreenDataUseCase.kt | 17 ++- .../e/apps/domain/home/HomeScreenResult.kt | 0 .../e/apps/domain/home/HomeSection.kt | 0 .../home/FetchHomeScreenDataUseCaseTest.kt | 75 ++++++----- 8 files changed, 172 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/application/HomeRepositoryAdapter.kt rename {app => domain}/src/main/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCase.kt (86%) rename {app => domain}/src/main/java/foundation/e/apps/domain/home/HomeScreenResult.kt (100%) rename {app => domain}/src/main/java/foundation/e/apps/domain/home/HomeSection.kt (100%) rename {app => domain}/src/test/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCaseTest.kt (54%) diff --git a/app/src/main/java/foundation/e/apps/data/application/HomeRepositoryAdapter.kt b/app/src/main/java/foundation/e/apps/data/application/HomeRepositoryAdapter.kt new file mode 100644 index 000000000..14b62f52b --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/application/HomeRepositoryAdapter.kt @@ -0,0 +1,116 @@ +package foundation.e.apps.data.application + +import androidx.lifecycle.asFlow +import foundation.e.apps.domain.ResultSupreme +import foundation.e.apps.domain.application.model.Application +import foundation.e.apps.domain.application.model.Home +import foundation.e.apps.domain.home.HomeRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class HomeRepositoryAdapter @Inject constructor( + private val applicationRepository: ApplicationRepository, +) : HomeRepository { + override fun getHomeScreenData(): Flow>> { + return applicationRepository.getHomeScreenData().asFlow().map { result -> + when (result) { + is foundation.e.apps.data.ResultSupreme.Success -> { + ResultSupreme.Success(result.data.orEmpty().map { it.toDomain() }) + } + + is foundation.e.apps.data.ResultSupreme.Timeout -> { + ResultSupreme.Timeout( + result.data?.map { it.toDomain() }, + result.exception ?: java.util.concurrent.TimeoutException(), + ) + } + + is foundation.e.apps.data.ResultSupreme.Error -> { + val data = result.data?.map { it.toDomain() } + ResultSupreme.create( + foundation.e.apps.domain.enums.ResultStatus.UNKNOWN, + data, + result.message, + result.exception, + ) + } + } + } + } + + private fun foundation.e.apps.data.application.data.Home.toDomain() = Home( + title = title, + list = list.map { it.toDomain() }, + source = source, + id = id, + ) + + private fun foundation.e.apps.data.application.data.Application.toDomain() = Application( + _id = _id, + author = author, + category = category, + description = description, + perms = perms, + reportId = reportId, + icon_image_path = icon_image_path, + icon_url = icon_url, + last_modified = last_modified, + latest_version_code = latest_version_code, + latest_version_number = latest_version_number, + latest_downloaded_version = latest_downloaded_version, + licence = licence, + name = name, + other_images_path = other_images_path, + package_name = package_name, + ratings = ratings.toDomain(), + offer_type = offer_type, + status = foundation.e.apps.domain.enums.Status.valueOf(status.name), + shareUrl = shareUrl, + originalSize = originalSize, + appSize = appSize, + source = source, + price = price, + isFree = isFree, + is_pwa = is_pwa, + pwaPlayerDbId = pwaPlayerDbId, + url = url, + type = foundation.e.apps.domain.enums.Type.valueOf(type.name), + privacyScore = privacyScore, + isPurchased = isPurchased, + updatedOn = updatedOn, + numberOfPermission = numberOfPermission, + numberOfTracker = numberOfTracker, + restriction = restriction.toDomain(), + isPlaceHolder = isPlaceHolder, + filterLevel = foundation.e.apps.domain.enums.FilterLevel.valueOf(filterLevel.name), + isGplayReplaced = isGplayReplaced, + isFDroidApp = isFDroidApp, + contentRating = contentRating.toDomain(), + antiFeatures = antiFeatures, + isSystemApp = isSystemApp, + ) + + private fun foundation.e.apps.data.application.data.Ratings.toDomain() = + foundation.e.apps.domain.application.model.Ratings( + privacyScore = privacyScore, + usageQualityScore = usageQualityScore, + ) + + private fun com.aurora.gplayapi.data.models.ContentRating.toDomain() = + foundation.e.apps.domain.application.model.ContentRating( + id = id, + title = title, + description = description, + artworkUrl = artwork.url, + ) + + private fun com.aurora.gplayapi.Constants.Restriction.toDomain() = + if (this == com.aurora.gplayapi.Constants.Restriction.NOT_RESTRICTED) { + foundation.e.apps.domain.application.model.AppRestriction.NOT_RESTRICTED + } else { + foundation.e.apps.domain.application.model.AppRestriction.RESTRICTED + } +} diff --git a/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt b/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt index 43465bbc6..b0d2bf58c 100644 --- a/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt +++ b/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt @@ -5,6 +5,7 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.Stores +import foundation.e.apps.data.application.HomeRepositoryAdapter import foundation.e.apps.data.preference.AppLoungePreference import javax.inject.Singleton @@ -15,6 +16,12 @@ interface DomainBindingsModule { @Singleton fun bindStores(impl: Stores): foundation.e.apps.domain.Stores + @Binds + @Singleton + fun bindHomeRepository( + impl: HomeRepositoryAdapter + ): foundation.e.apps.domain.home.HomeRepository + @Binds @Singleton fun bindSearchPreferenceProvider( diff --git a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt index 33acdd009..63b3910b1 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt @@ -115,7 +115,7 @@ class HomeFragment : Fragment(R.layout.fragment_home) { private fun loadData() { if (shouldLoadData()) { showLoadingUI() - homeViewModel.getHomeScreenData(viewLifecycleOwner) + homeViewModel.getHomeScreenData() } } diff --git a/app/src/main/java/foundation/e/apps/ui/home/HomeViewModel.kt b/app/src/main/java/foundation/e/apps/ui/home/HomeViewModel.kt index 025a30260..b6efbc08d 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/HomeViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/HomeViewModel.kt @@ -19,7 +19,6 @@ package foundation.e.apps.ui.home import androidx.annotation.VisibleForTesting -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -31,6 +30,7 @@ import foundation.e.apps.domain.home.FetchHomeScreenDataUseCase import foundation.e.apps.domain.home.HomeScreenResult import foundation.e.apps.domain.home.HomeSection import foundation.e.apps.ui.home.model.ApplicationDomainDiffUtil +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import javax.inject.Inject @@ -66,13 +66,13 @@ class HomeViewModel @Inject constructor( return true } - fun getHomeScreenData(lifecycleOwner: LifecycleOwner) { + fun getHomeScreenData() { viewModelScope.launch { - fetchHomeScreenDataUseCase().observe(lifecycleOwner) { + fetchHomeScreenDataUseCase().collect { postHomeResult(it) if (it.isSuccess()) { - return@observe + return@collect } } } diff --git a/app/src/main/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCase.kt b/domain/src/main/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCase.kt similarity index 86% rename from app/src/main/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCase.kt rename to domain/src/main/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCase.kt index 16924f26d..500c1bab1 100644 --- a/app/src/main/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCase.kt +++ b/domain/src/main/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCase.kt @@ -18,20 +18,19 @@ package foundation.e.apps.domain.home -import androidx.lifecycle.LiveData -import androidx.lifecycle.map -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.application.data.Home +import foundation.e.apps.domain.ResultSupreme import foundation.e.apps.domain.application.ApplicationDomain +import foundation.e.apps.domain.application.model.Application +import foundation.e.apps.domain.application.model.Home +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import javax.inject.Inject class FetchHomeScreenDataUseCase @Inject constructor( - private val applicationRepository: ApplicationRepository, + private val homeRepository: HomeRepository, ) { - operator fun invoke(): LiveData { - return applicationRepository.getHomeScreenData().map { result -> + operator fun invoke(): Flow { + return homeRepository.getHomeScreenData().map { result -> val homeSections = result.data?.map { it.toDomain() }.orEmpty() when (result) { is ResultSupreme.Success -> HomeScreenResult.Success(homeSections) diff --git a/app/src/main/java/foundation/e/apps/domain/home/HomeScreenResult.kt b/domain/src/main/java/foundation/e/apps/domain/home/HomeScreenResult.kt similarity index 100% rename from app/src/main/java/foundation/e/apps/domain/home/HomeScreenResult.kt rename to domain/src/main/java/foundation/e/apps/domain/home/HomeScreenResult.kt diff --git a/app/src/main/java/foundation/e/apps/domain/home/HomeSection.kt b/domain/src/main/java/foundation/e/apps/domain/home/HomeSection.kt similarity index 100% rename from app/src/main/java/foundation/e/apps/domain/home/HomeSection.kt rename to domain/src/main/java/foundation/e/apps/domain/home/HomeSection.kt diff --git a/app/src/test/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCaseTest.kt b/domain/src/test/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCaseTest.kt similarity index 54% rename from app/src/test/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCaseTest.kt rename to domain/src/test/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCaseTest.kt index ae1cb31e3..7d4c5325e 100644 --- a/app/src/test/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCaseTest.kt +++ b/domain/src/test/java/foundation/e/apps/domain/home/FetchHomeScreenDataUseCaseTest.kt @@ -17,46 +17,49 @@ package foundation.e.apps.domain.home -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asFlow import com.google.common.truth.Truth.assertThat -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.application.data.Home -import foundation.e.apps.util.MainCoroutineRule +import foundation.e.apps.domain.ResultSupreme +import foundation.e.apps.domain.application.model.Application +import foundation.e.apps.domain.application.model.Home +import foundation.e.apps.domain.cleanapk.CleanApkConstants import io.mockk.coEvery import io.mockk.mockk +import java.lang.IllegalStateException +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import org.junit.Rule +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before import org.junit.Test -import java.lang.IllegalStateException @OptIn(ExperimentalCoroutinesApi::class) class FetchHomeScreenDataUseCaseTest { + private val homeRepository = mockk() + private val testDispatcher = StandardTestDispatcher() - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - - @get:Rule - val mainCoroutineRule = MainCoroutineRule() + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + } - private val applicationRepository = mockk() + @After + fun tearDown() { + Dispatchers.resetMain() + } @Test - fun invoke_maps_result_to_domain() = runTest(mainCoroutineRule.testDispatcher) { + fun invoke_maps_result_to_domain() = runTest(testDispatcher) { val app = Application(_id = "app-1", package_name = "pkg.one", name = "App One") - val home = Home(title = "Play", list = listOf(app), source = ApplicationRepository.APP_TYPE_ANY) - val liveData = MutableLiveData>>() - liveData.value = ResultSupreme.Success(listOf(home)) - - coEvery { applicationRepository.getHomeScreenData() } returns liveData + val home = Home(title = "Play", list = listOf(app), source = CleanApkConstants.APP_TYPE_ANY) + coEvery { homeRepository.getHomeScreenData() } returns flowOf(ResultSupreme.Success(listOf(home))) - val useCase = FetchHomeScreenDataUseCase(applicationRepository) - val result = useCase().asFlow().first() + val useCase = FetchHomeScreenDataUseCase(homeRepository) + val result = useCase().first() assertThat(result).isInstanceOf(HomeScreenResult.Success::class.java) assertThat(result.data).hasSize(1) @@ -66,16 +69,13 @@ class FetchHomeScreenDataUseCaseTest { } @Test - fun invoke_maps_timeout_to_domain() = runTest(mainCoroutineRule.testDispatcher) { + fun invoke_maps_timeout_to_domain() = runTest(testDispatcher) { val app = Application(_id = "app-2", package_name = "pkg.two", name = "App Two") - val home = Home(title = "Play", list = listOf(app), source = ApplicationRepository.APP_TYPE_ANY) - val liveData = MutableLiveData>>() - liveData.value = ResultSupreme.Timeout(listOf(home)) + val home = Home(title = "Play", list = listOf(app), source = CleanApkConstants.APP_TYPE_ANY) + coEvery { homeRepository.getHomeScreenData() } returns flowOf(ResultSupreme.Timeout(listOf(home))) - coEvery { applicationRepository.getHomeScreenData() } returns liveData - - val useCase = FetchHomeScreenDataUseCase(applicationRepository) - val result = useCase().asFlow().first() + val useCase = FetchHomeScreenDataUseCase(homeRepository) + val result = useCase().first() assertThat(result).isInstanceOf(HomeScreenResult.Timeout::class.java) assertThat(result.data).hasSize(1) @@ -83,15 +83,12 @@ class FetchHomeScreenDataUseCaseTest { } @Test - fun invoke_maps_error_to_domain() = runTest(mainCoroutineRule.testDispatcher) { - val liveData = MutableLiveData>>() + fun invoke_maps_error_to_domain() = runTest(testDispatcher) { val exception = IllegalStateException("boom") - liveData.value = ResultSupreme.Error("failed", exception) - - coEvery { applicationRepository.getHomeScreenData() } returns liveData + coEvery { homeRepository.getHomeScreenData() } returns flowOf(ResultSupreme.Error("failed", exception)) - val useCase = FetchHomeScreenDataUseCase(applicationRepository) - val result = useCase().asFlow().first() + val useCase = FetchHomeScreenDataUseCase(homeRepository) + val result = useCase().first() assertThat(result).isInstanceOf(HomeScreenResult.Error::class.java) val error = result as HomeScreenResult.Error -- GitLab From 110522f353fdfdc351abe05f162f8423a5e557f5 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 11 Mar 2026 09:59:06 +0100 Subject: [PATCH 09/14] remove duplication --- app/build.gradle | 1 + .../mapper/ApplicationDomainMapper.kt | 39 ++++++++-- .../domain/application/ApplicationDomain.kt | 72 ------------------- .../java/foundation/e/apps/ui/MainActivity.kt | 4 +- .../e/apps/ui/MainActivityViewModel.kt | 4 +- .../foundation/e/apps/ui/home/HomeFragment.kt | 2 +- .../apps/ui/home/model/HomeChildRVAdapter.kt | 24 ++++--- 7 files changed, 52 insertions(+), 94 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt diff --git a/app/build.gradle b/app/build.gradle index 824fa9420..4f29f3484 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -223,6 +223,7 @@ dependencies { implementation(project(":auth-data-lib")) implementation(project(":parental-control-data")) implementation(project(":data")) + implementation(project(":domain")) implementation(project(":ui")) // eFoundation libraries diff --git a/app/src/main/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapper.kt b/app/src/main/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapper.kt index 22ac25a74..c1db36da2 100644 --- a/app/src/main/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapper.kt +++ b/app/src/main/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapper.kt @@ -18,7 +18,11 @@ package foundation.e.apps.data.application.mapper +import com.aurora.gplayapi.Constants +import com.aurora.gplayapi.data.models.Artwork +import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.domain.application.ApplicationDomain fun ApplicationDomain.toApplication() = Application( @@ -39,29 +43,50 @@ fun ApplicationDomain.toApplication() = Application( other_images_path = otherImagesPath, package_name = packageName, offer_type = offerType, - status = status, + status = foundation.e.apps.data.enums.Status.valueOf(status.name), shareUrl = shareUrl, originalSize = originalSize, appSize = appSize, - source = source, + source = foundation.e.apps.data.enums.Source.valueOf(source.name), price = price, isFree = isFree, is_pwa = isPwa, pwaPlayerDbId = pwaPlayerDbId, url = url, - type = type, + type = foundation.e.apps.data.enums.Type.valueOf(type.name), privacyScore = privacyScore, isPurchased = isPurchased, updatedOn = updatedOn, numberOfPermission = numberOfPermission, numberOfTracker = numberOfTracker, - filterLevel = filterLevel, + filterLevel = foundation.e.apps.data.enums.FilterLevel.valueOf(filterLevel.name), isGplayReplaced = isGplayReplaced, isFDroidApp = isFDroidApp, - contentRating = contentRating, - restriction = restriction, + contentRating = contentRating.toAurora(), + restriction = restriction.toAurora(), antiFeatures = antiFeatures, isPlaceHolder = isPlaceHolder, - ratings = ratings, + ratings = ratings.toData(), isSystemApp = isSystemApp, ) + +private fun foundation.e.apps.domain.application.model.Ratings.toData() = Ratings( + privacyScore = privacyScore, + usageQualityScore = usageQualityScore, +) + +private fun foundation.e.apps.domain.application.model.ContentRating.toAurora() = ContentRating( + id = id, + title = title, + description = description, + artwork = Artwork(url = artworkUrl), +) + +private fun foundation.e.apps.domain.application.model.AppRestriction.toAurora() = + if (this == foundation.e.apps.domain.application.model.AppRestriction.NOT_RESTRICTED) { + Constants.Restriction.NOT_RESTRICTED + } else { + Constants.Restriction.values() + .firstOrNull { it != Constants.Restriction.NOT_RESTRICTED } + ?: Constants.Restriction.NOT_RESTRICTED + } diff --git a/app/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt b/app/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt deleted file mode 100644 index 0bea64fb4..000000000 --- a/app/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2026 e Foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package foundation.e.apps.domain.application - -import com.aurora.gplayapi.Constants.Restriction -import com.aurora.gplayapi.data.models.ContentRating -import foundation.e.apps.data.application.data.Ratings -import foundation.e.apps.data.enums.FilterLevel -import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.enums.Type - -data class ApplicationDomain( - val id: String = String(), - val author: String = String(), - val category: String = String(), - val description: String = String(), - val perms: List = emptyList(), - val reportId: Long = -1L, - val packageName: String = String(), - val name: String = String(), - val iconImagePath: String = String(), - val iconUrl: String? = null, - val otherImagesPath: List = emptyList(), - val lastModified: String = String(), - val latestVersionCode: Long = -1, - val latestVersionNumber: String = String(), - val latestDownloadedVersion: String = String(), - val licence: String = String(), - val source: Source = Source.PLAY_STORE, - val status: Status = Status.UNAVAILABLE, - val ratings: Ratings = Ratings(), - val offerType: Int = -1, - val isFree: Boolean = true, - val price: String = String(), - val isPwa: Boolean = false, - val url: String = String(), - val pwaPlayerDbId: Long = -1L, - val type: Type = Type.NATIVE, - val originalSize: Long = 0, - val appSize: String = String(), - val shareUrl: String = String(), - val privacyScore: Int = -1, - val isPurchased: Boolean = false, - val updatedOn: String = String(), - val numberOfPermission: Int = 0, - val numberOfTracker: Int = 0, - val contentRating: ContentRating = ContentRating(), - val restriction: Restriction = Restriction.NOT_RESTRICTED, - val filterLevel: FilterLevel = FilterLevel.UNKNOWN, - val isGplayReplaced: Boolean = false, - val isFDroidApp: Boolean = false, - val antiFeatures: List> = emptyList(), - val isPlaceHolder: Boolean = false, - val isSystemApp: Boolean = false, -) diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt index 6102b1d82..79683013e 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -47,14 +47,14 @@ import foundation.e.apps.BuildConfig import foundation.e.apps.R import foundation.e.apps.contract.ParentalControlContract.COLUMN_LOGIN_TYPE import foundation.e.apps.data.Constants -import foundation.e.apps.data.enums.User import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.event.EventBus -import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.updates.UpdatesNotifier import foundation.e.apps.data.login.core.StoreType import foundation.e.apps.data.system.ParentalControlAuthenticator import foundation.e.apps.databinding.ActivityMainBinding +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.error.AppUnavailableDialogDirections import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index 8d41a6a95..1daf284ec 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -232,7 +232,7 @@ class MainActivityViewModel @Inject constructor( homeApp: ApplicationDomain, alertDialogContext: Context? = null ): Boolean { - if (!homeApp.filterLevel.isUnFiltered()) { + if (homeApp.filterLevel != foundation.e.apps.domain.enums.FilterLevel.NONE) { alertDialogContext?.let { context -> AlertDialog.Builder(context).apply { setTitle(R.string.unsupported_app_title) @@ -336,7 +336,7 @@ class MainActivityViewModel @Inject constructor( } val status = downloadingItem?.status ?: applicationRepository.getFusedAppInstallationStatus(homeApp.toApplication()) - homeApp.copy(status = status) + homeApp.copy(status = foundation.e.apps.domain.enums.Status.valueOf(status.name)) } } diff --git a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt index 63b3910b1..f37ea04e4 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt @@ -28,10 +28,10 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R -import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.download.data.DownloadProgress import foundation.e.apps.databinding.FragmentHomeBinding import foundation.e.apps.domain.application.ApplicationDomain +import foundation.e.apps.domain.enums.Status import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel diff --git a/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt index 3c527119f..dca17a759 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt @@ -32,11 +32,12 @@ import com.facebook.shimmer.ShimmerDrawable import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import foundation.e.apps.R -import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.enums.Status as DataStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.state.LoginState import foundation.e.apps.databinding.HomeChildListItemBinding import foundation.e.apps.domain.application.ApplicationDomain +import foundation.e.apps.domain.enums.Status as DomainStatus import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.home.HomeFragmentDirections @@ -96,31 +97,34 @@ class HomeChildRVAdapter( } when (homeApp.status) { - Status.INSTALLED -> { + DomainStatus.INSTALLED -> { handleInstalled(homeApp) } - Status.UPDATABLE -> { + DomainStatus.UPDATABLE -> { handleUpdatable(homeApp) } - Status.UNAVAILABLE -> { + DomainStatus.UNAVAILABLE -> { handleUnavailable(homeApp, holder) } - Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> { + DomainStatus.QUEUED, + DomainStatus.AWAITING, + DomainStatus.DOWNLOADING, + DomainStatus.DOWNLOADED -> { handleQueued(homeApp) } - Status.INSTALLING -> { + DomainStatus.INSTALLING -> { handleInstalling() } - Status.BLOCKED -> { + DomainStatus.BLOCKED -> { handleBlocked() } - Status.INSTALLATION_ISSUE -> { + DomainStatus.INSTALLATION_ISSUE -> { handleInstallationIssue(homeApp) } @@ -217,7 +221,7 @@ class HomeChildRVAdapter( homeApp: ApplicationDomain ) { installButton.apply { - enableInstallButton(Status.UPDATABLE) + enableInstallButton(DataStatus.UPDATABLE) text = if (mainActivityViewModel.checkUnsupportedApplication(homeApp)) { context.getString(R.string.not_available) } else { @@ -237,7 +241,7 @@ class HomeChildRVAdapter( homeApp: ApplicationDomain ) { installButton.apply { - enableInstallButton(Status.INSTALLED) + enableInstallButton(DataStatus.INSTALLED) text = context.getString(R.string.open) setOnClickListener { if (homeApp.isPwa) { -- GitLab From 27202c75cbeb7cd1b517e0e1667ff4b803b44a4a Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 11 Mar 2026 10:03:47 +0100 Subject: [PATCH 10/14] add ai-reviewer --- .gitlab-ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 21075797c..dcc3910aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,11 @@ stages: - build - publish +include: + - project: "e/os/ai-review" + ref: main + file: ".gitlab-ci.yml" + include: - project: "e/templates" ref: main -- GitLab From 6e3d338c5f9c2f5e63bfeb04dc43ea4ee55b2571 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 11 Mar 2026 10:42:42 +0100 Subject: [PATCH 11/14] fix pipeline --- .../main/java/foundation/e/apps/ui/MainActivity.kt | 4 ++-- .../e/apps/ui/home/model/HomeChildRVAdapter.kt | 4 ++-- .../mapper/ApplicationDomainMapperTest.kt | 12 ++++++------ .../java/foundation/e/apps/home/HomeViewModelTest.kt | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt index 79683013e..6102b1d82 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -47,14 +47,14 @@ import foundation.e.apps.BuildConfig import foundation.e.apps.R import foundation.e.apps.contract.ParentalControlContract.COLUMN_LOGIN_TYPE import foundation.e.apps.data.Constants +import foundation.e.apps.data.enums.User import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.event.EventBus +import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.updates.UpdatesNotifier import foundation.e.apps.data.login.core.StoreType import foundation.e.apps.data.system.ParentalControlAuthenticator import foundation.e.apps.databinding.ActivityMainBinding -import foundation.e.apps.data.enums.User -import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.error.AppUnavailableDialogDirections import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections diff --git a/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt index dca17a759..9a7e9bea9 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt @@ -32,17 +32,17 @@ import com.facebook.shimmer.ShimmerDrawable import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import foundation.e.apps.R -import foundation.e.apps.data.enums.Status as DataStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.state.LoginState import foundation.e.apps.databinding.HomeChildListItemBinding import foundation.e.apps.domain.application.ApplicationDomain -import foundation.e.apps.domain.enums.Status as DomainStatus import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.home.HomeFragmentDirections import foundation.e.apps.ui.utils.disableInstallButton import foundation.e.apps.ui.utils.enableInstallButton +import foundation.e.apps.data.enums.Status as DataStatus +import foundation.e.apps.domain.enums.Status as DomainStatus class HomeChildRVAdapter( private val appInfoFetchViewModel: AppInfoFetchViewModel, diff --git a/app/src/test/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapperTest.kt b/app/src/test/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapperTest.kt index ec0c55990..d42aaeed1 100644 --- a/app/src/test/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapperTest.kt +++ b/app/src/test/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapperTest.kt @@ -18,9 +18,9 @@ package foundation.e.apps.data.application.mapper import com.google.common.truth.Truth.assertThat -import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.enums.Status import foundation.e.apps.domain.application.ApplicationDomain +import foundation.e.apps.domain.enums.Source as DomainSource +import foundation.e.apps.domain.enums.Status as DomainStatus import org.junit.Test class ApplicationDomainMapperTest { @@ -31,8 +31,8 @@ class ApplicationDomainMapperTest { id = "app-1", packageName = "pkg.one", name = "App One", - source = Source.PLAY_STORE, - status = Status.INSTALLED, + source = DomainSource.PLAY_STORE, + status = DomainStatus.INSTALLED, isPwa = true, iconUrl = "https://example.com/icon.png", perms = listOf("CAMERA"), @@ -48,8 +48,8 @@ class ApplicationDomainMapperTest { assertThat(application._id).isEqualTo("app-1") assertThat(application.package_name).isEqualTo("pkg.one") assertThat(application.name).isEqualTo("App One") - assertThat(application.source).isEqualTo(Source.PLAY_STORE) - assertThat(application.status).isEqualTo(Status.INSTALLED) + assertThat(application.source).isEqualTo(foundation.e.apps.data.enums.Source.PLAY_STORE) + assertThat(application.status).isEqualTo(foundation.e.apps.data.enums.Status.INSTALLED) assertThat(application.is_pwa).isTrue() assertThat(application.icon_url).isEqualTo("https://example.com/icon.png") assertThat(application.perms).containsExactly("CAMERA") diff --git a/app/src/test/java/foundation/e/apps/home/HomeViewModelTest.kt b/app/src/test/java/foundation/e/apps/home/HomeViewModelTest.kt index b3e87a7d4..44af94f45 100644 --- a/app/src/test/java/foundation/e/apps/home/HomeViewModelTest.kt +++ b/app/src/test/java/foundation/e/apps/home/HomeViewModelTest.kt @@ -19,9 +19,9 @@ package foundation.e.apps.home import foundation.e.apps.data.Stores -import foundation.e.apps.data.enums.Status import foundation.e.apps.domain.home.FetchHomeScreenDataUseCase import foundation.e.apps.domain.application.ApplicationDomain +import foundation.e.apps.domain.enums.Status import foundation.e.apps.domain.home.HomeSection import foundation.e.apps.ui.home.HomeViewModel import org.junit.Assert.assertFalse -- GitLab From bca8916ee977afb3a62022938aea832d0aa3cec0 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 11 Mar 2026 11:16:19 +0100 Subject: [PATCH 12/14] add .post stage explicitely --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dcc3910aa..f5db062c1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,7 @@ stages: - auto-merge-main - build - publish + - .post include: - project: "e/os/ai-review" -- GitLab From 9bb9bb1f5e965f6234e3417d8d0d5b7c789597b6 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 11 Mar 2026 11:21:26 +0100 Subject: [PATCH 13/14] specify ai-review stage --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f5db062c1..6c5e71755 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,7 +13,7 @@ stages: - auto-merge-main - build - publish - - .post + - ai-review include: - project: "e/os/ai-review" -- GitLab From 96a7d26f3bdf7257fe265ba3d8ffbb7556a3424c Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 11 Mar 2026 11:27:24 +0100 Subject: [PATCH 14/14] fix pipeline --- .gitlab-ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6c5e71755..6124af705 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,19 +11,17 @@ variables: stages: - auto-merge-main + - ai-review - build - publish - - ai-review - -include: - - project: "e/os/ai-review" - ref: main - file: ".gitlab-ci.yml" include: - project: "e/templates" ref: main file: "/.gitlab/gitlab-ci/gitlab-ci-auto-merge-main.yml" + - project: "e/os/ai-review" + ref: main + file: ".gitlab-ci.yml" auto_merge_main: extends: .auto-merge-main -- GitLab