diff --git a/.gitignore b/.gitignore index 10cfdbfaf8ed53e905f414c7ae3c4c4050d83105..ea5746adee419a3e52c26e69eaa8fde5f1bfd092 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ .externalNativeBuild .cxx local.properties +data/build +domain/build +ui/build diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 21075797c8c8f766ef105904ef6ff69f32036d90..6124af70536ed777c347b051c7a7a8c9f8471dd7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,6 +11,7 @@ variables: stages: - auto-merge-main + - ai-review - build - publish @@ -18,6 +19,9 @@ 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 diff --git a/app/build.gradle b/app/build.gradle index 8d65015a3d0989230af383917d476f3be3426f3f..4f29f3484ef1158bc1e6ecc1848255a4e89ea7ea 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,9 @@ dependencies { // Project dependencies implementation(project(":auth-data-lib")) implementation(project(":parental-control-data")) + implementation(project(":data")) + implementation(project(":domain")) + implementation(project(":ui")) // eFoundation libraries implementation(libs.telemetry) @@ -253,6 +283,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/app/src/main/java/foundation/e/apps/data/Stores.kt b/app/src/main/java/foundation/e/apps/data/Stores.kt index ca076e86cf386adb2a1ad317712434ecdd56656b..c0c082b542957f41dc4ee1dc2f80649387927f23 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/application/HomeRepositoryAdapter.kt b/app/src/main/java/foundation/e/apps/data/application/HomeRepositoryAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..14b62f52b7cc4948f0699c83051dfd334fd40ac1 --- /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/application/mapper/ApplicationDomainMapper.kt b/app/src/main/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapper.kt index 22ac25a743e4a402e6a4a0dfbfe6ebcc67d9b0eb..c1db36da22a92b538cd107433dad941c1b3108c0 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/data/di/bindings/DomainBindingsModule.kt b/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0d2bf58cf555999c25d62ea64ad207b33ea6a89 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/di/bindings/DomainBindingsModule.kt @@ -0,0 +1,30 @@ +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 foundation.e.apps.data.application.HomeRepositoryAdapter +import foundation.e.apps.data.preference.AppLoungePreference +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface DomainBindingsModule { + @Binds + @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( + 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 eb4164dda593907b2d98ed647696c0405a62b61b..b161230c117517bc0f3b52e3111142f3d0425a48 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/enums/Source.kt b/app/src/main/java/foundation/e/apps/data/enums/Source.kt index fb1d51a1b6fe85fe16f0e22d67e81b0ec3365bba..469ed27dc849a610738277dc828842157e4ab077 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/data/playstore/search/PlayStoreSuggestionSource.kt b/app/src/main/java/foundation/e/apps/data/playstore/search/PlayStoreSuggestionSource.kt index fbd2f6e697d8f3ce5c0c94943cd391159febfed1..b4695e65850fc7bed1edc6a55efc0be98ffa2962 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 98a50ef647b1235851bf0337e06840451ff4c3aa..c4c25cf6e21202e1c554f28c7ed0a33edfb870c8 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/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index 8d41a6a959f7e87208500ba814a4f4d83717e946..1daf284ec1eaf2cad3803702246447a66dd64fe8 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/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index 3148ea144ed4480d01108090957690b60559551c..655fe8858c7fcecb4140ff99cccbb0c1562e2e48 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 786ea776041e13bdd0e8788d10db0b4c26275230..b69beaf02ba199716a683d8cf9e92d3cfbd901cf 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 596a463d0423d900bbfb4fb76e2873d1661386e4..85b71b133eaee12f4ba21d1ea8239ffd186d02bb 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 c20c6cc1a4c458392044afe780a770def115c650..8f1b13d48bda6272d1c3ce0821cb1509e3bea6c8 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 f4c304efba18488d17853bb755c021834c42cc5f..c735fe3c4181403dc9614af7f98e55bad4578e94 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 0000000000000000000000000000000000000000..f0a9785a722850a1effa6cfbf67d2e37c737d214 --- /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/main/java/foundation/e/apps/ui/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt index 33acdd009431521f4571239daa85304b1094f0e6..f37ea04e46f3ee3c9fc3a516f04b1eaf6f17e388 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 @@ -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 025a3026083a03877e95999f1d134da4b8ae3cc9..b6efbc08d0b572a9c2d28c2138716213f99a004e 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/ui/home/model/HomeChildRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt index 3c527119f75c82904dbaeb9f1f5b192b145956db..9a7e9bea94291701664ec855ed9277ee96182d72 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,7 +32,6 @@ 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.User import foundation.e.apps.data.login.state.LoginState import foundation.e.apps.databinding.HomeChildListItemBinding @@ -42,6 +41,8 @@ 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, @@ -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) { 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 ec0c55990be67b7749cb14c9be516ee310a22e92..d42aaeed1e734a8cb78c9a63112ffa393cb701a4 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/data/enums/SourceTest.kt b/app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt deleted file mode 100644 index 33156a79e68e6b6132ea09dcedcf2699d33cb427..0000000000000000000000000000000000000000 --- 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/data/search/FakeSuggestionSource.kt b/app/src/test/java/foundation/e/apps/data/search/FakeSuggestionSource.kt index 911ce37014fdc87d25c18bb251e2def6a88a60b5..30284499593d8b121e4ab0774b61be444d4dbf02 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/test/java/foundation/e/apps/home/HomeViewModelTest.kt b/app/src/test/java/foundation/e/apps/home/HomeViewModelTest.kt index b3e87a7d40ffbf9595df3e42f2f1157fa84ffd3d..44af94f4599662a5805491ebc285c56f0b086d4f 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 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 0000000000000000000000000000000000000000..fba0cca9e4c9c0f5bcdd80482ed602e968fcfb51 --- /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/data/build.gradle b/data/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..ef3636b37ecc5339874442cdcfd63ff792e47bb6 --- /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 0000000000000000000000000000000000000000..43a38762d748e00489fdf9b4f306539c8b8516ed --- /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 0000000000000000000000000000000000000000..e97bf40a97cf2cc91ce70e5581f1f979b919d5f1 --- /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 0000000000000000000000000000000000000000..444de3a245f56e232c720e794e9def10c5343805 --- /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/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 0000000000000000000000000000000000000000..6c9d394a17b8e51de839894e1449eb6547825ab3 --- /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/Stores.kt b/domain/src/main/java/foundation/e/apps/domain/Stores.kt new file mode 100644 index 0000000000000000000000000000000000000000..6b867524e51246b76ea818359def4f1ca196ade7 --- /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 +} diff --git a/app/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt b/domain/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt similarity index 84% rename from app/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt rename to domain/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt index 0bea64fb45eb53ffcba674e7439c3f3cce0b4c3c..689f5295dade0ef2bf3d6bfeaed095535e60bba2 100644 --- a/app/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt +++ b/domain/src/main/java/foundation/e/apps/domain/application/ApplicationDomain.kt @@ -18,13 +18,13 @@ 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 +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(), @@ -62,7 +62,7 @@ data class ApplicationDomain( val numberOfPermission: Int = 0, val numberOfTracker: Int = 0, val contentRating: ContentRating = ContentRating(), - val restriction: Restriction = Restriction.NOT_RESTRICTED, + val restriction: AppRestriction = AppRestriction.NOT_RESTRICTED, val filterLevel: FilterLevel = FilterLevel.UNKNOWN, val isGplayReplaced: Boolean = false, val isFDroidApp: Boolean = false, 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 0000000000000000000000000000000000000000..81a6cf0078be721139b0f1716ae3d8292ceeb493 --- /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 0000000000000000000000000000000000000000..8b71ebd9d422ec55f5649dcb5245d98ae5561ad2 --- /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/application/model/AppRestriction.kt b/domain/src/main/java/foundation/e/apps/domain/application/model/AppRestriction.kt new file mode 100644 index 0000000000000000000000000000000000000000..82f4b6b5051fbfa6ea2a7aaca58caafeb0080155 --- /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 0000000000000000000000000000000000000000..0c1402ba5f5cf2903c7b228c93a99301de0a6884 --- /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 0000000000000000000000000000000000000000..5e0a36796ae8146af5cd7bda4960baceadf23666 --- /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 0000000000000000000000000000000000000000..8b64487649b30496662fcddd2629a13e51b34a9d --- /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 0000000000000000000000000000000000000000..a2d38041cbbca166ab999aad292e08326637ba30 --- /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 0000000000000000000000000000000000000000..ee3ccfa324327626b394f9c4c65416b60e84c197 --- /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 +) 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 0000000000000000000000000000000000000000..1d80a7cdb4766cc0d6517b4b29dcedd86092dda6 --- /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 0000000000000000000000000000000000000000..a4fcbca0494364aea9937a7465de864484d8c1f5 --- /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 0000000000000000000000000000000000000000..01a37f35879743a895cbfe5d1b002b0e38b1255f --- /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 0000000000000000000000000000000000000000..30a8fa966cb24c7d328a861a88bd94e0ec0af095 --- /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 0000000000000000000000000000000000000000..63aad50377ad962d4a0144540eaf446c3fbaa6a3 --- /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 0000000000000000000000000000000000000000..37abd2a512981ced171c35027c7d8c5ca1998917 --- /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 0000000000000000000000000000000000000000..07c864c80484385e091da06085322fedfd9023ea --- /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 0000000000000000000000000000000000000000..990e43ddd254d99572d07f591dc4277848775079 --- /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/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 16924f26dbf2fd4dbe39394d52f36cbb3ead46e2..500c1bab18c184da7544c8ea24ba6841563512f9 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/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 0000000000000000000000000000000000000000..c3ed1b9bc72688af76218f51bbaaf3b8b714db0d --- /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>> +} 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/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 49ed7ff372459bc81662ba0a99ca0a6ce73f5206..84f634bb6c46704326f1e13fca9515bd368c8e3b 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 d798460a7fd11484c3dab3c75faa9d8fede6cfd2..f2948fbd023da4524cd985e252fd2e0d67dc8dbb 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 0000000000000000000000000000000000000000..cf9fa89e1516707d2a191010bbf33fcedb0e297e --- /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 0615e84e00a24e162519dd36f917424c95fecf4c..19497ac2c1401d109ad65d88d8307d248fd2beda 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 81b13870b560bcad0da7cfe55a457b76498a1ed1..792be8a2d5cf6ef96f65556f9e479a895b1f2f0d 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 fe0c307381fd1726e784e3e3f00ad8a5f9a7de35..0924c51eab616462d667456a46b5f85b852d2458 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/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 0000000000000000000000000000000000000000..0725b0b5c68ddee9e111e006e702515af8b9075f --- /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 0000000000000000000000000000000000000000..0d22ae30a785af9c221a8904b0846c872c240165 --- /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 0000000000000000000000000000000000000000..ac7c0124aef556e36f40df6ab08d030c6651819e --- /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() + } +} 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 ae1cb31e37f27fcd25da96b49397a689d38a30ea..7d4c5325e76ca08fd99373d1ba03c32d0e5bc877 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 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 c5c2d1cea06bac65ebbe65e5bd497461fb8bd535..6d8f78629aceaabf7cf9cac95d0ef4c8cb9e8446 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 f8bba4590762f2473924ffbc3d09a8ed221022e1..8f1248c9cf9b69bd76e88907ac315d510b553190 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec1b556bcb0966e2ea48c2e5f08f6718f767a079..0751a471f4241d9ba368d7bd416bfc0145a5519e 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 d4ebad2334a502f5275289938cb094bbf27a61e8..0de6a9b7db0714f75fac1067ae907e019a9ee226 100644 --- a/lint.xml +++ b/lint.xml @@ -51,10 +51,10 @@ + - diff --git a/settings.gradle b/settings.gradle index 0b35903798457ed980886edba821edacc925afc5..07796292cb2c2fea516a982f5f5c9899790bfc4a 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 0000000000000000000000000000000000000000..17e41fd8545c0cda836f2361dbf76f2b434e1a0f --- /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 0000000000000000000000000000000000000000..8072ee00dbf16d9161b7464ef3d2194a7d659bcc --- /dev/null +++ b/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + +