From fbfc0931a08a06101abd4e3e2bf8642d94250c94 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 24 May 2024 14:18:39 +0600 Subject: [PATCH 01/64] initial UI implementation --- .../e/apps/data/ParentalControlRepository.kt | 4 ++++ .../e/apps/ui/application/ApplicationFragment.kt | 8 ++++++++ app/src/main/res/drawable/visibility_off.png | Bin 0 -> 313 bytes .../res/layout/fragment_application_title.xml | 10 +++++++++- app/src/main/res/values/strings.xml | 3 +++ 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt create mode 100644 app/src/main/res/drawable/visibility_off.png diff --git a/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt b/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt new file mode 100644 index 000000000..53b1f26e5 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt @@ -0,0 +1,4 @@ +package foundation.e.apps.data + +class ParentalControlRepository { +} \ No newline at end of file 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 d71d6def5..02c92bd58 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 @@ -233,6 +233,14 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { stopLoadingUI() collectState() + + binding.titleInclude.nsfwMessage.setOnClickListener { + ApplicationDialogFragment( + title = getString(R.string.nsfw_dialog_title), + message = getString(R.string.nsfw_dialog_message), + drawableResId = R.drawable.visibility_off + ).show(childFragmentManager, TAG) + } } private fun collectState() { diff --git a/app/src/main/res/drawable/visibility_off.png b/app/src/main/res/drawable/visibility_off.png new file mode 100644 index 0000000000000000000000000000000000000000..6a9e078f666c11fe1e9153ac1a805b228a009cae GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{y9$<$B+ufwZV*nhYWal_+B-5IUEXbQ1v+^ z!^8d{q11r$Fr&qdg$$g^PwghO^*+|9|9-vp(x0U-y}r0xXgMU@n;|Xgec!a`s<~R= z%!+3x=7>zrn>f3*y>W)c8pl%Orn+?&s^_@pdQ3b%&s><(YUx(bOUy;*UL0>wJH@V% zzHKf?)4DXJUfB>86Iiwi|s7Ea%vS@7yESakjG z=>=7yo9(67+DWLcTe$v=aNoA%yqV&^1O@MkE`0GYbqBLqkgKXr%Q7jT&lo&i{an^L HB{Ts5J6Lp_ literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/fragment_application_title.xml b/app/src/main/res/layout/fragment_application_title.xml index 96ec06c1f..1d4321be4 100644 --- a/app/src/main/res/layout/fragment_application_title.xml +++ b/app/src/main/res/layout/fragment_application_title.xml @@ -74,7 +74,15 @@ - + Having troubles? https://doc.e.foundation/support-topics/app_lounge_troubleshooting Share + This app may contain inappropriate content. Update All @@ -223,4 +224,6 @@ Split Install channel Sign in Ignore + Content Warning + The app may contain nudity, profanity, slurs, violence, intense sexuality, political incorrectness, or other potentially disturbing subject matter. This is especially relevant in environments like workplaces, schools, religious and family settings. \ No newline at end of file -- GitLab From 278ddf85e87d6806f703380d7b2fbf2f57c1c9f3 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 31 May 2024 09:58:57 +0600 Subject: [PATCH 02/64] setup dev environment --- app/src/main/java/foundation/e/apps/data/NetworkHandler.kt | 1 + .../foundation/e/apps/data/application/data/Application.kt | 4 +++- .../java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt | 4 ++-- .../java/foundation/e/apps/data/cleanapk/RetrofitModule.kt | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index f5c5cdd50..96422cf38 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -66,6 +66,7 @@ private fun resultSupremeGplayHttpRequestException(e: GplayHttpRequestExcept private fun handleOthersException(e: Exception): ResultSupreme.Error { val message = extractErrorMessage(e) + Timber.d("message: $message exceptoin: $e") return ResultSupreme.Error(message, e) } diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index dd32f1a5d..a69ddfb43 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -102,7 +102,9 @@ data class Application( var isGplayReplaced: Boolean = false, @SerializedName(value = "on_fdroid") val isFDroidApp: Boolean = false, - val contentRating: ContentRating = ContentRating() + val contentRating: ContentRating = ContentRating(), + @SerializedName(value = "antifeature") + val antiFeatures: Map> = emptyMap(), ) { fun updateType() { this.type = if (this.is_pwa) PWA else NATIVE diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt index 3da3d4014..b7c502fac 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt @@ -31,8 +31,8 @@ interface CleanApkRetrofit { companion object { // API endpoints - const val BASE_URL = "https://api.cleanapk.org/v2/" - const val ASSET_URL = "https://api.cleanapk.org/v2/media/" + const val BASE_URL = "https://api.dev.cleanapk.org/v2/" + const val ASSET_URL = "https://api.dev.cleanapk.org/v2/media/" // Application sources const val APP_SOURCE_FOSS = "open" diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt index 8744d01df..9400017ec 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt @@ -165,12 +165,15 @@ object RetrofitModule { fun provideInterceptor(): Interceptor { return Interceptor { chain -> val builder = chain.request().newBuilder() + Timber.d("Request: URL: ${chain.request().url}") builder.header( "User-Agent", "Dalvik/2.1.0 (Linux; U; Android ${Build.VERSION.RELEASE};)" ).header("Accept-Language", Locale.getDefault().language) - return@Interceptor chain.proceed(builder.build()) + val response = chain.proceed(builder.build()) + Timber.d("Response: Code: ${response.code} message: ${response.message}") + return@Interceptor response } } -- GitLab From f8bb4725d637670f47089d2e095e8e72fa11f75a Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 4 Jun 2024 21:06:33 +0600 Subject: [PATCH 03/64] refactor: implement UI logic to show/hide nsfw rating Improved app text color and icon color in dark mode in app details screen. --- .../e/apps/data/ParentalControlRepository.kt | 4 - .../apps/data/application/data/Application.kt | 16 +++- .../ui/application/ApplicationFragment.kt | 41 ++++++++-- .../main/res/drawable/ic_visibility_off.xml | 27 ++++++ app/src/main/res/drawable/visibility_off.png | Bin 313 -> 0 bytes .../res/layout/fragment_application_title.xml | 77 ++++++++++++++---- app/src/main/res/values-night/colors.xml | 8 +- app/src/main/res/values/colors.xml | 8 +- app/src/main/res/values/strings.xml | 1 + 9 files changed, 150 insertions(+), 32 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt create mode 100644 app/src/main/res/drawable/ic_visibility_off.xml delete mode 100644 app/src/main/res/drawable/visibility_off.png diff --git a/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt b/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt deleted file mode 100644 index 53b1f26e5..000000000 --- a/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt +++ /dev/null @@ -1,4 +0,0 @@ -package foundation.e.apps.data - -class ParentalControlRepository { -} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index a69ddfb43..1b7abe31b 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -104,7 +104,7 @@ data class Application( val isFDroidApp: Boolean = false, val contentRating: ContentRating = ContentRating(), @SerializedName(value = "antifeature") - val antiFeatures: Map> = emptyMap(), + val antiFeatures: AntiFeatures? = null, // FIXME: Update the model to match backend response structure ) { fun updateType() { this.type = if (this.is_pwa) PWA else NATIVE @@ -119,6 +119,20 @@ data class Application( } } +data class AntiFeatures( + @SerializedName("NSFW") + val nsfw: String?, + + @SerializedName("NonFreeAssets") + val nonFreeAssets: String?, + + @SerializedName("NonFreeNet") + val nonFreeNet: String?, + + @SerializedName("Tracking") + val tracking: String? +) + val Application.shareUri: Uri get() = when (type) { PWA -> Uri.parse(url) 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 a0a2e593e..624170711 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 @@ -234,13 +234,6 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { collectState() - binding.titleInclude.nsfwMessage.setOnClickListener { - ApplicationDialogFragment( - title = getString(R.string.nsfw_dialog_title), - message = getString(R.string.nsfw_dialog_message), - drawableResId = R.drawable.visibility_off - ).show(childFragmentManager, TAG) - } } private fun collectState() { @@ -440,6 +433,35 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { appIcon.load(it.icon_image_path) } } + + updateAntiFeaturesUi(it) + } + + private fun updateAntiFeaturesUi(app: Application) { + val isNsfwApp = + app.antiFeatures?.nsfw != null // nsfw can have empty value, so check only for null + + Timber.tag("Anti-features").i("${app.name} has anti-features?: $isNsfwApp") + + if (isNsfwApp) { + with(binding.titleInclude) { + antiFeature.apply { + isVisible = true + text = getString(R.string.nsfw) + } + + antiFeatureInfoLayout.apply { + isVisible = true + setOnClickListener { + ApplicationDialogFragment( + title = getString(R.string.nsfw_dialog_title), + message = getString(R.string.nsfw_dialog_message), + drawableResId = R.drawable.ic_visibility_off + ).show(childFragmentManager, TAG) + } + } + } + } } private fun updateCategoryTitle(app: Application) { @@ -451,11 +473,14 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { catText == "web_games" -> catText = getString(R.string.games) // PWA games } - catText = catText.replace("_", " ") + catText = formatCategoryText(catText) categoryTitle.text = catText } } + private fun formatCategoryText(catText: String) = catText.replace("_", " ") + .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + private fun setupScreenshotRVAdapter() { screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(origin) binding.recyclerView.apply { diff --git a/app/src/main/res/drawable/ic_visibility_off.xml b/app/src/main/res/drawable/ic_visibility_off.xml new file mode 100644 index 000000000..9951bd0cd --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_off.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/drawable/visibility_off.png b/app/src/main/res/drawable/visibility_off.png deleted file mode 100644 index 6a9e078f666c11fe1e9153ac1a805b228a009cae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{y9$<$B+ufwZV*nhYWal_+B-5IUEXbQ1v+^ z!^8d{q11r$Fr&qdg$$g^PwghO^*+|9|9-vp(x0U-y}r0xXgMU@n;|Xgec!a`s<~R= z%!+3x=7>zrn>f3*y>W)c8pl%Orn+?&s^_@pdQ3b%&s><(YUx(bOUy;*UL0>wJH@V% zzHKf?)4DXJUfB>86Iiwi|s7Ea%vS@7yESakjG z=>=7yo9(67+DWLcTe$v=aNoA%yqV&^1O@MkE`0GYbqBLqkgKXr%Q7jT&lo&i{an^L HB{Ts5J6Lp_ diff --git a/app/src/main/res/layout/fragment_application_title.xml b/app/src/main/res/layout/fragment_application_title.xml index 1d4321be4..30d3dbb73 100644 --- a/app/src/main/res/layout/fragment_application_title.xml +++ b/app/src/main/res/layout/fragment_application_title.xml @@ -41,12 +41,13 @@ + android:scaleType="fitXY" + tools:src="@tools:sample/avatars" /> + android:textSize="22sp" + tools:text="@tools:sample/lorem" /> + android:textSize="16sp" + tools:text="@tools:sample/lorem" /> - + android:layout_marginHorizontal="16dp" + android:gravity="center_vertical" + android:orientation="horizontal" + android:visibility="gone" + tools:ignore="UseCompoundDrawables" + tools:visibility="visible"> + + + + + + + + + + + tools:text="Open Source" + tools:visibility="visible" /> + tools:text="Racing" + tools:visibility="visible" /> diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index f708dbec7..436fa01cd 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,7 +1,6 @@ @@ -26,4 +26,8 @@ @color/colorNavBar + + + #99FFFFFF + #99FFFFFF \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d1fae8b16..95e85d6e8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,7 +1,6 @@ @@ -40,4 +40,8 @@ #EDEDED + + #99000000 + #000000 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14bcd87ed..eb86bf36b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,6 +224,7 @@ Split Install channel Sign in Ignore + NSFW Content Warning The app may contain nudity, profanity, slurs, violence, intense sexuality, political incorrectness, or other potentially disturbing subject matter. This is especially relevant in environments like workplaces, schools, religious and family settings. \ No newline at end of file -- GitLab From cb366aca61b8d3389db573e9911cc94bf99d9cba Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 5 Jun 2024 12:43:24 +0600 Subject: [PATCH 04/64] refactor: update Application data class to map anti-features response Removed anti-features label from title panel as per the discussion in Figma. --- .../apps/data/application/data/Application.kt | 18 ++------------- .../ui/application/ApplicationFragment.kt | 12 +++------- .../res/layout/fragment_application_title.xml | 23 ++----------------- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 8 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index 1b7abe31b..3ffc54419 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -103,8 +103,8 @@ data class Application( @SerializedName(value = "on_fdroid") val isFDroidApp: Boolean = false, val contentRating: ContentRating = ContentRating(), - @SerializedName(value = "antifeature") - val antiFeatures: AntiFeatures? = null, // FIXME: Update the model to match backend response structure + @SerializedName(value = "antifeatures") + val antiFeatures: List> = emptyList() ) { fun updateType() { this.type = if (this.is_pwa) PWA else NATIVE @@ -119,20 +119,6 @@ data class Application( } } -data class AntiFeatures( - @SerializedName("NSFW") - val nsfw: String?, - - @SerializedName("NonFreeAssets") - val nonFreeAssets: String?, - - @SerializedName("NonFreeNet") - val nonFreeNet: String?, - - @SerializedName("Tracking") - val tracking: String? -) - val Application.shareUri: Uri get() = when (type) { PWA -> Uri.parse(url) 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 624170711..669452018 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 @@ -147,6 +147,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private const val PRIVACY_GUIDELINE_URL = "https://doc.e.foundation/privacy_score" private const val REQUEST_EXODUS_REPORT_URL = "https://reports.exodus-privacy.eu.org/en/analysis/submit#" + private const val KEY_ANTI_FEATURES_NSFW = "NSFW" } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -284,7 +285,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } appTrackers.setOnClickListener { val fusedApp = applicationViewModel.getFusedApp() - var trackers = + val trackers = buildTrackersString(fusedApp) ApplicationDialogFragment( @@ -439,17 +440,10 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun updateAntiFeaturesUi(app: Application) { val isNsfwApp = - app.antiFeatures?.nsfw != null // nsfw can have empty value, so check only for null - - Timber.tag("Anti-features").i("${app.name} has anti-features?: $isNsfwApp") + app.antiFeatures.find { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } != null if (isNsfwApp) { with(binding.titleInclude) { - antiFeature.apply { - isVisible = true - text = getString(R.string.nsfw) - } - antiFeatureInfoLayout.apply { isVisible = true setOnClickListener { diff --git a/app/src/main/res/layout/fragment_application_title.xml b/app/src/main/res/layout/fragment_application_title.xml index 30d3dbb73..bb6c0ce04 100644 --- a/app/src/main/res/layout/fragment_application_title.xml +++ b/app/src/main/res/layout/fragment_application_title.xml @@ -119,25 +119,7 @@ android:background="@color/colorBackground" android:backgroundTint="@color/colorBackground"> - - - - - - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb86bf36b..cda343445 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,7 +224,7 @@ Split Install channel Sign in Ignore - NSFW + Content Warning The app may contain nudity, profanity, slurs, violence, intense sexuality, political incorrectness, or other potentially disturbing subject matter. This is especially relevant in environments like workplaces, schools, religious and family settings. \ No newline at end of file -- GitLab From 13b79f0631a74ab48cee05b111fc4ecb1b5140f7 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 5 Jun 2024 13:33:49 +0600 Subject: [PATCH 05/64] refactor: improve network response logging Added HttpLoggingInterceptor to log network requests and responses instead of using Timber. Minor adjustments in colors and strings. --- app/build.gradle | 1 + .../foundation/e/apps/data/NetworkHandler.kt | 5 +- .../e/apps/data/cleanapk/CleanApkRetrofit.kt | 12 +++-- .../e/apps/data/cleanapk/RetrofitModule.kt | 50 ++++++++++++------- app/src/main/res/values-night/colors.xml | 2 +- app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 7 files changed, 47 insertions(+), 27 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c324b3683..6691da931 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -206,6 +206,7 @@ dependencies { implementation "com.squareup.moshi:moshi-kotlin:1.13.0" // implementation "com.squareup.moshi:moshi-adapters:1.5.0" implementation "com.squareup.okhttp3:okhttp:4.9.2" + implementation "com.squareup.okhttp3:logging-interceptor:4.9.2" // JSON Converter implementation 'com.squareup.retrofit2:converter-gson:2.5.0' diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index 96422cf38..fab3d0367 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -1,6 +1,5 @@ /* - * Copyright MURENA SAS 2023 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 @@ -14,6 +13,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.data @@ -66,7 +66,6 @@ private fun resultSupremeGplayHttpRequestException(e: GplayHttpRequestExcept private fun handleOthersException(e: Exception): ResultSupreme.Error { val message = extractErrorMessage(e) - Timber.d("message: $message exceptoin: $e") return ResultSupreme.Error(message, e) } diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt index b7c502fac..7c0ec7dde 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt @@ -1,6 +1,5 @@ /* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION + * 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 @@ -14,6 +13,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.data.cleanapk @@ -31,8 +31,12 @@ interface CleanApkRetrofit { companion object { // API endpoints - const val BASE_URL = "https://api.dev.cleanapk.org/v2/" - const val ASSET_URL = "https://api.dev.cleanapk.org/v2/media/" + const val BASE_URL = "https://api.cleanapk.org/v2/" + const val ASSET_URL = "https://api.cleanapk.org/v2/media/" + + // TODO: Configure dev and prod flavors to auto switch URLs + // const val BASE_URL_DEV = "https://api.dev.cleanapk.org/v2/" + // const val ASSET_URL_DEV = "https://api.dev.cleanapk.org/v2/media/" // Application sources const val APP_SOURCE_FOSS = "open" diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt index 9400017ec..688092c3e 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt @@ -1,6 +1,5 @@ /* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION + * 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 @@ -14,6 +13,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.data.cleanapk @@ -29,33 +29,32 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import foundation.e.apps.BuildConfig import foundation.e.apps.data.cleanapk.data.app.Application import foundation.e.apps.data.ecloud.EcloudApiInterface import foundation.e.apps.data.exodus.ExodusTrackerApi import foundation.e.apps.data.fdroid.FdroidApiInterface import okhttp3.Cache import okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient -import okhttp3.Protocol -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory -import timber.log.Timber -import java.net.ConnectException import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Named import javax.inject.Singleton + @Module @InstallIn(SingletonComponent::class) object RetrofitModule { private const val HTTP_TIMEOUT_IN_SECOND = 10L + private const val HEADER_USER_AGENT = "User-Agent" + private const val HEADER_ACCEPT_LANGUAGE = "Accept-Language" /** * Provides an instance of Retrofit to work with CleanAPK API @@ -63,7 +62,7 @@ object RetrofitModule { */ @Singleton @Provides - fun provideCleanAPKInterface(okHttpClient: OkHttpClient, moshi: Moshi): CleanApkRetrofit { + fun provideCleanApkInterface(okHttpClient: OkHttpClient, moshi: Moshi): CleanApkRetrofit { return Retrofit.Builder() .baseUrl(CleanApkRetrofit.BASE_URL) .client(okHttpClient) @@ -78,7 +77,7 @@ object RetrofitModule { */ @Singleton @Provides - fun provideCleanAPKDetailApi( + fun provideCleanApkDetailApi( okHttpClient: OkHttpClient, @Named("gsonCustomAdapter") gson: Gson ): CleanApkAppDetailsRetrofit { @@ -165,23 +164,40 @@ object RetrofitModule { fun provideInterceptor(): Interceptor { return Interceptor { chain -> val builder = chain.request().newBuilder() - Timber.d("Request: URL: ${chain.request().url}") - builder.header( - "User-Agent", - "Dalvik/2.1.0 (Linux; U; Android ${Build.VERSION.RELEASE};)" - ).header("Accept-Language", Locale.getDefault().language) + .header( + HEADER_USER_AGENT, "Dalvik/2.1.0 (Linux; U; Android ${Build.VERSION.RELEASE};)" + ) + .header(HEADER_ACCEPT_LANGUAGE, Locale.getDefault().language) val response = chain.proceed(builder.build()) - Timber.d("Response: Code: ${response.code} message: ${response.message}") + return@Interceptor response } } + @Provides + @Singleton + fun provideLoggingInterceptor(): HttpLoggingInterceptor { + val interceptor = HttpLoggingInterceptor() + + interceptor.level = when { + BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY + else -> HttpLoggingInterceptor.Level.NONE + } + + return interceptor + } + @Singleton @Provides - fun provideOkHttpClient(cache: Cache, interceptor: Interceptor): OkHttpClient { + fun provideOkHttpClient( + cache: Cache, + interceptor: Interceptor, + httpLoggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(interceptor) + .addInterceptor(httpLoggingInterceptor) // Put logging interceptor last .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) .cache(cache) .build() diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 436fa01cd..454425315 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -30,4 +30,4 @@ #99FFFFFF #99FFFFFF - \ No newline at end of file + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 95e85d6e8..4985df2e5 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -44,4 +44,4 @@ #99000000 #000000 - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cda343445..3bbaef536 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -227,4 +227,4 @@ Content Warning The app may contain nudity, profanity, slurs, violence, intense sexuality, political incorrectness, or other potentially disturbing subject matter. This is especially relevant in environments like workplaces, schools, religious and family settings. - \ No newline at end of file + -- GitLab From 7ebe73b32cc5dc6f91801eb16feebc69d33d47fa Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 5 Jun 2024 14:12:51 +0600 Subject: [PATCH 06/64] refactor: split network modules into smaller modules No more complaints from detekt about TooManyFunctions! --- .../e/apps/data/cleanapk/InterceptorModule.kt | 73 ++++ .../e/apps/data/cleanapk/NetworkModule.kt | 90 +++++ ...RetrofitModule.kt => RetrofitApiModule.kt} | 320 +++++++----------- .../e/apps/data/fdroid/FdroidApiInterface.kt | 20 +- 4 files changed, 297 insertions(+), 206 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/cleanapk/InterceptorModule.kt create mode 100644 app/src/main/java/foundation/e/apps/data/cleanapk/NetworkModule.kt rename app/src/main/java/foundation/e/apps/data/cleanapk/{RetrofitModule.kt => RetrofitApiModule.kt} (56%) diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/InterceptorModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/InterceptorModule.kt new file mode 100644 index 000000000..1dc6bfd2b --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/InterceptorModule.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 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.data.cleanapk + +import android.os.Build +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.BuildConfig +import java.util.Locale +import javax.inject.Singleton +import okhttp3.Interceptor +import okhttp3.logging.HttpLoggingInterceptor + +@Module +@InstallIn(SingletonComponent::class) +class InterceptorModule { + + companion object { + private const val HEADER_USER_AGENT = "User-Agent" + private const val HEADER_ACCEPT_LANGUAGE = "Accept-Language" + } + + @Singleton + @Provides + fun provideInterceptor(): Interceptor { + return Interceptor { chain -> + val builder = + chain + .request() + .newBuilder() + .header( + HEADER_USER_AGENT, + "Dalvik/2.1.0 (Linux; U; Android ${Build.VERSION.RELEASE};)") + .header(HEADER_ACCEPT_LANGUAGE, Locale.getDefault().language) + + val response = chain.proceed(builder.build()) + + return@Interceptor response + } + } + + @Provides + @Singleton + fun provideLoggingInterceptor(): HttpLoggingInterceptor { + val interceptor = HttpLoggingInterceptor() + + interceptor.level = + when { + BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY + else -> HttpLoggingInterceptor.Level.NONE + } + + return interceptor + } +} diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/NetworkModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/NetworkModule.kt new file mode 100644 index 000000000..7d56a43f9 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/NetworkModule.kt @@ -0,0 +1,90 @@ +/* + * 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.data.cleanapk + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.cleanapk.data.app.Application +import okhttp3.Cache +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.converter.jackson.JacksonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + private const val HTTP_TIMEOUT_IN_SECOND = 10L + + @Singleton + @Provides + fun getMoshi(): Moshi { + return Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + } + + @Singleton + @Provides + @Named("gsonCustomAdapter") + fun getGson(): Gson { + return GsonBuilder() + .registerTypeAdapter(Application::class.java, ApplicationDeserializer()) + .enableComplexMapKeySerialization() + .create() + } + + /** + * Used in [RetrofitApiModule.provideFdroidApi]. + * Reference: https://stackoverflow.com/a/69859687 + */ + @Singleton + @Provides + @Named("yamlFactory") + fun getYamlFactory(): JacksonConverterFactory { + return JacksonConverterFactory.create(ObjectMapper(YAMLFactory())) + } + + @Singleton + @Provides + fun provideOkHttpClient( + cache: Cache, + interceptor: Interceptor, + httpLoggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(interceptor) + .addInterceptor(httpLoggingInterceptor) // Put logging interceptor last + .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) + .cache(cache) + .build() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitApiModule.kt similarity index 56% rename from app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt rename to app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitApiModule.kt index 688092c3e..662ba206b 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitApiModule.kt @@ -1,205 +1,115 @@ -/* - * 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.data.cleanapk - -import android.os.Build -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import foundation.e.apps.BuildConfig -import foundation.e.apps.data.cleanapk.data.app.Application -import foundation.e.apps.data.ecloud.EcloudApiInterface -import foundation.e.apps.data.exodus.ExodusTrackerApi -import foundation.e.apps.data.fdroid.FdroidApiInterface -import okhttp3.Cache -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.converter.jackson.JacksonConverterFactory -import retrofit2.converter.moshi.MoshiConverterFactory -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Named -import javax.inject.Singleton - - -@Module -@InstallIn(SingletonComponent::class) -object RetrofitModule { - - private const val HTTP_TIMEOUT_IN_SECOND = 10L - private const val HEADER_USER_AGENT = "User-Agent" - private const val HEADER_ACCEPT_LANGUAGE = "Accept-Language" - - /** - * Provides an instance of Retrofit to work with CleanAPK API - * @return instance of [CleanApkRetrofit] - */ - @Singleton - @Provides - fun provideCleanApkInterface(okHttpClient: OkHttpClient, moshi: Moshi): CleanApkRetrofit { - return Retrofit.Builder() - .baseUrl(CleanApkRetrofit.BASE_URL) - .client(okHttpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - .create(CleanApkRetrofit::class.java) - } - - /** - * Provides an instance of Retrofit to work with CleanAPK API - * @return instance of [CleanApkAppDetailsRetrofit] - */ - @Singleton - @Provides - fun provideCleanApkDetailApi( - okHttpClient: OkHttpClient, - @Named("gsonCustomAdapter") gson: Gson - ): CleanApkAppDetailsRetrofit { - return Retrofit.Builder() - .baseUrl(CleanApkRetrofit.BASE_URL) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build() - .create(CleanApkAppDetailsRetrofit::class.java) - } - - @Singleton - @Provides - fun provideExodusApi(okHttpClient: OkHttpClient, moshi: Moshi): ExodusTrackerApi { - return Retrofit.Builder() - .baseUrl(ExodusTrackerApi.BASE_URL) - .client(okHttpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - .create(ExodusTrackerApi::class.java) - } - - /** - * The fdroid api returns results in .yaml format. - * Hence we need a yaml convertor. - * Convertor is being provided by [getYamlFactory]. - */ - @Singleton - @Provides - fun provideFdroidApi( - okHttpClient: OkHttpClient, - @Named("yamlFactory") yamlFactory: JacksonConverterFactory - ): FdroidApiInterface { - return Retrofit.Builder() - .baseUrl(FdroidApiInterface.BASE_URL) - .client(okHttpClient) - .addConverterFactory(yamlFactory) - .build() - .create(FdroidApiInterface::class.java) - } - - @Singleton - @Provides - fun provideEcloudApi(okHttpClient: OkHttpClient, moshi: Moshi): EcloudApiInterface { - return Retrofit.Builder() - .baseUrl(EcloudApiInterface.BASE_URL) - .client(okHttpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - .create(EcloudApiInterface::class.java) - } - - @Singleton - @Provides - fun getMoshi(): Moshi { - return Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - } - - @Singleton - @Provides - @Named("gsonCustomAdapter") - fun getGson(): Gson { - return GsonBuilder() - .registerTypeAdapter(Application::class.java, ApplicationDeserializer()) - .enableComplexMapKeySerialization() - .create() - } - - /** - * Used in above [provideFdroidApi]. - * Reference: https://stackoverflow.com/a/69859687 - */ - @Singleton - @Provides - @Named("yamlFactory") - fun getYamlFactory(): JacksonConverterFactory { - return JacksonConverterFactory.create(ObjectMapper(YAMLFactory())) - } - - @Singleton - @Provides - fun provideInterceptor(): Interceptor { - return Interceptor { chain -> - val builder = chain.request().newBuilder() - .header( - HEADER_USER_AGENT, "Dalvik/2.1.0 (Linux; U; Android ${Build.VERSION.RELEASE};)" - ) - .header(HEADER_ACCEPT_LANGUAGE, Locale.getDefault().language) - - val response = chain.proceed(builder.build()) - - return@Interceptor response - } - } - - @Provides - @Singleton - fun provideLoggingInterceptor(): HttpLoggingInterceptor { - val interceptor = HttpLoggingInterceptor() - - interceptor.level = when { - BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY - else -> HttpLoggingInterceptor.Level.NONE - } - - return interceptor - } - - @Singleton - @Provides - fun provideOkHttpClient( - cache: Cache, - interceptor: Interceptor, - httpLoggingInterceptor: HttpLoggingInterceptor - ): OkHttpClient { - return OkHttpClient.Builder() - .addInterceptor(interceptor) - .addInterceptor(httpLoggingInterceptor) // Put logging interceptor last - .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) - .cache(cache) - .build() - } -} +/* + * Copyright (C) 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.data.cleanapk + +import com.google.gson.Gson +import com.squareup.moshi.Moshi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.cleanapk.NetworkModule.getYamlFactory +import foundation.e.apps.data.ecloud.EcloudApiInterface +import foundation.e.apps.data.exodus.ExodusTrackerApi +import foundation.e.apps.data.fdroid.FdroidApiInterface +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.jackson.JacksonConverterFactory +import retrofit2.converter.moshi.MoshiConverterFactory +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class RetrofitApiModule { + /** + * Provides an instance of Retrofit to work with CleanAPK API + * @return instance of [CleanApkRetrofit] + */ + @Singleton + @Provides + fun provideCleanApkInterface(okHttpClient: OkHttpClient, moshi: Moshi): CleanApkRetrofit { + return Retrofit.Builder() + .baseUrl(CleanApkRetrofit.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(CleanApkRetrofit::class.java) + } + + /** + * Provides an instance of Retrofit to work with CleanAPK API + * @return instance of [CleanApkAppDetailsRetrofit] + */ + @Singleton + @Provides + fun provideCleanApkDetailApi( + okHttpClient: OkHttpClient, + @Named("gsonCustomAdapter") gson: Gson + ): CleanApkAppDetailsRetrofit { + return Retrofit.Builder() + .baseUrl(CleanApkRetrofit.BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + .create(CleanApkAppDetailsRetrofit::class.java) + } + + @Singleton + @Provides + fun provideExodusApi(okHttpClient: OkHttpClient, moshi: Moshi): ExodusTrackerApi { + return Retrofit.Builder() + .baseUrl(ExodusTrackerApi.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(ExodusTrackerApi::class.java) + } + + /** + * The fdroid api returns results in .yaml format. + * Hence we need a yaml convertor. + * Convertor is being provided by [getYamlFactory]. + */ + @Singleton + @Provides + fun provideFdroidApi( + okHttpClient: OkHttpClient, + @Named("yamlFactory") yamlFactory: JacksonConverterFactory + ): FdroidApiInterface { + return Retrofit.Builder() + .baseUrl(FdroidApiInterface.BASE_URL) + .client(okHttpClient) + .addConverterFactory(yamlFactory) + .build() + .create(FdroidApiInterface::class.java) + } + + @Singleton + @Provides + fun provideEcloudApi(okHttpClient: OkHttpClient, moshi: Moshi): EcloudApiInterface { + return Retrofit.Builder() + .baseUrl(EcloudApiInterface.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(EcloudApiInterface::class.java) + } +} diff --git a/app/src/main/java/foundation/e/apps/data/fdroid/FdroidApiInterface.kt b/app/src/main/java/foundation/e/apps/data/fdroid/FdroidApiInterface.kt index 0480873f1..1d893461b 100644 --- a/app/src/main/java/foundation/e/apps/data/fdroid/FdroidApiInterface.kt +++ b/app/src/main/java/foundation/e/apps/data/fdroid/FdroidApiInterface.kt @@ -1,3 +1,21 @@ +/* + * Copyright (C) 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.data.fdroid import foundation.e.apps.data.fdroid.models.FdroidApiModel @@ -7,7 +25,7 @@ import retrofit2.http.Path /** * Interface for retrofit calls. - * Created from [foundation.e.apps.data.cleanapk.RetrofitModule.provideFdroidApi]. + * Created from [foundation.e.apps.data.cleanapk.RetrofitApiModule.provideFdroidApi]. */ interface FdroidApiInterface { -- GitLab From 5e6a71e3a1b09c222ca9e61f4f3be6203df0a002 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 30 May 2024 14:10:34 +0530 Subject: [PATCH 07/64] contentprovider for login type --- app/src/main/AndroidManifest.xml | 6 ++ .../data/login/AuthenticatorRepository.kt | 2 +- .../e/apps/provider/AgeRatingProvider.kt | 82 +++++++++++++++++++ .../e/apps/provider/ProviderConstants.kt | 15 ++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt create mode 100644 app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e4492e32d..2fbb55770 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -139,6 +139,12 @@ android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> + + diff --git a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt index 2e4bc761d..717c9d7ac 100644 --- a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt @@ -78,7 +78,7 @@ class AuthenticatorRepository @Inject constructor( return validateAuthData } - private fun getUserType(): User { + fun getUserType(): User { return loginCommon.getUserType() } } diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt new file mode 100644 index 000000000..8d3601072 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -0,0 +1,82 @@ +package foundation.e.apps.provider + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.UriMatcher +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.provider.ProviderConstants.Companion.AGE_RATING +import foundation.e.apps.provider.ProviderConstants.Companion.AUTHORITY +import foundation.e.apps.provider.ProviderConstants.Companion.LOGIN_TYPE +import foundation.e.apps.provider.ProviderConstants.Companion.PACKAGE_NAME +import foundation.e.apps.provider.ProviderConstants.Companion.PATH_AGE_RATINGS +import foundation.e.apps.provider.ProviderConstants.Companion.PATH_LOGIN_TYPE +import javax.inject.Inject + +class AgeRatingProvider @Inject constructor( + private val authenticatorRepository: AuthenticatorRepository, +): ContentProvider() { + + private val CODE_LOGIN_TYPE = 1 + private val CODE_AGE_RATING = 2 + + private val uriMatcher by lazy { + UriMatcher(UriMatcher.NO_MATCH).apply { + addURI(AUTHORITY, PATH_LOGIN_TYPE, CODE_LOGIN_TYPE) + addURI(AUTHORITY, PATH_AGE_RATINGS, CODE_AGE_RATING) + } + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + val code = uriMatcher.match(uri) + return when (code) { + CODE_LOGIN_TYPE -> getLoginType() + else -> null + } + } + + private fun getLoginType(): Cursor { + val cursor = MatrixCursor(arrayOf(LOGIN_TYPE)) + cursor.addRow(arrayOf(authenticatorRepository.getUserType())) + return cursor + } + + private fun getAgeRatings(): Cursor { + val cursor = MatrixCursor(arrayOf(PACKAGE_NAME, AGE_RATING)) + return cursor + } + + override fun onCreate(): Boolean { + return true + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + return 0 + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + return 0 + } + +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt new file mode 100644 index 000000000..4611cbf4e --- /dev/null +++ b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt @@ -0,0 +1,15 @@ +package foundation.e.apps.provider + +import foundation.e.apps.BuildConfig + +class ProviderConstants { + companion object { + const val PACKAGE_NAME = "package_name" + const val AGE_RATING = "age_rating" + const val LOGIN_TYPE = "login_type" + + const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider" + const val PATH_LOGIN_TYPE = "login_type" + const val PATH_AGE_RATINGS = "age_ratings" + } +} \ No newline at end of file -- GitLab From 2d1b262a9e5a1bcf2d57520dab48c14d11889a2d Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 30 May 2024 20:34:15 +0530 Subject: [PATCH 08/64] corrections --- app/src/main/AndroidManifest.xml | 2 + .../e/apps/provider/AgeRatingProvider.kt | 37 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2fbb55770..9d089feae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,6 +49,8 @@ + + ? ): Int { - return 0 + throw UnsupportedOperationException("Not supported") } - override fun getType(uri: Uri): String? { - return null + override fun getType(uri: Uri): String { + return when (uriMatcher.match(uri)) { + CODE_LOGIN_TYPE -> "vnd.android.cursor.item/${AUTHORITY}.$CODE_LOGIN_TYPE" + CODE_AGE_RATING -> "vnd.android.cursor.item/${AUTHORITY}.$CODE_AGE_RATING" + else -> throw IllegalArgumentException("Unknown URI: $uri") + } } override fun insert(uri: Uri, values: ContentValues?): Uri? { - return null + throw UnsupportedOperationException("Not supported") } override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - return 0 + throw UnsupportedOperationException("Not supported") } } \ No newline at end of file -- GitLab From b3598e1c202616d7bf5393f26917d59e2cf94af4 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Fri, 31 May 2024 00:03:20 +0530 Subject: [PATCH 09/64] create getAgeRatings() --- .../e/apps/provider/AgeRatingProvider.kt | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index d91c4d5d7..5d4ce9a43 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -10,13 +10,22 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.install.pkg.AppLoungePackageManager import foundation.e.apps.provider.ProviderConstants.Companion.AGE_RATING import foundation.e.apps.provider.ProviderConstants.Companion.AUTHORITY import foundation.e.apps.provider.ProviderConstants.Companion.LOGIN_TYPE import foundation.e.apps.provider.ProviderConstants.Companion.PACKAGE_NAME import foundation.e.apps.provider.ProviderConstants.Companion.PATH_AGE_RATINGS import foundation.e.apps.provider.ProviderConstants.Companion.PATH_LOGIN_TYPE +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext class AgeRatingProvider : ContentProvider() { @@ -24,9 +33,15 @@ class AgeRatingProvider : ContentProvider() { @InstallIn(SingletonComponent::class) interface ContentProviderEntryPoint { fun getAuthenticationRepository(): AuthenticatorRepository + fun getPlayStoreRepository(): PlayStoreRepository + fun getApplicationRepository(): ApplicationRepository + fun getPackageManager(): AppLoungePackageManager } private lateinit var authenticatorRepository: AuthenticatorRepository + private lateinit var playStoreRepository: PlayStoreRepository + private lateinit var applicationRepository: ApplicationRepository + private lateinit var appLoungePackageManager: AppLoungePackageManager private val CODE_LOGIN_TYPE = 1 private val CODE_AGE_RATING = 2 @@ -48,6 +63,7 @@ class AgeRatingProvider : ContentProvider() { val code = uriMatcher.match(uri) return when (code) { CODE_LOGIN_TYPE -> getLoginType() + CODE_AGE_RATING -> getAgeRatings(uri) else -> null } } @@ -58,8 +74,37 @@ class AgeRatingProvider : ContentProvider() { return cursor } - private fun getAgeRatings(): Cursor { + private fun getAgeRatings(uri: Uri): Cursor { val cursor = MatrixCursor(arrayOf(PACKAGE_NAME, AGE_RATING)) + val packagesNames = appLoungePackageManager.getAllUserApps().map { it.packageName } + runBlocking { + withContext(IO) { + val contentRatingsDeferred = packagesNames.map { packagesName -> + async { + val authData = runCatching { + authenticatorRepository.gplayAuth + }.getOrNull() ?: return@async null + val appDetails = + applicationRepository.getApplicationDetails( + "", + packagesName, + authData, + Origin.GPLAY + ) + if (appDetails.first.package_name.isBlank()) return@async null + playStoreRepository.updateContentRatingWithId( + packagesName, + appDetails.first.contentRating + ).apply { + } + } + } + val contentsRatings = contentRatingsDeferred.awaitAll() + packagesNames.forEachIndexed { index, packageName -> + cursor.addRow(arrayOf(packageName, contentsRatings[index]?.title)) + } + } + } return cursor } @@ -69,6 +114,9 @@ class AgeRatingProvider : ContentProvider() { EntryPointAccessors.fromApplication(appContext, ContentProviderEntryPoint::class.java) authenticatorRepository = hiltEntryPoint.getAuthenticationRepository() + playStoreRepository = hiltEntryPoint.getPlayStoreRepository() + applicationRepository = hiltEntryPoint.getApplicationRepository() + appLoungePackageManager = hiltEntryPoint.getPackageManager() return true } -- GitLab From 796b50e63ac5ef21731d3d02bee694045dcefb84 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Fri, 31 May 2024 13:44:17 +0530 Subject: [PATCH 10/64] use id instead of title --- .../java/foundation/e/apps/provider/AgeRatingProvider.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 5d4ce9a43..bfa90496d 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -95,13 +95,12 @@ class AgeRatingProvider : ContentProvider() { playStoreRepository.updateContentRatingWithId( packagesName, appDetails.first.contentRating - ).apply { - } + ) } } val contentsRatings = contentRatingsDeferred.awaitAll() packagesNames.forEachIndexed { index, packageName -> - cursor.addRow(arrayOf(packageName, contentsRatings[index]?.title)) + cursor.addRow(arrayOf(packageName, contentsRatings[index]?.id)) } } } -- GitLab From 622f17dd679e9e701d5bb183a41db32e80c08c15 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 5 Jun 2024 16:32:11 +0530 Subject: [PATCH 11/64] rename method after rebase --- .../main/java/foundation/e/apps/provider/AgeRatingProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index bfa90496d..96007d19c 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -92,7 +92,7 @@ class AgeRatingProvider : ContentProvider() { Origin.GPLAY ) if (appDetails.first.package_name.isBlank()) return@async null - playStoreRepository.updateContentRatingWithId( + playStoreRepository.getContentRatingWithId( packagesName, appDetails.first.contentRating ) -- GitLab From 9cb353b416a2eb2f68605ea91756945201b9ee56 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 5 Jun 2024 17:22:15 +0530 Subject: [PATCH 12/64] remove unnecessary uri --- .../main/java/foundation/e/apps/provider/AgeRatingProvider.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 96007d19c..1f358e3d5 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -63,7 +63,7 @@ class AgeRatingProvider : ContentProvider() { val code = uriMatcher.match(uri) return when (code) { CODE_LOGIN_TYPE -> getLoginType() - CODE_AGE_RATING -> getAgeRatings(uri) + CODE_AGE_RATING -> getAgeRatings() else -> null } } @@ -74,7 +74,7 @@ class AgeRatingProvider : ContentProvider() { return cursor } - private fun getAgeRatings(uri: Uri): Cursor { + private fun getAgeRatings(): Cursor { val cursor = MatrixCursor(arrayOf(PACKAGE_NAME, AGE_RATING)) val packagesNames = appLoungePackageManager.getAllUserApps().map { it.packageName } runBlocking { -- GitLab From 38b7eaf09a450e98c2a062f3578fa71c05d770db Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 6 Jun 2024 21:03:00 +0600 Subject: [PATCH 13/64] refactor: simplify logic of validating app installation --- app/detekt-baseline.xml | 2 +- .../data/blockedApps/ContentRatingGroup.kt | 29 ---- .../blockedApps/ContentRatingsRepository.kt | 60 -------- .../blockedApps/ParentalControlRepository.kt | 60 -------- .../e/apps/data/install/models/AppInstall.kt | 24 +++ .../AppInstallationPermissionState.kt | 25 ++++ ...GetAppInstallationPermissionUseCaseImpl.kt | 138 ++++++++++++++++++ .../GetParentalControlStateUseCaseImpl.kt | 53 +++++++ .../GooglePlayContentRatingGroup.kt | 28 ++++ .../GooglePlayContentRatingParser.kt} | 37 +++-- .../GooglePlayContentRatingsRepository.kt | 52 +++++++ .../foundation/e/apps/di/UseCaseModule.kt | 45 ++++++ .../apps/domain/ValidateAppAgeLimitUseCase.kt | 109 -------------- .../GetAppInstallationPermissionUseCase.kt | 26 ++++ .../GetParentalControlStateUseCase.kt | 25 ++++ .../parentalcontrol/model/AgeGroupValue.kt | 27 ++++ .../model/ParentalControlState.kt | 28 ++++ .../workmanager/AppInstallProcessor.kt | 53 ++++--- .../e/apps/ui/MainActivityViewModel.kt | 6 +- .../ui/application/ApplicationFragment.kt | 9 +- .../AppInstallProcessorTest.kt | 32 ++-- 21 files changed, 555 insertions(+), 313 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt create mode 100644 app/src/main/java/foundation/e/apps/data/parentalcontrol/AppInstallationPermissionState.kt create mode 100644 app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt create mode 100644 app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt create mode 100644 app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt rename app/src/main/java/foundation/e/apps/data/{blockedApps/ContentRatingParser.kt => parentalcontrol/gplayrating/GooglePlayContentRatingParser.kt} (63%) create mode 100644 app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt create mode 100644 app/src/main/java/foundation/e/apps/di/UseCaseModule.kt delete mode 100644 app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt create mode 100644 app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetAppInstallationPermissionUseCase.kt create mode 100644 app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt create mode 100644 app/src/main/java/foundation/e/apps/domain/parentalcontrol/model/AgeGroupValue.kt create mode 100644 app/src/main/java/foundation/e/apps/domain/parentalcontrol/model/ParentalControlState.kt diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 3c941c851..3ba5196c4 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -36,7 +36,7 @@ LongParameterList:ApplicationViewModel.kt$ApplicationViewModel$( id: String, packageName: String, origin: Origin, isFdroidLink: Boolean, authObjectList: List<AuthObject>, retryBlock: (failedObjects: List<AuthObject>) -> Boolean, ) LongParameterList:CleanApkRetrofit.kt$CleanApkRetrofit$( @Query("keyword") keyword: String, @Query("source") source: String = APP_SOURCE_FOSS, @Query("type") type: String = APP_TYPE_ANY, @Query("nres") nres: Int = 20, @Query("page") page: Int = 1, @Query("by") by: String? = null, ) LongParameterList:EglExtensionProvider.kt$EglExtensionProvider$( egl10: EGL10, eglDisplay: EGLDisplay, eglConfig: EGLConfig?, ai: IntArray, ai1: IntArray?, set: MutableSet<String> ) - LongParameterList:MainActivityViewModel.kt$MainActivityViewModel$( private val appLoungeDataStore: AppLoungeDataStore, private val applicationRepository: ApplicationRepository, private val appManagerWrapper: AppManagerWrapper, private val appLoungePackageManager: AppLoungePackageManager, private val pwaManager: PWAManager, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, private val contentRatingsRepository: ContentRatingsRepository, private val appInstallProcessor: AppInstallProcessor, ) + LongParameterList:MainActivityViewModel.kt$MainActivityViewModel$( private val appLoungeDataStore: AppLoungeDataStore, private val applicationRepository: ApplicationRepository, private val appManagerWrapper: AppManagerWrapper, private val appLoungePackageManager: AppLoungePackageManager, private val pwaManager: PWAManager, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val appInstallProcessor: AppInstallProcessor, ) LongParameterList:UpdatesManagerImpl.kt$UpdatesManagerImpl$( @ApplicationContext private val context: Context, private val appLoungePackageManager: AppLoungePackageManager, private val applicationRepository: ApplicationRepository, private val faultyAppRepository: FaultyAppRepository, private val appLoungePreference: AppLoungePreference, private val fdroidRepository: FdroidRepository, private val blockedAppRepository: BlockedAppRepository, ) LongParameterList:UpdatesWorker.kt$UpdatesWorker$( @Assisted private val context: Context, @Assisted private val params: WorkerParameters, private val updatesManagerRepository: UpdatesManagerRepository, private val dataStoreManager: DataStoreManager, private val authenticatorRepository: AuthenticatorRepository, private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, ) MagicNumber:AnonymousLoginManager.kt$AnonymousLoginManager$200 diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt deleted file mode 100644 index cfb734abc..000000000 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * 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.data.blockedApps - -import com.google.gson.annotations.SerializedName - -data class ContentRatingGroup( - val id: String, - @SerializedName("age_group") - val ageGroup: String, - var ratings: List -) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt deleted file mode 100644 index 44f17df51..000000000 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * 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.data.blockedApps - -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import com.google.gson.reflect.TypeToken -import foundation.e.apps.data.DownloadManager -import foundation.e.apps.data.install.FileManager -import timber.log.Timber -import java.io.File -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class ContentRatingsRepository @Inject constructor( - private val downloadManager: DownloadManager, - private val contentRatingParser: ContentRatingParser -) { - - private var _contentRatingGroups = listOf() - val contentRatingGroups: List - get() = _contentRatingGroups - - companion object { - private const val CONTENT_RATINGS_FILE_URL = - "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + - "content_ratings.json?ref_type=heads&inline=false" - private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" - } - - fun fetchContentRatingData() { - downloadManager.downloadFileInCache( - CONTENT_RATINGS_FILE_URL, - fileName = CONTENT_RATINGS_FILE_NAME - ) { success, _ -> - if (success) { - contentRatingParser.parseContentRatingData() - } - } - } -} diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt deleted file mode 100644 index 6cab641e3..000000000 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * 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.data.blockedApps - -import android.content.Context -import android.net.Uri -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ParentalControlRepository @Inject constructor( - @ApplicationContext private val context: Context -) { - - companion object { - private const val URI_PARENTAL_CONTROL_PROVIDER = - "content://foundation.e.parentalcontrol.provider/age" - } - - fun getSelectedAgeGroup(): Age { - val uri = Uri.parse(URI_PARENTAL_CONTROL_PROVIDER) - val cursor = context.contentResolver.query(uri, null, null, null, null) - - cursor?.use { - if (it.moveToFirst()) { - val ageOrdinal = it.getInt(it.getColumnIndexOrThrow("age")) - return Age.values()[ageOrdinal] - } - } - - return Age.PARENTAL_CONTROL_DISABLED - } -} - -enum class Age { - THREE, - SIX, - ELEVEN, - FIFTEEN, - SEVENTEEN, - PARENTAL_CONTROL_DISABLED -} diff --git a/app/src/main/java/foundation/e/apps/data/install/models/AppInstall.kt b/app/src/main/java/foundation/e/apps/data/install/models/AppInstall.kt index 28f92b4d6..8d4f8a7e5 100644 --- a/app/src/main/java/foundation/e/apps/data/install/models/AppInstall.kt +++ b/app/src/main/java/foundation/e/apps/data/install/models/AppInstall.kt @@ -1,3 +1,21 @@ +/* + * Copyright (C) 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.data.install.models import androidx.room.Entity @@ -40,6 +58,12 @@ data class AppInstall( @Ignore var contentRating: ContentRating = ContentRating() + @Ignore + var isFDroidApp: Boolean = false + + @Ignore + var antiFeatures: List> = emptyList() + fun isAppInstalling() = installingStatusList.contains(status) fun isAwaiting() = status == Status.AWAITING diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/AppInstallationPermissionState.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/AppInstallationPermissionState.kt new file mode 100644 index 000000000..3821d3403 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/AppInstallationPermissionState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 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.data.parentalcontrol + +sealed class AppInstallationPermissionState { + object Allowed : AppInstallationPermissionState() + object Denied : AppInstallationPermissionState() + object DeniedOnDataLoadError : AppInstallationPermissionState() +} diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt new file mode 100644 index 000000000..8467b486a --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 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.data.parentalcontrol + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError +import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository +import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase +import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState.AgeGroup +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState.Disabled +import foundation.e.apps.domain.parentalcontrol.model.isEnabled +import javax.inject.Inject + +class GetAppInstallationPermissionUseCaseImpl +@Inject +constructor( + private val applicationRepository: ApplicationRepository, + private val dataStoreManager: DataStoreManager, + private val contentRatingRepository: GooglePlayContentRatingsRepository, + private val getParentalControlStateUseCase: GetParentalControlStateUseCase, + private val playStoreRepository: PlayStoreRepository +) : GetAppInstallationPermissionUseCase { + + companion object { + private const val KEY_ANTI_FEATURES_NSFW = "NSFW" + } + + override suspend operator fun invoke(app: AppInstall): AppInstallationPermissionState { + val parentalControl = getParentalControlStateUseCase.getParentalControlState() + return when (parentalControl) { + is Disabled -> Allowed + is AgeGroup -> + when { + isFDroidApp(app) -> validateNsfwAntiFeature(app, parentalControl) + else -> validateAgeLimit(app, parentalControl) + } + } + } + + private fun validateNsfwAntiFeature( + app: AppInstall, + parentalControl: ParentalControlState + ): AppInstallationPermissionState { + return when { + hasNoAntiFeatures(app) -> Allowed + isNsfwFDroidApp(app) && parentalControl.isEnabled -> Denied + else -> Allowed + } + } + + private fun hasNoAntiFeatures(app: AppInstall) = app.antiFeatures.isEmpty() + + private fun isNsfwFDroidApp(app: AppInstall) = + app.antiFeatures.any { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } + + private suspend fun validateAgeLimit( + app: AppInstall, + parentalControlState: AgeGroup + ): AppInstallationPermissionState { + + return when { + isGPlayApp(app) && hasNoContentRating(app) -> DeniedOnDataLoadError + hasValidAgeLimit(app, parentalControlState) -> Allowed + else -> Denied + } + } + + private fun isGPlayApp(app: AppInstall): Boolean { + return !isFDroidApp(app) && app.type != Type.PWA + } + + private fun isFDroidApp(app: AppInstall): Boolean = app.isFDroidApp + + private suspend fun hasNoContentRating(app: AppInstall): Boolean { + val authData = dataStoreManager.getAuthData() + return !verifyContentRatingExists(app, authData) + } + + private fun hasValidAgeLimit( + app: AppInstall, + parentalControlState: AgeGroup, + ): Boolean { + val allowedContentRatingGroup = + contentRatingRepository.contentRatingGroups.find { + it.id == parentalControlState.ageGroup.name + } + + return (app.contentRating.id.isNotEmpty() && + allowedContentRatingGroup?.ratings?.contains(app.contentRating.id) == true) + } + + private suspend fun verifyContentRatingExists(app: AppInstall, authData: AuthData): Boolean { + if (app.contentRating.title.isEmpty()) { + applicationRepository + .getApplicationDetails(app.id, app.packageName, authData, app.origin) + .let { (appDetails, resultStatus) -> + if (resultStatus == ResultStatus.OK) { + app.contentRating = appDetails.contentRating + } else { + return false + } + } + } + + if (app.contentRating.id.isEmpty()) { + app.contentRating = + playStoreRepository.getContentRatingWithId(app.packageName, app.contentRating) + } + + return app.contentRating.title.isNotEmpty() && app.contentRating.id.isNotEmpty() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt new file mode 100644 index 000000000..ee7a543a2 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 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.data.parentalcontrol + +import android.content.Context +import android.net.Uri +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase +import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GetParentalControlStateUseCaseImpl @Inject constructor( + @ApplicationContext private val context: Context +) : GetParentalControlStateUseCase { + companion object { + private const val URI_PARENTAL_CONTROL_PROVIDER = + "content://foundation.e.parentalcontrol.provider/age" + } + + override fun getParentalControlState(): ParentalControlState { + val uri = Uri.parse(URI_PARENTAL_CONTROL_PROVIDER) + context.contentResolver.query( + uri, null, null, null, null + )?.use { cursor -> + if (cursor.moveToFirst()) { + val ageOrdinal = cursor.getColumnIndexOrThrow("age").let(cursor::getInt) + val ageGroup = AgeGroupValue.values()[ageOrdinal] + return ParentalControlState.AgeGroup(ageGroup) + } + } + + return ParentalControlState.Disabled + } +} diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt new file mode 100644 index 000000000..6b59d5ba2 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 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.data.parentalcontrol.gplayrating + +import com.google.gson.annotations.SerializedName + +data class GooglePlayContentRatingGroup( + val id: String, + @SerializedName("age_group") + val ageGroup: String, + var ratings: List +) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingParser.kt similarity index 63% rename from app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt rename to app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingParser.kt index cde87e638..936675b79 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingParser.kt @@ -1,23 +1,22 @@ /* - * Copyright MURENA SAS 2024 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 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. + * 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 . + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * */ -package foundation.e.apps.data.blockedApps +package foundation.e.apps.data.parentalcontrol.gplayrating import com.google.gson.Gson import com.google.gson.JsonSyntaxException @@ -29,7 +28,7 @@ import java.io.IOException import javax.inject.Inject import javax.inject.Named -class ContentRatingParser @Inject constructor( +class GooglePlayContentRatingParser @Inject constructor( private val gson: Gson, @Named("cacheDir") private val cacheDir: String ) { @@ -38,7 +37,7 @@ class ContentRatingParser @Inject constructor( private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" } - fun parseContentRatingData(): List { + fun parseContentRatingData(): List { return try { val outputPath = moveFile() val contentRatingJson = readJsonFromFile(outputPath) @@ -69,9 +68,9 @@ class ContentRatingParser @Inject constructor( return outputPath } - private fun parseJsonOfContentRatingGroup(contentRatingJson: String): List { - val contentRatingsListTypeGroup = object : TypeToken>() {}.type - val contentRatingGroups: List = + private fun parseJsonOfContentRatingGroup(contentRatingJson: String): List { + val contentRatingsListTypeGroup = object : TypeToken>() {}.type + val contentRatingGroups: List = gson.fromJson(contentRatingJson, contentRatingsListTypeGroup) return contentRatingGroups.map { @@ -82,7 +81,7 @@ class ContentRatingParser @Inject constructor( } } - private fun handleException(exception: Exception): List { + private fun handleException(exception: Exception): List { Timber.e(exception.localizedMessage ?: "", exception) return listOf() } diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt new file mode 100644 index 000000000..ca544b115 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 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.data.parentalcontrol.gplayrating + +import foundation.e.apps.data.DownloadManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GooglePlayContentRatingsRepository @Inject constructor( + private val downloadManager: DownloadManager, + private val googlePlayContentRatingParser: GooglePlayContentRatingParser +) { + + private var _contentRatingGroups = listOf() + val contentRatingGroups: List + get() = _contentRatingGroups + + companion object { + private const val CONTENT_RATINGS_FILE_URL = + "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + + "content_ratings.json?ref_type=heads&inline=false" + private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" + } + + fun fetchContentRatingData() { + downloadManager.downloadFileInCache( + CONTENT_RATINGS_FILE_URL, + fileName = CONTENT_RATINGS_FILE_NAME + ) { success, _ -> + if (success) { + googlePlayContentRatingParser.parseContentRatingData() + } + } + } +} diff --git a/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt b/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt new file mode 100644 index 000000000..80cf276dd --- /dev/null +++ b/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 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.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl +import foundation.e.apps.data.parentalcontrol.GetParentalControlStateUseCaseImpl +import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase +import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface UseCaseModule { + @Singleton + @Binds + fun bindGetAppInstallationPermissionUseCase( + useCase: GetAppInstallationPermissionUseCaseImpl + ): GetAppInstallationPermissionUseCase + + @Singleton + @Binds + fun bindGetParentalControlStateUseCase( + useCase: GetParentalControlStateUseCaseImpl + ): GetParentalControlStateUseCase +} diff --git a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt deleted file mode 100644 index c859fe5e1..000000000 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 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 - -import com.aurora.gplayapi.data.models.AuthData -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.blockedApps.Age -import foundation.e.apps.data.blockedApps.ContentRatingGroup -import foundation.e.apps.data.blockedApps.ContentRatingsRepository -import foundation.e.apps.data.blockedApps.ParentalControlRepository -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.playstore.PlayStoreRepository -import foundation.e.apps.data.preference.DataStoreManager -import timber.log.Timber -import javax.inject.Inject - -class ValidateAppAgeLimitUseCase @Inject constructor( - private val applicationRepository: ApplicationRepository, - private val dataStoreManager: DataStoreManager, - private val contentRatingRepository: ContentRatingsRepository, - private val parentalControlRepository: ParentalControlRepository, - private val playStoreRepository: PlayStoreRepository -) { - - suspend operator fun invoke(app: AppInstall): ResultSupreme { - val authData = dataStoreManager.getAuthData() - val ageGroup = parentalControlRepository.getSelectedAgeGroup() - - return when { - isParentalControlDisabled(ageGroup) -> ResultSupreme.Success(data = true) - hasNoContentRating(app, authData) -> ResultSupreme.Error(data = false) - else -> validateAgeLimit(ageGroup, app) - } - } - - private fun validateAgeLimit( - ageGroup: Age, - app: AppInstall - ): ResultSupreme.Success { - val allowedContentRating = - contentRatingRepository.contentRatingGroups.find { it.id == ageGroup.toString() } - - Timber.d( - "Selected age group: $ageGroup \n" + - "Content rating: ${app.contentRating.id} \n" + - "Allowed content rating: $allowedContentRating" - ) - - return ResultSupreme.Success(isValidAppAgeRating(app, allowedContentRating)) - } - - private suspend fun hasNoContentRating(app: AppInstall, authData: AuthData) = - !verifyContentRatingExists(app, authData) - - private fun isValidAppAgeRating( - app: AppInstall, - allowedContentRating: ContentRatingGroup? - ) = (app.contentRating.id.isNotEmpty() - && allowedContentRating?.ratings?.contains(app.contentRating.id) == true) - - private fun isParentalControlDisabled(ageGroup: Age) = ageGroup == Age.PARENTAL_CONTROL_DISABLED - - private suspend fun verifyContentRatingExists( - app: AppInstall, - authData: AuthData - ): Boolean { - if (app.contentRating.title.isEmpty()) { - applicationRepository - .getApplicationDetails( - app.id, app.packageName, authData, app.origin - ).let { (appDetails, resultStatus) -> - if (resultStatus == ResultStatus.OK) { - app.contentRating = appDetails.contentRating - } else { - return false - } - } - } - - if (app.contentRating.id.isEmpty()) { - app.contentRating = - playStoreRepository.getContentRatingWithId( - app.packageName, - app.contentRating - ) - } - - return app.contentRating.title.isNotEmpty() && - app.contentRating.id.isNotEmpty() - } -} diff --git a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetAppInstallationPermissionUseCase.kt b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetAppInstallationPermissionUseCase.kt new file mode 100644 index 000000000..f6c509b5e --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetAppInstallationPermissionUseCase.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 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.parentalcontrol + +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState + +interface GetAppInstallationPermissionUseCase { + suspend operator fun invoke(app: AppInstall): AppInstallationPermissionState +} diff --git a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt new file mode 100644 index 000000000..36339959e --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 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.parentalcontrol + +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState + +interface GetParentalControlStateUseCase { + fun getParentalControlState(): ParentalControlState +} diff --git a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/model/AgeGroupValue.kt b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/model/AgeGroupValue.kt new file mode 100644 index 000000000..3df25d153 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/model/AgeGroupValue.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 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.parentalcontrol.model + +enum class AgeGroupValue { + THREE, + SIX, + ELEVEN, + FIFTEEN, + SEVENTEEN, +} diff --git a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/model/ParentalControlState.kt b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/model/ParentalControlState.kt new file mode 100644 index 000000000..421970ed2 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/model/ParentalControlState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 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.parentalcontrol.model + +sealed class ParentalControlState { + object Disabled : ParentalControlState() + + class AgeGroup(val ageGroup: AgeGroupValue) : ParentalControlState() +} + +val ParentalControlState.isEnabled + get() = this != ParentalControlState.Disabled && this is ParentalControlState.AgeGroup diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index 4a269e555..0e3880b67 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -1,6 +1,5 @@ /* - * Copyright MURENA SAS 2023 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 @@ -14,6 +13,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.install.workmanager @@ -23,16 +23,19 @@ import com.aurora.gplayapi.exceptions.ApiException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.enums.Type import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.UpdatesDao import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError +import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.DataStoreManager -import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.download.DownloadManagerUtils import foundation.e.apps.install.notification.StorageNotificationManager @@ -53,7 +56,7 @@ class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, private val appInstallComponents: AppInstallComponents, private val applicationRepository: ApplicationRepository, - private val validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase, + private val getAppInstallationPermissionUseCaseImpl: GetAppInstallationPermissionUseCaseImpl, private val dataStoreManager: DataStoreManager, private val storageNotificationManager: StorageNotificationManager, ) { @@ -93,8 +96,10 @@ class AppInstallProcessor @Inject constructor( application.offer_type, application.isFree, application.originalSize - ).also { - it.contentRating = application.contentRating + ).apply { + this.contentRating = application.contentRating + this.isFDroidApp = application.isFDroidApp + this.antiFeatures = application.antiFeatures } if (appInstall.type == Type.PWA) { @@ -131,17 +136,31 @@ class AppInstallProcessor @Inject constructor( return } - val ageLimitValidationResult = validateAppAgeLimitUseCase.invoke(appInstall) - if (ageLimitValidationResult.data == false) { - if (ageLimitValidationResult.isSuccess()) { - Timber.i("Content rating is not allowed for: ${appInstall.name}") + + val installationPermission = + getAppInstallationPermissionUseCaseImpl.invoke(appInstall) + when (installationPermission) { + Allowed -> { + Timber.tag("Parental control") + .i("${appInstall.name} is allowed to be installed.") + // no operation, allow installation + } + + Denied -> { + Timber.tag("Parental control") + .i("${appInstall.name} can't be installed because of parental control setting.") EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(appInstall.name)) - } else { - EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) + appInstallComponents.appManagerWrapper.cancelDownload(appInstall) + return } - appInstallComponents.appManagerWrapper.cancelDownload(appInstall) - return + DeniedOnDataLoadError -> { + Timber.tag("Parental control") + .i("${appInstall.name} can't be installed because of unavailable data.") + EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) + appInstallComponents.appManagerWrapper.cancelDownload(appInstall) + return + } } if (!context.isNetworkAvailable()) { 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 d3976c568..981cd623d 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -35,7 +35,7 @@ import foundation.e.apps.R import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.blockedApps.BlockedAppRepository -import foundation.e.apps.data.blockedApps.ContentRatingsRepository +import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository import foundation.e.apps.data.ecloud.EcloudRepository import foundation.e.apps.data.enums.User import foundation.e.apps.data.enums.isInitialized @@ -60,7 +60,7 @@ class MainActivityViewModel @Inject constructor( private val pwaManager: PWAManager, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, - private val contentRatingsRepository: ContentRatingsRepository, + private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val appInstallProcessor: AppInstallProcessor, ) : ViewModel() { @@ -233,7 +233,7 @@ class MainActivityViewModel @Inject constructor( fun updateContentRatings() { viewModelScope.launch { - contentRatingsRepository.fetchContentRatingData() + googlePlayContentRatingsRepository.fetchContentRatingData() } } 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 4bb2230ba..8c1a60aa8 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 @@ -473,8 +473,13 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } } - private fun formatCategoryText(catText: String) = catText.replace("_", " ") - .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + private fun formatCategoryText(text: String): String { + return text + .replace("_", " ") + .replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } // Capitalize, example: books and reference -> Books and reference + } private fun setupScreenshotRVAdapter() { screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(origin) diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt index 480eeadda..4895aa527 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -1,6 +1,5 @@ /* - * Copyright MURENA SAS 2023 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 @@ -14,6 +13,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.installProcessor @@ -21,17 +21,15 @@ package foundation.e.apps.installProcessor import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.ContentRating -import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fdroid.FdroidRepository import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState import foundation.e.apps.data.preference.DataStoreManager -import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.workmanager.AppInstallProcessor @@ -46,7 +44,6 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import kotlin.reflect.jvm.internal.ReflectProperties.Val @OptIn(ExperimentalCoroutinesApi::class) class AppInstallProcessorTest { @@ -82,7 +79,7 @@ class AppInstallProcessorTest { private lateinit var appInstallProcessor: AppInstallProcessor @Mock - private lateinit var validateAppAgeRatingUseCase: ValidateAppAgeLimitUseCase + private lateinit var getAppInstallationPermissionUseCase: GetAppInstallationPermissionUseCaseImpl @Mock private lateinit var storageNotificationManager: StorageNotificationManager @@ -101,7 +98,7 @@ class AppInstallProcessorTest { context, appInstallComponents, applicationRepository, - validateAppAgeRatingUseCase, + getAppInstallationPermissionUseCase, dataStoreManager, storageNotificationManager ) @@ -193,8 +190,8 @@ class AppInstallProcessorTest { @Test fun `processInstallTest when age limit is satisfied`() = runTest { val fusedDownload = initTest() - Mockito.`when`(validateAppAgeRatingUseCase.invoke(fusedDownload)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, true)) + Mockito.`when`(getAppInstallationPermissionUseCase.invoke(fusedDownload)) + .thenReturn(AppInstallationPermissionState.Allowed) val finalFusedDownload = runProcessInstall(fusedDownload) assertEquals("processInstall", finalFusedDownload, null) @@ -203,8 +200,17 @@ class AppInstallProcessorTest { @Test fun `processInstallTest when age limit is not satisfied`() = runTest { val fusedDownload = initTest() - Mockito.`when`(validateAppAgeRatingUseCase.invoke(fusedDownload)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, false)) + Mockito.`when`(getAppInstallationPermissionUseCase.invoke(fusedDownload)) + .thenReturn(AppInstallationPermissionState.Denied) + + val finalFusedDownload = runProcessInstall(fusedDownload) + assertEquals("processInstall", finalFusedDownload, null) + } + @Test + fun `processInstallTest when installation denied for data loading error`() = runTest { + val fusedDownload = initTest() + Mockito.`when`(getAppInstallationPermissionUseCase.invoke(fusedDownload)) + .thenReturn(AppInstallationPermissionState.DeniedOnDataLoadError) val finalFusedDownload = runProcessInstall(fusedDownload) assertEquals("processInstall", finalFusedDownload, null) -- GitLab From 669e0c4cd235ede8d604ec5ec19243de54847317 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 6 Jun 2024 23:52:34 +0600 Subject: [PATCH 14/64] test: add unit tests for GetAppInstallationPermissionUseCase --- ...GetAppInstallationPermissionUseCaseImpl.kt | 3 +- .../GetParentalControlStateUseCaseImpl.kt | 2 +- .../data/playstore/PlayStoreRepositoryImpl.kt | 2 +- .../GetParentalControlStateUseCase.kt | 2 +- ...GetAppInstallationPermissionUseCaseTest.kt | 299 ++++++++++++++++++ 5 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt index 8467b486a..80af242f2 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt @@ -52,8 +52,7 @@ constructor( } override suspend operator fun invoke(app: AppInstall): AppInstallationPermissionState { - val parentalControl = getParentalControlStateUseCase.getParentalControlState() - return when (parentalControl) { + return when (val parentalControl = getParentalControlStateUseCase.invoke()) { is Disabled -> Allowed is AgeGroup -> when { diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt index ee7a543a2..21107206a 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt @@ -36,7 +36,7 @@ class GetParentalControlStateUseCaseImpl @Inject constructor( "content://foundation.e.parentalcontrol.provider/age" } - override fun getParentalControlState(): ParentalControlState { + override suspend fun invoke(): ParentalControlState { val uri = Uri.parse(URI_PARENTAL_CONTROL_PROVIDER) context.contentResolver.query( uri, null, null, null, null diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt index da8847721..9c1703cb8 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt @@ -218,7 +218,7 @@ class PlayStoreRepositoryImpl @Inject constructor( appPackage: String, contentRating: ContentRating ): ContentRating { - val authData = authenticatorRepository.gplayAuth!! + val authData = authenticatorRepository.gplayAuth ?: return contentRating val contentRatingHelper = ContentRatingHelper(authData) return withContext(Dispatchers.IO) { diff --git a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt index 36339959e..3f24c4cc2 100644 --- a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt @@ -21,5 +21,5 @@ package foundation.e.apps.domain.parentalcontrol import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState interface GetParentalControlStateUseCase { - fun getParentalControlState(): ParentalControlState + suspend operator fun invoke(): ParentalControlState } diff --git a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt new file mode 100644 index 000000000..e01242ceb --- /dev/null +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -0,0 +1,299 @@ +/* + * Copyright (C) 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.parentalcontrol + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.ContentRating +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied +import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl +import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingGroup +import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository +import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase +import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase +import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState +import foundation.e.apps.util.MainCoroutineRule +import kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +private const val s = "" + +class GetAppInstallationPermissionUseCaseTest { + // Run tasks synchronously + @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() + + // Sets the main coroutines dispatcher to a TestCoroutineScope for unit testing. + @ExperimentalCoroutinesApi @get:Rule var mainCoroutineRule = MainCoroutineRule() + + private lateinit var useCase: GetAppInstallationPermissionUseCase + + @Mock private lateinit var applicationRepository: ApplicationRepository + + @Mock private lateinit var dataStoreManager: DataStoreManager + + @Mock private lateinit var contentRatingsRepository: GooglePlayContentRatingsRepository + + @Mock private lateinit var getParentalControlStateUseCase: GetParentalControlStateUseCase + + @Mock private lateinit var playStoreRepository: PlayStoreRepository + + @Mock private lateinit var authenticatorRepository: AuthenticatorRepository + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + useCase = + GetAppInstallationPermissionUseCaseImpl( + applicationRepository, + dataStoreManager, + contentRatingsRepository, + getParentalControlStateUseCase, + playStoreRepository) + } + + @Test + fun `allow app installation when parental control is disabled`() { + runTest { + val appPendingInstallation = AppInstall() + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.Disabled) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `allow app installation when parental control is disabled and F-Droid app has no anti-features`() { + runTest { + val appPendingInstallation = + AppInstall().apply { + isFDroidApp = true + antiFeatures = emptyList() + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.Disabled) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `allow app installation when parental control is disabled and F-Droid app has anti-features other than NSFW`() { + runTest { + val appPendingInstallation = + AppInstall().apply { + isFDroidApp = true + antiFeatures = + listOf( + mapOf( + "NonFreeAssets" to + "Artwork, layouts and prerecorded voices are under a non-commercial license")) + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.Disabled) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `allow app installation when parental control is enabled and F-Droid app has anti-features other than NSFW`() { + runTest { + val appPendingInstallation = + AppInstall().apply { + isFDroidApp = true + antiFeatures = + listOf( + mapOf( + "NonFreeAssets" to + "Artwork, layouts and prerecorded voices are under a non-commercial license")) + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `deny app installation when parental control is enabled and F-Droid app has NSFW anti-features`() { + runTest { + val appPendingInstallation = + AppInstall().apply { + isFDroidApp = true + antiFeatures = listOf(mapOf("NSFW" to "Shows explicit content.")) + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Denied, installationPermissionState) + } + } + + @Test + fun `allow app installation when parental control is enabled and Google Play app's rating is equal to child's age group`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "Rated for 3+" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "THREE", + ageGroup = "0-3", + ratings = + listOf("rated for 3+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.OK)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `deny app installation when parental control is enabled and Google Play app's rating exceeds child's age group`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "Rated for 7+" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "THREE", + ageGroup = "0-3", + ratings = + listOf("rated for 3+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.OK)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Denied, installationPermissionState) + } + } +} -- GitLab From b4eb977cdd054df84c668b08a3395c00046a481a Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 7 Jun 2024 00:12:09 +0600 Subject: [PATCH 15/64] refactor: remove unneeded code [skip-ci] --- .../GetAppInstallationPermissionUseCaseTest.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt index e01242ceb..4c823b3b1 100644 --- a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -38,7 +38,6 @@ import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState import foundation.e.apps.util.MainCoroutineRule -import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -47,8 +46,7 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations - -private const val s = "" +import kotlin.test.assertEquals class GetAppInstallationPermissionUseCaseTest { // Run tasks synchronously -- GitLab From dd92fe593a22de929e1db134dbc09bf0254b647e Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 6 Jun 2024 23:48:28 +0530 Subject: [PATCH 16/64] correct content ratings not being stored after fetching --- .../e/apps/data/blockedApps/ContentRatingsRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 44f17df51..7d96e3a13 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -53,7 +53,7 @@ class ContentRatingsRepository @Inject constructor( fileName = CONTENT_RATINGS_FILE_NAME ) { success, _ -> if (success) { - contentRatingParser.parseContentRatingData() + _contentRatingGroups = contentRatingParser.parseContentRatingData() } } } -- GitLab From 29d72686352c371cd510b7c771e3f0fb6a1da29c Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 6 Jun 2024 23:49:08 +0530 Subject: [PATCH 17/64] use validateAppAgeLimitUseCase and pass only eligible package names --- .../e/apps/provider/AgeRatingProvider.kt | 57 ++++++++++++------- .../e/apps/provider/ProviderConstants.kt | 1 - 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 1f358e3d5..2b3c53a86 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -11,11 +11,14 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.blockedApps.ContentRatingsRepository import foundation.e.apps.data.enums.Origin +import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.install.pkg.AppLoungePackageManager -import foundation.e.apps.provider.ProviderConstants.Companion.AGE_RATING import foundation.e.apps.provider.ProviderConstants.Companion.AUTHORITY import foundation.e.apps.provider.ProviderConstants.Companion.LOGIN_TYPE import foundation.e.apps.provider.ProviderConstants.Companion.PACKAGE_NAME @@ -36,12 +39,18 @@ class AgeRatingProvider : ContentProvider() { fun getPlayStoreRepository(): PlayStoreRepository fun getApplicationRepository(): ApplicationRepository fun getPackageManager(): AppLoungePackageManager + fun getContentRatingsRepository(): ContentRatingsRepository + fun getValidateAppAgeLimitUseCase(): ValidateAppAgeLimitUseCase + fun getDataStoreManager(): DataStoreManager } private lateinit var authenticatorRepository: AuthenticatorRepository private lateinit var playStoreRepository: PlayStoreRepository private lateinit var applicationRepository: ApplicationRepository private lateinit var appLoungePackageManager: AppLoungePackageManager + private lateinit var contentRatingsRepository: ContentRatingsRepository + private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase + private lateinit var dataStoreManager: DataStoreManager private val CODE_LOGIN_TYPE = 1 private val CODE_AGE_RATING = 2 @@ -70,37 +79,41 @@ class AgeRatingProvider : ContentProvider() { private fun getLoginType(): Cursor { val cursor = MatrixCursor(arrayOf(LOGIN_TYPE)) - cursor.addRow(arrayOf(authenticatorRepository.getUserType())) + cursor.addRow(arrayOf(dataStoreManager.getUserType())) return cursor } private fun getAgeRatings(): Cursor { - val cursor = MatrixCursor(arrayOf(PACKAGE_NAME, AGE_RATING)) + val cursor = MatrixCursor(arrayOf(PACKAGE_NAME)) val packagesNames = appLoungePackageManager.getAllUserApps().map { it.packageName } runBlocking { withContext(IO) { - val contentRatingsDeferred = packagesNames.map { packagesName -> + + if (contentRatingsRepository.contentRatingGroups.isEmpty()) { + contentRatingsRepository.fetchContentRatingData() + } + + val contentRatingsDeferred = packagesNames.map { packageName -> async { - val authData = runCatching { - authenticatorRepository.gplayAuth - }.getOrNull() ?: return@async null - val appDetails = - applicationRepository.getApplicationDetails( - "", - packagesName, - authData, - Origin.GPLAY - ) - if (appDetails.first.package_name.isBlank()) return@async null - playStoreRepository.getContentRatingWithId( - packagesName, - appDetails.first.contentRating + val authData = dataStoreManager.getAuthData() + if (authData.email.isBlank() && authData.aasToken.isBlank()) { + return@async null + } else { + authenticatorRepository.gplayAuth = authData + } + val fakeAppInstall = AppInstall( + packageName = packageName, + origin = Origin.GPLAY ) + val validateResult = validateAppAgeLimitUseCase(fakeAppInstall) + validateResult.data ?: false } } val contentsRatings = contentRatingsDeferred.awaitAll() - packagesNames.forEachIndexed { index, packageName -> - cursor.addRow(arrayOf(packageName, contentsRatings[index]?.id)) + contentsRatings.forEachIndexed { index: Int, isValid: Boolean? -> + if (isValid == true) { + cursor.addRow(arrayOf(packagesNames[index])) + } } } } @@ -116,6 +129,10 @@ class AgeRatingProvider : ContentProvider() { playStoreRepository = hiltEntryPoint.getPlayStoreRepository() applicationRepository = hiltEntryPoint.getApplicationRepository() appLoungePackageManager = hiltEntryPoint.getPackageManager() + contentRatingsRepository = hiltEntryPoint.getContentRatingsRepository() + validateAppAgeLimitUseCase = hiltEntryPoint.getValidateAppAgeLimitUseCase() + dataStoreManager = hiltEntryPoint.getDataStoreManager() + return true } diff --git a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt index 4611cbf4e..3cad0c306 100644 --- a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt +++ b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt @@ -5,7 +5,6 @@ import foundation.e.apps.BuildConfig class ProviderConstants { companion object { const val PACKAGE_NAME = "package_name" - const val AGE_RATING = "age_rating" const val LOGIN_TYPE = "login_type" const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider" -- GitLab From 8297a2a01a96fe71119277d738710de082caca48 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Fri, 7 Jun 2024 00:22:04 +0530 Subject: [PATCH 18/64] use retrofit api to get age groups --- .../e/apps/data/ageRating/AgeGroupApi.kt | 35 +++++++++++++++++++ .../data/blockedApps/ContentRatingGroup.kt | 4 +-- .../blockedApps/ContentRatingsRepository.kt | 31 ++++------------ .../e/apps/data/cleanapk/RetrofitModule.kt | 18 ++++++---- 4 files changed, 55 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt diff --git a/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt b/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt new file mode 100644 index 000000000..48952e1cd --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt @@ -0,0 +1,35 @@ +/* + * Copyright MURENA SAS 2024 + * 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.data.ageRating + +import foundation.e.apps.data.blockedApps.ContentRatingGroup +import retrofit2.Response +import retrofit2.http.GET + +interface AgeGroupApi { + + companion object { + val BASE_URL = "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + } + + @GET("content_ratings.json?ref_type=heads") + suspend fun getDefinedAgeGroups(): Response> + +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt index cfb734abc..ec2be627e 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt @@ -19,11 +19,11 @@ package foundation.e.apps.data.blockedApps -import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json data class ContentRatingGroup( val id: String, - @SerializedName("age_group") + @Json(name = "age_group") val ageGroup: String, var ratings: List ) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 7d96e3a13..09e2fea8e 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -19,42 +19,23 @@ package foundation.e.apps.data.blockedApps -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import com.google.gson.reflect.TypeToken -import foundation.e.apps.data.DownloadManager -import foundation.e.apps.data.install.FileManager -import timber.log.Timber -import java.io.File +import foundation.e.apps.data.ageRating.AgeGroupApi import javax.inject.Inject -import javax.inject.Named import javax.inject.Singleton @Singleton class ContentRatingsRepository @Inject constructor( - private val downloadManager: DownloadManager, - private val contentRatingParser: ContentRatingParser + private val ageGroupApi: AgeGroupApi, ) { private var _contentRatingGroups = listOf() val contentRatingGroups: List get() = _contentRatingGroups - companion object { - private const val CONTENT_RATINGS_FILE_URL = - "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + - "content_ratings.json?ref_type=heads&inline=false" - private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" - } - - fun fetchContentRatingData() { - downloadManager.downloadFileInCache( - CONTENT_RATINGS_FILE_URL, - fileName = CONTENT_RATINGS_FILE_NAME - ) { success, _ -> - if (success) { - _contentRatingGroups = contentRatingParser.parseContentRatingData() - } + suspend fun fetchContentRatingData() { + val response = ageGroupApi.getDefinedAgeGroups() + if (response.isSuccessful) { + _contentRatingGroups = response.body() ?: emptyList() } } } diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt index 8744d01df..28c5ea449 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt @@ -29,23 +29,18 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.ageRating.AgeGroupApi import foundation.e.apps.data.cleanapk.data.app.Application import foundation.e.apps.data.ecloud.EcloudApiInterface import foundation.e.apps.data.exodus.ExodusTrackerApi import foundation.e.apps.data.fdroid.FdroidApiInterface import okhttp3.Cache import okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient -import okhttp3.Protocol -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory -import timber.log.Timber -import java.net.ConnectException import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Named @@ -131,6 +126,17 @@ object RetrofitModule { .create(EcloudApiInterface::class.java) } + @Singleton + @Provides + fun provideAgeGroupApi(okHttpClient: OkHttpClient, moshi: Moshi): AgeGroupApi { + return Retrofit.Builder() + .baseUrl(AgeGroupApi.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(AgeGroupApi::class.java) + } + @Singleton @Provides fun getMoshi(): Moshi { -- GitLab From 6379c7c0c1e184cc9f0e703709af11eeb6603cf3 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Fri, 7 Jun 2024 00:23:22 +0530 Subject: [PATCH 19/64] add some licenses --- .../e/apps/provider/AgeRatingProvider.kt | 19 +++++++++++++++++++ .../e/apps/provider/ProviderConstants.kt | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 2b3c53a86..3a17b795e 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -1,3 +1,22 @@ +/* + * Copyright MURENA SAS 2024 + * 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.provider import android.content.ContentProvider diff --git a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt index 3cad0c306..0af7a9d06 100644 --- a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt +++ b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt @@ -1,3 +1,22 @@ +/* + * Copyright MURENA SAS 2024 + * 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.provider import foundation.e.apps.BuildConfig -- GitLab From 45adc5e3e6d3f9eeaad8eeabff9d2fc2d3c8cd47 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Fri, 7 Jun 2024 01:05:01 +0530 Subject: [PATCH 20/64] convert age ratings to lowercase before comparison --- .../foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index c859fe5e1..6e4d3195f 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -73,8 +73,10 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private fun isValidAppAgeRating( app: AppInstall, allowedContentRating: ContentRatingGroup? - ) = (app.contentRating.id.isNotEmpty() - && allowedContentRating?.ratings?.contains(app.contentRating.id) == true) + ): Boolean { + val allowedAgeRatings = allowedContentRating?.ratings?.map { it.lowercase() } ?: emptyList() + return app.contentRating.id.isNotEmpty() && allowedAgeRatings.contains(app.contentRating.id) + } private fun isParentalControlDisabled(ageGroup: Age) = ageGroup == Age.PARENTAL_CONTROL_DISABLED -- GitLab From 45f75c6bc1e05864b00ea601e399679b9796b845 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 7 Jun 2024 11:47:31 +0600 Subject: [PATCH 21/64] test: add more unit tests to cover all the scenarios --- ...GetAppInstallationPermissionUseCaseImpl.kt | 30 +-- ...GetAppInstallationPermissionUseCaseTest.kt | 185 +++++++++++++++++- 2 files changed, 201 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt index 80af242f2..9006b9242 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt @@ -42,7 +42,7 @@ class GetAppInstallationPermissionUseCaseImpl constructor( private val applicationRepository: ApplicationRepository, private val dataStoreManager: DataStoreManager, - private val contentRatingRepository: GooglePlayContentRatingsRepository, + private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val getParentalControlStateUseCase: GetParentalControlStateUseCase, private val playStoreRepository: PlayStoreRepository ) : GetAppInstallationPermissionUseCase { @@ -57,7 +57,7 @@ constructor( is AgeGroup -> when { isFDroidApp(app) -> validateNsfwAntiFeature(app, parentalControl) - else -> validateAgeLimit(app, parentalControl) + else -> validateGooglePlayContentRating(app, parentalControl) } } } @@ -78,19 +78,19 @@ constructor( private fun isNsfwFDroidApp(app: AppInstall) = app.antiFeatures.any { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } - private suspend fun validateAgeLimit( + private suspend fun validateGooglePlayContentRating( app: AppInstall, parentalControlState: AgeGroup ): AppInstallationPermissionState { return when { - isGPlayApp(app) && hasNoContentRating(app) -> DeniedOnDataLoadError - hasValidAgeLimit(app, parentalControlState) -> Allowed + isGooglePlayApp(app) && hasNoContentRating(app) -> DeniedOnDataLoadError + hasValidContentRating(app, parentalControlState) -> Allowed else -> Denied } } - private fun isGPlayApp(app: AppInstall): Boolean { + private fun isGooglePlayApp(app: AppInstall): Boolean { return !isFDroidApp(app) && app.type != Type.PWA } @@ -101,17 +101,21 @@ constructor( return !verifyContentRatingExists(app, authData) } - private fun hasValidAgeLimit( + private fun hasValidContentRating( app: AppInstall, parentalControlState: AgeGroup, ): Boolean { - val allowedContentRatingGroup = - contentRatingRepository.contentRatingGroups.find { - it.id == parentalControlState.ageGroup.name + return when { + app.contentRating.id.isBlank() -> false + else -> { + val allowedContentRatingGroup = + googlePlayContentRatingsRepository.contentRatingGroups.find { + it.id == parentalControlState.ageGroup.name + } ?: return false + + allowedContentRatingGroup.ratings.contains(app.contentRating.id) } - - return (app.contentRating.id.isNotEmpty() && - allowedContentRatingGroup?.ratings?.contains(app.contentRating.id) == true) + } } private suspend fun verifyContentRatingExists(app: AppInstall, authData: AuthData): Boolean { diff --git a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt index 4c823b3b1..7f46db251 100644 --- a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -28,6 +28,7 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingGroup import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository @@ -38,6 +39,7 @@ import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState import foundation.e.apps.util.MainCoroutineRule +import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -46,9 +48,9 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import kotlin.test.assertEquals class GetAppInstallationPermissionUseCaseTest { + // Run tasks synchronously @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() @@ -294,4 +296,185 @@ class GetAppInstallationPermissionUseCaseTest { assertEquals(Denied, installationPermissionState) } } + + @Test + fun `deny app installation on data load error when parental control is enabled and Google Play app has no content rating`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "THREE", + ageGroup = "0-3", + ratings = + listOf("rated for 3+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.OK)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(DeniedOnDataLoadError, installationPermissionState) + } + } + + @Test + fun `deny app installation on data load error when parental control is enabled and Google Play app has no content rating and app details can't be loaded`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "THREE", + ageGroup = "0-3", + ratings = + listOf("rated for 3+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.UNKNOWN)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(DeniedOnDataLoadError, installationPermissionState) + } + } + + // Note: This case is unlikely to happen + @Test + fun `deny app installation when parental control is enabled and parental control state can't match with Google Play content ratings`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "Rated for 3+" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "EIGHTEEN", + ageGroup = "18+", + ratings = + listOf("rated for 18+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.UNKNOWN)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Denied, installationPermissionState) + } + } } -- GitLab From fb379e8fac1f7cab08e085a156bbf629dd87c1b3 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Sun, 9 Jun 2024 01:39:18 +0530 Subject: [PATCH 22/64] change from allowlist to blocklist --- .../main/java/foundation/e/apps/provider/AgeRatingProvider.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 3a17b795e..856d8d9c4 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -130,7 +130,8 @@ class AgeRatingProvider : ContentProvider() { } val contentsRatings = contentRatingsDeferred.awaitAll() contentsRatings.forEachIndexed { index: Int, isValid: Boolean? -> - if (isValid == true) { + if (isValid == false) { + // Collect package names for blocklist cursor.addRow(arrayOf(packagesNames[index])) } } -- GitLab From e9b3997fb96bf22e80fed5b2853bc45de9f26ffc Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Sun, 9 Jun 2024 03:49:18 +0530 Subject: [PATCH 23/64] send broadcast to parental control on successful login --- app/build.gradle | 5 +++++ app/src/main/AndroidManifest.xml | 6 ++++++ .../java/foundation/e/apps/MainActivity.kt | 20 ++++++++++++++++++- .../java/foundation/e/apps/data/Constants.kt | 5 +++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c324b3683..ee9b5dac6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,6 +54,11 @@ android { buildConfigField "String", "BUILD_ID", "\"${getGitHash() + "." + getDate()}\"" buildConfigField("String", "SENTRY_DSN", "\"${getSentryDsn()}\"") + def parentalControlPkgName = "foundation.e.parentalcontrol" + + manifestPlaceholders = [parentalControlPkgName: parentalControlPkgName] + buildConfigField "String", "PACKAGE_NAME_PARENTAL_CONTROL", "\"${parentalControlPkgName}\"" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d089feae..6f9bb1e64 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,6 +49,12 @@ + + + + + + {} } - it.find { it is AuthObject.GPlayAuth }?.result?.run { + val gPlayAuthObject = it.find { it is AuthObject.GPlayAuth } + + gPlayAuthObject?.result?.run { if (isSuccess()) { viewModel.gPlayAuthData = data as AuthData } else if (exception is GPlayValidationException) { @@ -333,7 +338,20 @@ class MainActivity : AppCompatActivity() { Timber.e(exception, "Login failed! message: ${exception?.localizedMessage}") } } + + // Broadcast if not gplay type login or successful gplay login + if (gPlayAuthObject == null || gPlayAuthObject.result.isSuccess()) { + broadcastGPlayLogin() + } + } + } + + private fun broadcastGPlayLogin() { + val intent = Intent(Constants.ACTION_PARENTAL_CONTROL_APP_LOUNGE_LOGIN).apply { + setPackage(BuildConfig.PACKAGE_NAME_PARENTAL_CONTROL) + putExtra(LOGIN_TYPE, viewModel.getUser().name) } + sendBroadcast(intent) } private fun setupViewModels() { diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index 09b79b972..3f8474aa4 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -18,6 +18,8 @@ package foundation.e.apps.data +import foundation.e.apps.BuildConfig + object Constants { const val timeoutDurationInMillis: Long = 10000 @@ -30,4 +32,7 @@ object Constants { const val ACTION_DUMP_APP_INSTALL_STATE = "foundation.e.apps.action.APP_INSTALL_STATE" const val TAG_APP_INSTALL_STATE = "APP_INSTALL_STATE" + + const val ACTION_PARENTAL_CONTROL_APP_LOUNGE_LOGIN = + "${BuildConfig.PACKAGE_NAME_PARENTAL_CONTROL}.action.APP_LOUNGE_LOGIN" } -- GitLab From aa1d4498248348e74dc736ea4a241a16c4cfd613 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Sun, 9 Jun 2024 03:50:50 +0530 Subject: [PATCH 24/64] return if authobjects are empty --- app/src/main/java/foundation/e/apps/MainActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index a5ddc2b83..82c2cdd26 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -318,6 +318,7 @@ class MainActivity : AppCompatActivity() { // Pop back stack to prevent showing TOSFragment on pressing back button. navController.popBackStack() navController.navigate(R.id.signInFragment) + return@observe } else -> {} -- GitLab From 38d2f736a7ce6098a2a5e46c5ef0276c9a102081 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Sun, 9 Jun 2024 06:55:33 +0530 Subject: [PATCH 25/64] Accept intent extra to login and close App Lounge --- .../main/java/foundation/e/apps/MainActivity.kt | 17 +++++++++++++++++ .../java/foundation/e/apps/data/Constants.kt | 2 ++ .../apps/data/preference/AppLoungeDataStore.kt | 2 +- .../e/apps/ui/MainActivityViewModel.kt | 4 ++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 82c2cdd26..eda010784 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -42,6 +42,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.data.Constants +import foundation.e.apps.data.enums.User import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.LoginViewModel @@ -76,6 +77,9 @@ class MainActivity : AppCompatActivity() { private const val SESSION_DIALOG_TAG = "session_dialog" } + private var gPlayLoginRequested = false + private var closeAfterLogin = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -126,6 +130,14 @@ class MainActivity : AppCompatActivity() { viewModel.updateContentRatings() observeEvents() + + gPlayLoginRequested = intent.getBooleanExtra(Constants.REQUEST_GPLAY_LOGIN, false) + + if (!gPlayLoginRequested) return + if (!viewModel.getTocStatus()) return + if (viewModel.getUser() !in listOf(User.GOOGLE, User.ANONYMOUS)) { + loginViewModel.logout() + } } private fun refreshSession() { @@ -318,6 +330,7 @@ class MainActivity : AppCompatActivity() { // Pop back stack to prevent showing TOSFragment on pressing back button. navController.popBackStack() navController.navigate(R.id.signInFragment) + if (gPlayLoginRequested) closeAfterLogin = true return@observe } @@ -344,6 +357,10 @@ class MainActivity : AppCompatActivity() { if (gPlayAuthObject == null || gPlayAuthObject.result.isSuccess()) { broadcastGPlayLogin() } + + if (closeAfterLogin && it.isNotEmpty() && it.all { it.result.isSuccess() }) { + finishAndRemoveTask() + } } } diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index 3f8474aa4..63fb6553e 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -35,4 +35,6 @@ object Constants { const val ACTION_PARENTAL_CONTROL_APP_LOUNGE_LOGIN = "${BuildConfig.PACKAGE_NAME_PARENTAL_CONTROL}.action.APP_LOUNGE_LOGIN" + + const val REQUEST_GPLAY_LOGIN = "request_gplay_login" } diff --git a/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt b/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt index 5d99c5f8f..71b69d88d 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt @@ -149,7 +149,7 @@ class AppLoungeDataStore @Inject constructor( } } -fun Flow.getSync(): String { +fun Flow.getSync(): T { return runBlocking { this@getSync.first() } 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 d3976c568..251568ce4 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -86,6 +86,10 @@ class MainActivityViewModel @Inject constructor( var shouldIgnoreSessionError = false + fun getTocStatus(): Boolean { + return appLoungeDataStore.tocStatus.getSync() + } + fun getUser(): User { return appLoungeDataStore.getUserType() } -- GitLab From b9c14ef9724bffa160837d79a030aa87a713dbb6 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 10 Jun 2024 15:46:46 +0600 Subject: [PATCH 26/64] refactor: fix the bug where the ratings from GitLab were not being set --- .../GooglePlayContentRatingsRepository.kt | 19 +++++++++++-------- .../workmanager/AppInstallProcessor.kt | 15 ++++++--------- .../AppInstallProcessorTest.kt | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt index ca544b115..9246609bb 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt @@ -23,7 +23,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class GooglePlayContentRatingsRepository @Inject constructor( +class GooglePlayContentRatingsRepository +@Inject +constructor( private val downloadManager: DownloadManager, private val googlePlayContentRatingParser: GooglePlayContentRatingParser ) { @@ -35,18 +37,19 @@ class GooglePlayContentRatingsRepository @Inject constructor( companion object { private const val CONTENT_RATINGS_FILE_URL = "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + - "content_ratings.json?ref_type=heads&inline=false" + "content_ratings.json?ref_type=heads&inline=false" private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" } fun fetchContentRatingData() { downloadManager.downloadFileInCache( - CONTENT_RATINGS_FILE_URL, - fileName = CONTENT_RATINGS_FILE_NAME - ) { success, _ -> - if (success) { - googlePlayContentRatingParser.parseContentRatingData() + CONTENT_RATINGS_FILE_URL, fileName = CONTENT_RATINGS_FILE_NAME) { success, _ -> + _contentRatingGroups = + if (success) { + googlePlayContentRatingParser.parseContentRatingData() + } else { + emptyList() + } } - } } } diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index 0e3880b67..c12733383 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -33,9 +33,9 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError -import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.download.DownloadManagerUtils import foundation.e.apps.install.notification.StorageNotificationManager @@ -56,7 +56,7 @@ class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, private val appInstallComponents: AppInstallComponents, private val applicationRepository: ApplicationRepository, - private val getAppInstallationPermissionUseCaseImpl: GetAppInstallationPermissionUseCaseImpl, + private val getAppInstallationPermissionUseCase: GetAppInstallationPermissionUseCase, private val dataStoreManager: DataStoreManager, private val storageNotificationManager: StorageNotificationManager, ) { @@ -138,25 +138,22 @@ class AppInstallProcessor @Inject constructor( val installationPermission = - getAppInstallationPermissionUseCaseImpl.invoke(appInstall) + getAppInstallationPermissionUseCase.invoke(appInstall) when (installationPermission) { Allowed -> { - Timber.tag("Parental control") - .i("${appInstall.name} is allowed to be installed.") + Timber.i("${appInstall.name} is allowed to be installed.") // no operation, allow installation } Denied -> { - Timber.tag("Parental control") - .i("${appInstall.name} can't be installed because of parental control setting.") + Timber.i("${appInstall.name} can't be installed because of parental control setting.") EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(appInstall.name)) appInstallComponents.appManagerWrapper.cancelDownload(appInstall) return } DeniedOnDataLoadError -> { - Timber.tag("Parental control") - .i("${appInstall.name} can't be installed because of unavailable data.") + Timber.i("${appInstall.name} can't be installed because of unavailable data.") EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) appInstallComponents.appManagerWrapper.cancelDownload(appInstall) return diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt index 4895aa527..5fa855884 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -29,7 +29,7 @@ import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState import foundation.e.apps.data.preference.DataStoreManager -import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl +import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.workmanager.AppInstallProcessor @@ -79,7 +79,7 @@ class AppInstallProcessorTest { private lateinit var appInstallProcessor: AppInstallProcessor @Mock - private lateinit var getAppInstallationPermissionUseCase: GetAppInstallationPermissionUseCaseImpl + private lateinit var getAppInstallationPermissionUseCase: GetAppInstallationPermissionUseCase @Mock private lateinit var storageNotificationManager: StorageNotificationManager -- GitLab From c3ecebe615104aada29ffa026822d29d836b1802 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 10 Jun 2024 16:06:22 +0530 Subject: [PATCH 27/64] move gplay login request to a different method --- app/src/main/java/foundation/e/apps/MainActivity.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index eda010784..d7b0129ae 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -131,7 +131,17 @@ class MainActivity : AppCompatActivity() { observeEvents() - gPlayLoginRequested = intent.getBooleanExtra(Constants.REQUEST_GPLAY_LOGIN, false) + checkGPlayLoginRequest(intent) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + checkGPlayLoginRequest(intent) + } + + private fun checkGPlayLoginRequest(intent: Intent?) { + gPlayLoginRequested = + intent?.getBooleanExtra(Constants.REQUEST_GPLAY_LOGIN, false) ?: false if (!gPlayLoginRequested) return if (!viewModel.getTocStatus()) return -- GitLab From 6eae986ed62b62cb61e99fcee022985e71debf0b Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 10 Jun 2024 19:13:52 +0600 Subject: [PATCH 28/64] refactor: merge 2203-paco branch [skip-ci] --- .../e/apps/data/ageRating/AgeGroupApi.kt | 35 ----------- .../e/apps/data/cleanapk/RetrofitApiModule.kt | 7 +-- .../apps/data/parentalcontrol/AgeGroupApi.kt | 35 +++++++++++ ...GetAppInstallationPermissionUseCaseImpl.kt | 6 +- .../GooglePlayContentRatingGroup.kt | 4 +- .../GooglePlayContentRatingsRepository.kt | 36 ++++------- .../e/apps/provider/AgeRatingProvider.kt | 60 +++++++++++-------- .../e/apps/provider/ProviderConstants.kt | 25 ++++---- 8 files changed, 100 insertions(+), 108 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt create mode 100644 app/src/main/java/foundation/e/apps/data/parentalcontrol/AgeGroupApi.kt diff --git a/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt b/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt deleted file mode 100644 index 48952e1cd..000000000 --- a/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * 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.data.ageRating - -import foundation.e.apps.data.blockedApps.ContentRatingGroup -import retrofit2.Response -import retrofit2.http.GET - -interface AgeGroupApi { - - companion object { - val BASE_URL = "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" - } - - @GET("content_ratings.json?ref_type=heads") - suspend fun getDefinedAgeGroups(): Response> - -} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitApiModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitApiModule.kt index cd909b568..adbd10209 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitApiModule.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitApiModule.kt @@ -25,20 +25,15 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.cleanapk.NetworkModule.getYamlFactory -import foundation.e.apps.data.ageRating.AgeGroupApi -import foundation.e.apps.data.cleanapk.data.app.Application +import foundation.e.apps.data.parentalcontrol.AgeGroupApi import foundation.e.apps.data.ecloud.EcloudApiInterface import foundation.e.apps.data.exodus.ExodusTrackerApi import foundation.e.apps.data.fdroid.FdroidApiInterface -import okhttp3.Cache -import okhttp3.Interceptor import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory -import java.util.Locale -import java.util.concurrent.TimeUnit import javax.inject.Named import javax.inject.Singleton diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/AgeGroupApi.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/AgeGroupApi.kt new file mode 100644 index 000000000..7e727318e --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/AgeGroupApi.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 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.data.parentalcontrol + +import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingGroup +import retrofit2.Response +import retrofit2.http.GET + +interface AgeGroupApi { + + companion object { + const val BASE_URL = + "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + } + + @GET("content_ratings.json?ref_type=heads") + suspend fun getDefinedAgeGroups(): Response> + +} diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt index 9006b9242..36ca4a324 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt @@ -101,13 +101,17 @@ constructor( return !verifyContentRatingExists(app, authData) } - private fun hasValidContentRating( + private suspend fun hasValidContentRating( app: AppInstall, parentalControlState: AgeGroup, ): Boolean { return when { app.contentRating.id.isBlank() -> false else -> { + if (googlePlayContentRatingsRepository.contentRatingGroups.isEmpty()) { + googlePlayContentRatingsRepository.fetchContentRatingData() + } + val allowedContentRatingGroup = googlePlayContentRatingsRepository.contentRatingGroups.find { it.id == parentalControlState.ageGroup.name diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt index 6b59d5ba2..8a0362cfb 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt @@ -18,11 +18,11 @@ package foundation.e.apps.data.parentalcontrol.gplayrating -import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json data class GooglePlayContentRatingGroup( val id: String, - @SerializedName("age_group") + @Json(name = "age_group") val ageGroup: String, var ratings: List ) diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt index 9246609bb..8925d3048 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt @@ -18,38 +18,24 @@ package foundation.e.apps.data.parentalcontrol.gplayrating -import foundation.e.apps.data.DownloadManager +import foundation.e.apps.data.parentalcontrol.AgeGroupApi import javax.inject.Inject import javax.inject.Singleton @Singleton -class GooglePlayContentRatingsRepository -@Inject -constructor( - private val downloadManager: DownloadManager, - private val googlePlayContentRatingParser: GooglePlayContentRatingParser -) { +class GooglePlayContentRatingsRepository @Inject constructor(private val ageGroupApi: AgeGroupApi) { private var _contentRatingGroups = listOf() val contentRatingGroups: List - get() = _contentRatingGroups + get() = + _contentRatingGroups.map { ratingGroup -> + ratingGroup.copy(ratings = ratingGroup.ratings.map { rating -> rating.lowercase() }) + } // Ratings need to be converted to lowercase - companion object { - private const val CONTENT_RATINGS_FILE_URL = - "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + - "content_ratings.json?ref_type=heads&inline=false" - private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" - } - - fun fetchContentRatingData() { - downloadManager.downloadFileInCache( - CONTENT_RATINGS_FILE_URL, fileName = CONTENT_RATINGS_FILE_NAME) { success, _ -> - _contentRatingGroups = - if (success) { - googlePlayContentRatingParser.parseContentRatingData() - } else { - emptyList() - } - } + suspend fun fetchContentRatingData() { + val response = ageGroupApi.getDefinedAgeGroups() + if (response.isSuccessful) { + _contentRatingGroups = response.body() ?: emptyList() + } } } diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 856d8d9c4..66c67d7ad 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -1,19 +1,18 @@ /* - * Copyright MURENA SAS 2024 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 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. + * 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 . + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * */ @@ -30,13 +29,15 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.blockedApps.ContentRatingsRepository import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError +import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository import foundation.e.apps.data.playstore.PlayStoreRepository import foundation.e.apps.data.preference.DataStoreManager -import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase import foundation.e.apps.install.pkg.AppLoungePackageManager import foundation.e.apps.provider.ProviderConstants.Companion.AUTHORITY import foundation.e.apps.provider.ProviderConstants.Companion.LOGIN_TYPE @@ -58,8 +59,8 @@ class AgeRatingProvider : ContentProvider() { fun getPlayStoreRepository(): PlayStoreRepository fun getApplicationRepository(): ApplicationRepository fun getPackageManager(): AppLoungePackageManager - fun getContentRatingsRepository(): ContentRatingsRepository - fun getValidateAppAgeLimitUseCase(): ValidateAppAgeLimitUseCase + fun getContentRatingsRepository(): GooglePlayContentRatingsRepository + fun getValidateAppAgeLimitUseCase(): GetAppInstallationPermissionUseCase fun getDataStoreManager(): DataStoreManager } @@ -67,8 +68,8 @@ class AgeRatingProvider : ContentProvider() { private lateinit var playStoreRepository: PlayStoreRepository private lateinit var applicationRepository: ApplicationRepository private lateinit var appLoungePackageManager: AppLoungePackageManager - private lateinit var contentRatingsRepository: ContentRatingsRepository - private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase + private lateinit var contentRatingsRepository: GooglePlayContentRatingsRepository + private lateinit var getAppInstallationPermissionUseCase: GetAppInstallationPermissionUseCase private lateinit var dataStoreManager: DataStoreManager private val CODE_LOGIN_TYPE = 1 @@ -124,15 +125,22 @@ class AgeRatingProvider : ContentProvider() { packageName = packageName, origin = Origin.GPLAY ) - val validateResult = validateAppAgeLimitUseCase(fakeAppInstall) - validateResult.data ?: false + val appInstallationPermissionState = + getAppInstallationPermissionUseCase(fakeAppInstall) + appInstallationPermissionState } } val contentsRatings = contentRatingsDeferred.awaitAll() - contentsRatings.forEachIndexed { index: Int, isValid: Boolean? -> - if (isValid == false) { - // Collect package names for blocklist - cursor.addRow(arrayOf(packagesNames[index])) + contentsRatings.forEachIndexed { index: Int, permission -> + when (permission) { + is Denied, DeniedOnDataLoadError -> { + // Collect package names for blocklist + cursor.addRow(arrayOf(packagesNames[index])) + } + + else -> { + // no-op + } } } } @@ -150,7 +158,7 @@ class AgeRatingProvider : ContentProvider() { applicationRepository = hiltEntryPoint.getApplicationRepository() appLoungePackageManager = hiltEntryPoint.getPackageManager() contentRatingsRepository = hiltEntryPoint.getContentRatingsRepository() - validateAppAgeLimitUseCase = hiltEntryPoint.getValidateAppAgeLimitUseCase() + getAppInstallationPermissionUseCase = hiltEntryPoint.getValidateAppAgeLimitUseCase() dataStoreManager = hiltEntryPoint.getDataStoreManager() @@ -182,4 +190,4 @@ class AgeRatingProvider : ContentProvider() { throw UnsupportedOperationException("Not supported") } -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt index 0af7a9d06..fe76e9040 100644 --- a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt +++ b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt @@ -1,19 +1,18 @@ /* - * Copyright MURENA SAS 2024 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 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. + * 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 . + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * */ @@ -30,4 +29,4 @@ class ProviderConstants { const val PATH_LOGIN_TYPE = "login_type" const val PATH_AGE_RATINGS = "age_ratings" } -} \ No newline at end of file +} -- GitLab From 26e1d47ea385e81bcf131626fe911dfa41089fea Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 10 Jun 2024 19:56:14 +0600 Subject: [PATCH 29/64] refactor: remove unneeded content rating fetching Updated app installation permission handling in AgeRatingProvider [skip-ci] --- .../GetAppInstallationPermissionUseCaseImpl.kt | 6 +----- .../java/foundation/e/apps/provider/AgeRatingProvider.kt | 5 ++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt index 36ca4a324..9006b9242 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt @@ -101,17 +101,13 @@ constructor( return !verifyContentRatingExists(app, authData) } - private suspend fun hasValidContentRating( + private fun hasValidContentRating( app: AppInstall, parentalControlState: AgeGroup, ): Boolean { return when { app.contentRating.id.isBlank() -> false else -> { - if (googlePlayContentRatingsRepository.contentRatingGroups.isEmpty()) { - googlePlayContentRatingsRepository.fetchContentRatingData() - } - val allowedContentRatingGroup = googlePlayContentRatingsRepository.contentRatingGroups.find { it.id == parentalControlState.ageGroup.name diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 66c67d7ad..f91a67ba8 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -32,6 +32,7 @@ import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository @@ -138,9 +139,11 @@ class AgeRatingProvider : ContentProvider() { cursor.addRow(arrayOf(packagesNames[index])) } - else -> { + Allowed -> { // no-op } + + else -> error("Invalid application permission state.") } } } -- GitLab From 72c352c2d09e4a024ea9a15cdf89b685f5b72eb4 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 10 Jun 2024 21:12:52 +0530 Subject: [PATCH 30/64] Use new gplayapi method to directly get ratings in english --- app/build.gradle | 2 +- .../e/apps/data/application/apps/AppsApi.kt | 3 ++ .../apps/data/application/apps/AppsApiImpl.kt | 7 ++++ .../data/playstore/PlayStoreRepository.kt | 4 ++ .../data/playstore/PlayStoreRepositoryImpl.kt | 9 ++++ .../apps/domain/ValidateAppAgeLimitUseCase.kt | 42 ++++--------------- 6 files changed, 33 insertions(+), 34 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ee9b5dac6..3f2f48ee7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,7 +158,7 @@ dependencies { api files('libs/splitinstall-lib.jar') implementation 'foundation.e.lib:telemetry:0.0.11-alpha' - implementation "foundation.e:gplayapi:3.2.10-3" + implementation "foundation.e:gplayapi:3.2.10-4" implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.5.6' diff --git a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt index 1a5ebfa36..fd62842cf 100644 --- a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt +++ b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt @@ -19,6 +19,7 @@ package foundation.e.apps.data.application.apps import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.FilterLevel import foundation.e.apps.data.enums.Origin @@ -66,4 +67,6 @@ interface AppsApi { fun isAnyAppInstallStatusChanged(currentList: List): Boolean fun isOpenSourceSelected(): Boolean + + suspend fun getEnglishContentRating(packageName: String): ContentRating? } diff --git a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt index 62876de4b..9a26fd075 100644 --- a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt @@ -21,6 +21,7 @@ package foundation.e.apps.data.application.apps import android.content.Context import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.ContentRating import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.AppSourcesContainer import foundation.e.apps.data.application.ApplicationDataManager @@ -265,4 +266,10 @@ class AppsApiImpl @Inject constructor( } override fun isOpenSourceSelected() = appLoungePreference.isOpenSourceSelected() + + override suspend fun getEnglishContentRating(packageName: String): ContentRating? { + return handleNetworkResult { + appSources.gplayRepo.getEnglishContentRating(packageName) + }.data + } } diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt index 0239ce1f3..5a0dbc61f 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt @@ -49,4 +49,8 @@ interface PlayStoreRepository : StoreRepository { appPackage: String, contentRating: ContentRating ): ContentRating + + suspend fun getEnglishContentRating( + appPackage: String, + ): ContentRating } diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt index da8847721..c95fdfbaf 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt @@ -228,4 +228,13 @@ class PlayStoreRepositoryImpl @Inject constructor( ) } } + + override suspend fun getEnglishContentRating(appPackage: String): ContentRating { + val authData = authenticatorRepository.gplayAuth!! + val contentRatingHelper = ContentRatingHelper(authData) + + return withContext(Dispatchers.IO) { + contentRatingHelper.getEnglishContentRating(appPackage) + } + } } diff --git a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index 6e4d3195f..0716a9b83 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -18,35 +18,28 @@ package foundation.e.apps.domain -import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.apps.AppsApi import foundation.e.apps.data.blockedApps.Age import foundation.e.apps.data.blockedApps.ContentRatingGroup import foundation.e.apps.data.blockedApps.ContentRatingsRepository import foundation.e.apps.data.blockedApps.ParentalControlRepository -import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.playstore.PlayStoreRepository -import foundation.e.apps.data.preference.DataStoreManager import timber.log.Timber import javax.inject.Inject class ValidateAppAgeLimitUseCase @Inject constructor( - private val applicationRepository: ApplicationRepository, - private val dataStoreManager: DataStoreManager, private val contentRatingRepository: ContentRatingsRepository, private val parentalControlRepository: ParentalControlRepository, - private val playStoreRepository: PlayStoreRepository + private val appsApi: AppsApi, ) { suspend operator fun invoke(app: AppInstall): ResultSupreme { - val authData = dataStoreManager.getAuthData() val ageGroup = parentalControlRepository.getSelectedAgeGroup() return when { isParentalControlDisabled(ageGroup) -> ResultSupreme.Success(data = true) - hasNoContentRating(app, authData) -> ResultSupreme.Error(data = false) + hasNoContentRating(app) -> ResultSupreme.Error(data = false) else -> validateAgeLimit(ageGroup, app) } } @@ -67,8 +60,8 @@ class ValidateAppAgeLimitUseCase @Inject constructor( return ResultSupreme.Success(isValidAppAgeRating(app, allowedContentRating)) } - private suspend fun hasNoContentRating(app: AppInstall, authData: AuthData) = - !verifyContentRatingExists(app, authData) + private suspend fun hasNoContentRating(app: AppInstall) = + !verifyContentRatingExists(app) private fun isValidAppAgeRating( app: AppInstall, @@ -80,29 +73,12 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private fun isParentalControlDisabled(ageGroup: Age) = ageGroup == Age.PARENTAL_CONTROL_DISABLED - private suspend fun verifyContentRatingExists( - app: AppInstall, - authData: AuthData - ): Boolean { - if (app.contentRating.title.isEmpty()) { - applicationRepository - .getApplicationDetails( - app.id, app.packageName, authData, app.origin - ).let { (appDetails, resultStatus) -> - if (resultStatus == ResultStatus.OK) { - app.contentRating = appDetails.contentRating - } else { - return false - } - } - } + private suspend fun verifyContentRatingExists(app: AppInstall): Boolean { if (app.contentRating.id.isEmpty()) { - app.contentRating = - playStoreRepository.getContentRatingWithId( - app.packageName, - app.contentRating - ) + appsApi.getEnglishContentRating(app.packageName)?.run { + app.contentRating = this + } } return app.contentRating.title.isNotEmpty() && -- GitLab From 3f7f817f583d865b6bada1c15a6bdb2b1d224e0d Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 14:26:32 +0530 Subject: [PATCH 31/64] change uri path of blocklist --- .../main/java/foundation/e/apps/provider/AgeRatingProvider.kt | 4 ++-- .../main/java/foundation/e/apps/provider/ProviderConstants.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 856d8d9c4..c6b25a2d6 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -41,7 +41,7 @@ import foundation.e.apps.install.pkg.AppLoungePackageManager import foundation.e.apps.provider.ProviderConstants.Companion.AUTHORITY import foundation.e.apps.provider.ProviderConstants.Companion.LOGIN_TYPE import foundation.e.apps.provider.ProviderConstants.Companion.PACKAGE_NAME -import foundation.e.apps.provider.ProviderConstants.Companion.PATH_AGE_RATINGS +import foundation.e.apps.provider.ProviderConstants.Companion.PATH_BLOCKLIST import foundation.e.apps.provider.ProviderConstants.Companion.PATH_LOGIN_TYPE import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async @@ -77,7 +77,7 @@ class AgeRatingProvider : ContentProvider() { private val uriMatcher by lazy { UriMatcher(UriMatcher.NO_MATCH).apply { addURI(AUTHORITY, PATH_LOGIN_TYPE, CODE_LOGIN_TYPE) - addURI(AUTHORITY, PATH_AGE_RATINGS, CODE_AGE_RATING) + addURI(AUTHORITY, PATH_BLOCKLIST, CODE_AGE_RATING) } } diff --git a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt index 0af7a9d06..c90f8cfda 100644 --- a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt +++ b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt @@ -28,6 +28,6 @@ class ProviderConstants { const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider" const val PATH_LOGIN_TYPE = "login_type" - const val PATH_AGE_RATINGS = "age_ratings" + const val PATH_BLOCKLIST = "block_list" } } \ No newline at end of file -- GitLab From e2d2b61e15f22a88b81a17ef7d2e8f9705cc7576 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 14:31:12 +0530 Subject: [PATCH 32/64] set protection level to content provider permission --- app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6f9bb1e64..e54c69130 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,7 +55,7 @@ - + Date: Tue, 11 Jun 2024 16:57:35 +0600 Subject: [PATCH 33/64] refactor: add method to fetch English content rating from Play Store This commit introduces a new method in the PlayStoreRepository to fetch the English content rating for a given app package. It also updates the GetAppInstallationPermissionUseCase to use this new method for validating content ratings. This ensures that the content rating is always available in English, regardless of the user's locale. Additionally, it updates the gplayapi library version and corrects some test cases related to the changes made. [skip-ci] --- app/build.gradle | 2 +- .../e/apps/data/cleanapk/CleanApkRetrofit.kt | 4 +-- ...GetAppInstallationPermissionUseCaseImpl.kt | 32 ++++--------------- .../data/playstore/PlayStoreRepository.kt | 4 +++ .../data/playstore/PlayStoreRepositoryImpl.kt | 10 ++++++ ...GetAppInstallationPermissionUseCaseTest.kt | 17 +++++----- 6 files changed, 32 insertions(+), 37 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f6ab12791..594f95f23 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,7 +158,7 @@ dependencies { api files('libs/splitinstall-lib.jar') implementation 'foundation.e.lib:telemetry:0.0.11-alpha' - implementation "foundation.e:gplayapi:3.2.10-3" + implementation "foundation.e:gplayapi:3.2.10-4" implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.5.6' diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt index 7c0ec7dde..1d48cf6d0 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt @@ -35,8 +35,8 @@ interface CleanApkRetrofit { const val ASSET_URL = "https://api.cleanapk.org/v2/media/" // TODO: Configure dev and prod flavors to auto switch URLs - // const val BASE_URL_DEV = "https://api.dev.cleanapk.org/v2/" - // const val ASSET_URL_DEV = "https://api.dev.cleanapk.org/v2/media/" +// const val BASE_URL = "https://api.dev.cleanapk.org/v2/" +// const val ASSET_URL = "https://api.dev.cleanapk.org/v2/media/" // Application sources const val APP_SOURCE_FOSS = "open" diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt index 9006b9242..e51521036 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt @@ -18,9 +18,6 @@ package foundation.e.apps.data.parentalcontrol -import com.aurora.gplayapi.data.models.AuthData -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed @@ -28,7 +25,6 @@ import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Den import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository import foundation.e.apps.data.playstore.PlayStoreRepository -import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState @@ -40,8 +36,6 @@ import javax.inject.Inject class GetAppInstallationPermissionUseCaseImpl @Inject constructor( - private val applicationRepository: ApplicationRepository, - private val dataStoreManager: DataStoreManager, private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val getParentalControlStateUseCase: GetParentalControlStateUseCase, private val playStoreRepository: PlayStoreRepository @@ -94,11 +88,12 @@ constructor( return !isFDroidApp(app) && app.type != Type.PWA } - private fun isFDroidApp(app: AppInstall): Boolean = app.isFDroidApp + private fun isFDroidApp(app: AppInstall): Boolean { + return app.isFDroidApp // FIXME: From search results, isFDroidApp is absent, so false always + } private suspend fun hasNoContentRating(app: AppInstall): Boolean { - val authData = dataStoreManager.getAuthData() - return !verifyContentRatingExists(app, authData) + return !verifyContentRatingExists(app) } private fun hasValidContentRating( @@ -118,22 +113,9 @@ constructor( } } - private suspend fun verifyContentRatingExists(app: AppInstall, authData: AuthData): Boolean { - if (app.contentRating.title.isEmpty()) { - applicationRepository - .getApplicationDetails(app.id, app.packageName, authData, app.origin) - .let { (appDetails, resultStatus) -> - if (resultStatus == ResultStatus.OK) { - app.contentRating = appDetails.contentRating - } else { - return false - } - } - } - - if (app.contentRating.id.isEmpty()) { - app.contentRating = - playStoreRepository.getContentRatingWithId(app.packageName, app.contentRating) + private suspend fun verifyContentRatingExists(app: AppInstall): Boolean { + if (app.contentRating.title.isEmpty() || app.contentRating.id.isEmpty()) { + app.contentRating = playStoreRepository.getEnglishContentRating(app.packageName) } return app.contentRating.title.isNotEmpty() && app.contentRating.id.isNotEmpty() diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt index 0239ce1f3..5a0dbc61f 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt @@ -49,4 +49,8 @@ interface PlayStoreRepository : StoreRepository { appPackage: String, contentRating: ContentRating ): ContentRating + + suspend fun getEnglishContentRating( + appPackage: String, + ): ContentRating } diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt index 9c1703cb8..2fd2765e6 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt @@ -228,4 +228,14 @@ class PlayStoreRepositoryImpl @Inject constructor( ) } } + + override suspend fun getEnglishContentRating(appPackage: String): ContentRating { + val authData = authenticatorRepository.gplayAuth!! + val contentRatingHelper = ContentRatingHelper(authData) + + return withContext(Dispatchers.IO) { + runCatching { contentRatingHelper.getEnglishContentRating(appPackage) } + .getOrDefault(ContentRating()) + } + } } diff --git a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt index 7f46db251..8855cfec0 100644 --- a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -39,7 +39,6 @@ import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState import foundation.e.apps.util.MainCoroutineRule -import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -48,6 +47,7 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +import kotlin.test.assertEquals class GetAppInstallationPermissionUseCaseTest { @@ -76,11 +76,10 @@ class GetAppInstallationPermissionUseCaseTest { MockitoAnnotations.openMocks(this) useCase = GetAppInstallationPermissionUseCaseImpl( - applicationRepository, - dataStoreManager, contentRatingsRepository, getParentalControlStateUseCase, - playStoreRepository) + playStoreRepository + ) } @Test @@ -228,7 +227,7 @@ class GetAppInstallationPermissionUseCaseTest { appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.OK)) - Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + Mockito.`when`(playStoreRepository.getEnglishContentRating(appPackage)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) @@ -288,7 +287,7 @@ class GetAppInstallationPermissionUseCaseTest { appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.OK)) - Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + Mockito.`when`(playStoreRepository.getEnglishContentRating(appPackage)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) @@ -348,7 +347,7 @@ class GetAppInstallationPermissionUseCaseTest { appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.OK)) - Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + Mockito.`when`(playStoreRepository.getEnglishContentRating(appPackage)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) @@ -408,7 +407,7 @@ class GetAppInstallationPermissionUseCaseTest { appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.UNKNOWN)) - Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + Mockito.`when`(playStoreRepository.getEnglishContentRating(appPackage)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) @@ -469,7 +468,7 @@ class GetAppInstallationPermissionUseCaseTest { appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.UNKNOWN)) - Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + Mockito.`when`(playStoreRepository.getEnglishContentRating(appPackage)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) -- GitLab From a2e2a7de8757b8123fe1beb373bd4b47929e425a Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 16:40:50 +0530 Subject: [PATCH 34/64] move getEnglishContentRating to ContentRatingsRepository --- .../e/apps/data/application/apps/AppsApi.kt | 2 -- .../e/apps/data/application/apps/AppsApiImpl.kt | 6 ------ .../data/blockedApps/ContentRatingsRepository.kt | 14 ++++++++++++++ .../e/apps/data/playstore/PlayStoreRepository.kt | 4 ---- .../apps/data/playstore/PlayStoreRepositoryImpl.kt | 9 --------- .../e/apps/domain/ValidateAppAgeLimitUseCase.kt | 3 +-- 6 files changed, 15 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt index fd62842cf..72f5eb4d6 100644 --- a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt +++ b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt @@ -67,6 +67,4 @@ interface AppsApi { fun isAnyAppInstallStatusChanged(currentList: List): Boolean fun isOpenSourceSelected(): Boolean - - suspend fun getEnglishContentRating(packageName: String): ContentRating? } diff --git a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt index 9a26fd075..e4847bece 100644 --- a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt @@ -266,10 +266,4 @@ class AppsApiImpl @Inject constructor( } override fun isOpenSourceSelected() = appLoungePreference.isOpenSourceSelected() - - override suspend fun getEnglishContentRating(packageName: String): ContentRating? { - return handleNetworkResult { - appSources.gplayRepo.getEnglishContentRating(packageName) - }.data - } } diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 09e2fea8e..1f8ded4cf 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -19,13 +19,18 @@ package foundation.e.apps.data.blockedApps +import com.aurora.gplayapi.data.models.ContentRating +import com.aurora.gplayapi.helpers.ContentRatingHelper import foundation.e.apps.data.ageRating.AgeGroupApi +import foundation.e.apps.data.handleNetworkResult +import foundation.e.apps.data.login.AuthenticatorRepository import javax.inject.Inject import javax.inject.Singleton @Singleton class ContentRatingsRepository @Inject constructor( private val ageGroupApi: AgeGroupApi, + private val authenticatorRepository: AuthenticatorRepository, ) { private var _contentRatingGroups = listOf() @@ -38,4 +43,13 @@ class ContentRatingsRepository @Inject constructor( _contentRatingGroups = response.body() ?: emptyList() } } + + suspend fun getEnglishContentRating(packageName: String): ContentRating? { + val authData = authenticatorRepository.gplayAuth!! + val contentRatingHelper = ContentRatingHelper(authData) + + return handleNetworkResult { + contentRatingHelper.getEnglishContentRating(packageName) + }.data + } } diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt index 5a0dbc61f..0239ce1f3 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt @@ -49,8 +49,4 @@ interface PlayStoreRepository : StoreRepository { appPackage: String, contentRating: ContentRating ): ContentRating - - suspend fun getEnglishContentRating( - appPackage: String, - ): ContentRating } diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt index c95fdfbaf..da8847721 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt @@ -228,13 +228,4 @@ class PlayStoreRepositoryImpl @Inject constructor( ) } } - - override suspend fun getEnglishContentRating(appPackage: String): ContentRating { - val authData = authenticatorRepository.gplayAuth!! - val contentRatingHelper = ContentRatingHelper(authData) - - return withContext(Dispatchers.IO) { - contentRatingHelper.getEnglishContentRating(appPackage) - } - } } diff --git a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index 0716a9b83..5094fa4e3 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -31,7 +31,6 @@ import javax.inject.Inject class ValidateAppAgeLimitUseCase @Inject constructor( private val contentRatingRepository: ContentRatingsRepository, private val parentalControlRepository: ParentalControlRepository, - private val appsApi: AppsApi, ) { suspend operator fun invoke(app: AppInstall): ResultSupreme { @@ -76,7 +75,7 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private suspend fun verifyContentRatingExists(app: AppInstall): Boolean { if (app.contentRating.id.isEmpty()) { - appsApi.getEnglishContentRating(app.packageName)?.run { + contentRatingRepository.getEnglishContentRating(app.packageName)?.run { app.contentRating = this } } -- GitLab From 8d36255e04273484bc162a54b01c6e82705cffcf Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 16:43:08 +0530 Subject: [PATCH 35/64] move di for AgeGroupApi to a different module class --- .../e/apps/data/cleanapk/RetrofitModule.kt | 11 ----- .../foundation/e/apps/di/AgeRatingModule.kt | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt index 28c5ea449..3a2d832d8 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt @@ -126,17 +126,6 @@ object RetrofitModule { .create(EcloudApiInterface::class.java) } - @Singleton - @Provides - fun provideAgeGroupApi(okHttpClient: OkHttpClient, moshi: Moshi): AgeGroupApi { - return Retrofit.Builder() - .baseUrl(AgeGroupApi.BASE_URL) - .client(okHttpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - .create(AgeGroupApi::class.java) - } - @Singleton @Provides fun getMoshi(): Moshi { diff --git a/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt b/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt new file mode 100644 index 000000000..1cb262678 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright MURENA SAS 2024 + * 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.di + +import com.squareup.moshi.Moshi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.ageRating.AgeGroupApi +import javax.inject.Singleton +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + +@Module +@InstallIn(SingletonComponent::class) +object AgeRatingModule { + + @Singleton + @Provides + fun provideAgeGroupApi(okHttpClient: OkHttpClient, moshi: Moshi): AgeGroupApi { + return Retrofit.Builder() + .baseUrl(AgeGroupApi.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(AgeGroupApi::class.java) + } +} -- GitLab From cf292257f09cf7f9afa3b8697fbda28f19e4c8de Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 16:43:43 +0530 Subject: [PATCH 36/64] more detekt issues solved --- .../main/java/foundation/e/apps/MainActivity.kt | 2 +- .../e/apps/data/ageRating/AgeGroupApi.kt | 4 ++-- .../e/apps/provider/AgeRatingProvider.kt | 14 +++++++------- .../e/apps/provider/ProviderConstants.kt | 16 +++++++--------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index d7b0129ae..729a46248 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -50,7 +50,7 @@ import foundation.e.apps.data.login.PlayStoreAuthenticator import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.install.updates.UpdatesNotifier -import foundation.e.apps.provider.ProviderConstants.Companion.LOGIN_TYPE +import foundation.e.apps.provider.ProviderConstants.LOGIN_TYPE import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections diff --git a/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt b/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt index 48952e1cd..9a3d04694 100644 --- a/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt +++ b/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt @@ -26,10 +26,10 @@ import retrofit2.http.GET interface AgeGroupApi { companion object { - val BASE_URL = "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + const val BASE_URL = "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" } @GET("content_ratings.json?ref_type=heads") suspend fun getDefinedAgeGroups(): Response> -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index c6b25a2d6..63c8688c1 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -38,11 +38,11 @@ import foundation.e.apps.data.playstore.PlayStoreRepository import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.install.pkg.AppLoungePackageManager -import foundation.e.apps.provider.ProviderConstants.Companion.AUTHORITY -import foundation.e.apps.provider.ProviderConstants.Companion.LOGIN_TYPE -import foundation.e.apps.provider.ProviderConstants.Companion.PACKAGE_NAME -import foundation.e.apps.provider.ProviderConstants.Companion.PATH_BLOCKLIST -import foundation.e.apps.provider.ProviderConstants.Companion.PATH_LOGIN_TYPE +import foundation.e.apps.provider.ProviderConstants.AUTHORITY +import foundation.e.apps.provider.ProviderConstants.LOGIN_TYPE +import foundation.e.apps.provider.ProviderConstants.PACKAGE_NAME +import foundation.e.apps.provider.ProviderConstants.PATH_BLOCKLIST +import foundation.e.apps.provider.ProviderConstants.PATH_LOGIN_TYPE import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -141,7 +141,7 @@ class AgeRatingProvider : ContentProvider() { } override fun onCreate(): Boolean { - val appContext = context?.applicationContext ?: throw IllegalStateException() + val appContext = context?.applicationContext ?: error("Null context in ${this::class.java.name}") val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ContentProviderEntryPoint::class.java) @@ -182,4 +182,4 @@ class AgeRatingProvider : ContentProvider() { throw UnsupportedOperationException("Not supported") } -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt index c90f8cfda..56c692021 100644 --- a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt +++ b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt @@ -21,13 +21,11 @@ package foundation.e.apps.provider import foundation.e.apps.BuildConfig -class ProviderConstants { - companion object { - const val PACKAGE_NAME = "package_name" - const val LOGIN_TYPE = "login_type" +object ProviderConstants { + const val PACKAGE_NAME = "package_name" + const val LOGIN_TYPE = "login_type" - const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider" - const val PATH_LOGIN_TYPE = "login_type" - const val PATH_BLOCKLIST = "block_list" - } -} \ No newline at end of file + const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider" + const val PATH_LOGIN_TYPE = "login_type" + const val PATH_BLOCKLIST = "block_list" +} -- GitLab From f0baa2b05522fd48239846a749939ab590d9b5f4 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 16:56:44 +0530 Subject: [PATCH 37/64] suppress lint error for permission name --- app/src/main/AndroidManifest.xml | 4 ++-- lint.xml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e54c69130..4d730670a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,7 +55,7 @@ - + + android:readPermission="${applicationId}.permission.PROVIDER_READ" /> diff --git a/lint.xml b/lint.xml index bfd91922c..5a6bcbd59 100644 --- a/lint.xml +++ b/lint.xml @@ -47,6 +47,10 @@ + + + + -- GitLab From f6447e2fda1b8c18ec963238d3b116badd11bac3 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 17:10:50 +0530 Subject: [PATCH 38/64] return null if authData is null --- .../e/apps/data/blockedApps/ContentRatingsRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 1f8ded4cf..595904bbe 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -45,7 +45,7 @@ class ContentRatingsRepository @Inject constructor( } suspend fun getEnglishContentRating(packageName: String): ContentRating? { - val authData = authenticatorRepository.gplayAuth!! + val authData = authenticatorRepository.gplayAuth ?: return null val contentRatingHelper = ContentRatingHelper(authData) return handleNetworkResult { -- GitLab From 41dc10dc80b81a392fbad58310d5f688302aede3 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 17:14:00 +0530 Subject: [PATCH 39/64] revert unintentional change --- .../foundation/e/apps/data/login/AuthenticatorRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt index 717c9d7ac..2e4bc761d 100644 --- a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt @@ -78,7 +78,7 @@ class AuthenticatorRepository @Inject constructor( return validateAuthData } - fun getUserType(): User { + private fun getUserType(): User { return loginCommon.getUserType() } } -- GitLab From db0fd7f9281641008f123482d94156a46527c1ca Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 17:19:58 +0530 Subject: [PATCH 40/64] rename hiltentrypoint methods and remove some methods --- .../e/apps/provider/AgeRatingProvider.kt | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 63c8688c1..c92916293 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -29,12 +29,10 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent -import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.blockedApps.ContentRatingsRepository import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository -import foundation.e.apps.data.playstore.PlayStoreRepository import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.install.pkg.AppLoungePackageManager @@ -54,18 +52,14 @@ class AgeRatingProvider : ContentProvider() { @EntryPoint @InstallIn(SingletonComponent::class) interface ContentProviderEntryPoint { - fun getAuthenticationRepository(): AuthenticatorRepository - fun getPlayStoreRepository(): PlayStoreRepository - fun getApplicationRepository(): ApplicationRepository - fun getPackageManager(): AppLoungePackageManager - fun getContentRatingsRepository(): ContentRatingsRepository - fun getValidateAppAgeLimitUseCase(): ValidateAppAgeLimitUseCase - fun getDataStoreManager(): DataStoreManager + fun provideAuthenticationRepository(): AuthenticatorRepository + fun providePackageManager(): AppLoungePackageManager + fun provideContentRatingsRepository(): ContentRatingsRepository + fun provideValidateAppAgeLimitUseCase(): ValidateAppAgeLimitUseCase + fun provideDataStoreManager(): DataStoreManager } private lateinit var authenticatorRepository: AuthenticatorRepository - private lateinit var playStoreRepository: PlayStoreRepository - private lateinit var applicationRepository: ApplicationRepository private lateinit var appLoungePackageManager: AppLoungePackageManager private lateinit var contentRatingsRepository: ContentRatingsRepository private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase @@ -145,13 +139,11 @@ class AgeRatingProvider : ContentProvider() { val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ContentProviderEntryPoint::class.java) - authenticatorRepository = hiltEntryPoint.getAuthenticationRepository() - playStoreRepository = hiltEntryPoint.getPlayStoreRepository() - applicationRepository = hiltEntryPoint.getApplicationRepository() - appLoungePackageManager = hiltEntryPoint.getPackageManager() - contentRatingsRepository = hiltEntryPoint.getContentRatingsRepository() - validateAppAgeLimitUseCase = hiltEntryPoint.getValidateAppAgeLimitUseCase() - dataStoreManager = hiltEntryPoint.getDataStoreManager() + authenticatorRepository = hiltEntryPoint.provideAuthenticationRepository() + appLoungePackageManager = hiltEntryPoint.providePackageManager() + contentRatingsRepository = hiltEntryPoint.provideContentRatingsRepository() + validateAppAgeLimitUseCase = hiltEntryPoint.provideValidateAppAgeLimitUseCase() + dataStoreManager = hiltEntryPoint.provideDataStoreManager() return true -- GitLab From 0ea8269fa30bff6297c7dd6fef964bf8d5191f58 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 17:22:35 +0530 Subject: [PATCH 41/64] added TODO --- .../foundation/e/apps/data/preference/AppLoungeDataStore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt b/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt index 71b69d88d..5b22a91a1 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt @@ -125,7 +125,7 @@ class AppLoungeDataStore @Inject constructor( } } - fun getUserType(): User { + fun getUserType(): User { // TODO: Rename this to getUser() return runBlocking { userType.first().run { val userStrings = User.values().map { it.name } -- GitLab From 908d31f1d2b051614a44c2c53933cc5d80553c9e Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 11 Jun 2024 20:10:49 +0600 Subject: [PATCH 42/64] refactor: fetch F-Droid app info before verifying installation - Added new function getAppDetailsById in CleanApkRepository interface and its implementations in CleanApkAppsRepositoryImpl and CleanApkPWARepository - Updated GetAppInstallationPermissionUseCaseImpl to fetch complete app information before validating NSFW anti-feature - Updated Application data class to handle missing isFDroidApp property in search results --- .../apps/data/application/data/Application.kt | 22 ++++++++----- .../cleanapk/CleanApkAppDetailsRetrofit.kt | 33 ++++++++++--------- .../CleanApkAppsRepositoryImpl.kt | 8 +++-- .../repositories/CleanApkPWARepository.kt | 8 +++-- .../repositories/CleanApkRepository.kt | 6 ++-- ...GetAppInstallationPermissionUseCaseImpl.kt | 27 +++++++++++---- 6 files changed, 68 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index 328c53ac6..aa55142aa 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -101,7 +101,8 @@ data class Application( var filterLevel: FilterLevel = FilterLevel.UNKNOWN, var isGplayReplaced: Boolean = false, @SerializedName(value = "on_fdroid") - val isFDroidApp: Boolean = false, + private val isFDroidAppBackingField: Boolean? = null, + val isFDroidApp: Boolean = isFDroidAppBackingField ?: isFDroid(type, origin), var contentRating: ContentRating = ContentRating(), @SerializedName(value = "antifeatures") val antiFeatures: List> = emptyList() @@ -119,14 +120,8 @@ data class Application( } } -val Application.shareUri: Uri - get() = when (type) { - PWA -> Uri.parse(url) - NATIVE -> when { - isFDroidApp -> buildFDroidUri(package_name) - else -> Uri.parse(shareUrl) - } - } +private fun isFDroid(type: Type, origin: Origin) = + (type == NATIVE && origin == Origin.CLEANAPK) private fun buildFDroidUri(packageName: String): Uri { return Uri.Builder() @@ -136,3 +131,12 @@ private fun buildFDroidUri(packageName: String): Uri { .appendPath(packageName) .build() } + +val Application.shareUri: Uri + get() = when (type) { + PWA -> Uri.parse(url) + NATIVE -> when { + isFDroidApp -> buildFDroidUri(package_name) + else -> Uri.parse(shareUrl) + } + } diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkAppDetailsRetrofit.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkAppDetailsRetrofit.kt index 69574481d..a5e162cf2 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkAppDetailsRetrofit.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkAppDetailsRetrofit.kt @@ -1,20 +1,18 @@ /* + * Copyright (C) 2024 MURENA SAS * - * * 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 . + * 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 . * */ @@ -33,4 +31,9 @@ interface CleanApkAppDetailsRetrofit { @Query("architectures") architectures: List? = null, @Query("type") type: String? = null ): Response + + @GET("apps?action=app_detail") + suspend fun getAppDetails( + @Query("id") id: String + ): Application } diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepositoryImpl.kt index e51a82823..6c7f68095 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepositoryImpl.kt @@ -1,6 +1,5 @@ /* - * Copyright MURENA SAS 2023 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 @@ -14,6 +13,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.data.cleanapk.repositories @@ -75,6 +75,10 @@ class CleanApkAppsRepositoryImpl( return cleanApkRetrofit.checkAvailablePackages(packageNames) } + override suspend fun getAppDetailsById(appId: String): Result { + return runCatching { cleanApkAppDetailsRetrofit.getAppDetails(appId) } + } + override suspend fun getAppDetails(packageNameOrId: String): Response { return cleanApkAppDetailsRetrofit.getAppOrPWADetailsByID(packageNameOrId, null, null) } diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkPWARepository.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkPWARepository.kt index 8e8f84f6a..234616142 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkPWARepository.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkPWARepository.kt @@ -1,6 +1,5 @@ /* - * Copyright MURENA SAS 2023 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 @@ -14,6 +13,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.data.cleanapk.repositories @@ -69,6 +69,10 @@ class CleanApkPWARepository( return cleanAPKRetrofit.checkAvailablePackages(packageNames) } + override suspend fun getAppDetailsById(appId: String): Result { + return runCatching { cleanApkAppDetailsRetrofit.getAppDetails(appId) } + } + override suspend fun getAppDetails(packageNameOrId: String): Response { return cleanApkAppDetailsRetrofit.getAppOrPWADetailsByID(packageNameOrId, null, null) } diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkRepository.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkRepository.kt index 2a9388f02..8a12327ce 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkRepository.kt @@ -1,6 +1,5 @@ /* - * Copyright MURENA SAS 2023 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 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 @@ -14,11 +13,13 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.data.cleanapk.repositories import foundation.e.apps.data.StoreRepository +import foundation.e.apps.data.cleanapk.data.app.Application import foundation.e.apps.data.cleanapk.data.categories.Categories import foundation.e.apps.data.cleanapk.data.search.Search import retrofit2.Response @@ -31,4 +32,5 @@ interface CleanApkRepository : StoreRepository { suspend fun getAppsByCategory(category: String, paginationParameter: Any? = null): Response suspend fun getCategories(): Response suspend fun checkAvailablePackages(packageNames: List): Response + suspend fun getAppDetailsById(appId: String): Result } diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt index e51521036..059db6c74 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt @@ -18,6 +18,8 @@ package foundation.e.apps.data.parentalcontrol +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed @@ -32,13 +34,15 @@ import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState.AgeGr import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState.Disabled import foundation.e.apps.domain.parentalcontrol.model.isEnabled import javax.inject.Inject +import javax.inject.Named class GetAppInstallationPermissionUseCaseImpl @Inject constructor( private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val getParentalControlStateUseCase: GetParentalControlStateUseCase, - private val playStoreRepository: PlayStoreRepository + private val playStoreRepository: PlayStoreRepository, + @Named("cleanApkAppsRepository") private val cleanApkRepository: CleanApkRepository ) : GetAppInstallationPermissionUseCase { companion object { @@ -56,10 +60,21 @@ constructor( } } - private fun validateNsfwAntiFeature( - app: AppInstall, + private suspend fun validateNsfwAntiFeature( + appPendingInstallation: AppInstall, parentalControl: ParentalControlState ): AppInstallationPermissionState { + // Need this API call for fetching complete information of F-Droid app. + // `appPendingInstallation` doesn't have complete data when installation is called from + // screens such as search, category, etc. + val app = + cleanApkRepository + .getAppDetailsById(appPendingInstallation.id) + .getOrElse { + return DeniedOnDataLoadError + } + .app + return when { hasNoAntiFeatures(app) -> Allowed isNsfwFDroidApp(app) && parentalControl.isEnabled -> Denied @@ -67,9 +82,9 @@ constructor( } } - private fun hasNoAntiFeatures(app: AppInstall) = app.antiFeatures.isEmpty() + private fun hasNoAntiFeatures(app: Application) = app.antiFeatures.isEmpty() - private fun isNsfwFDroidApp(app: AppInstall) = + private fun isNsfwFDroidApp(app: Application) = app.antiFeatures.any { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } private suspend fun validateGooglePlayContentRating( @@ -89,7 +104,7 @@ constructor( } private fun isFDroidApp(app: AppInstall): Boolean { - return app.isFDroidApp // FIXME: From search results, isFDroidApp is absent, so false always + return app.isFDroidApp } private suspend fun hasNoContentRating(app: AppInstall): Boolean { -- GitLab From e78209f2a5761ae32976a08d62f87a041da11b1a Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 20:15:45 +0530 Subject: [PATCH 43/64] contracts module --- build.gradle | 1 + contracts/.gitignore | 1 + contracts/build.gradle | 48 +++++++++++++++++++ .../e/apps/contracts/ProviderContracts.kt | 31 ++++++++++++ settings.gradle | 1 + 5 files changed, 82 insertions(+) create mode 100644 contracts/.gitignore create mode 100644 contracts/build.gradle create mode 100644 contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt diff --git a/build.gradle b/build.gradle index 4069cac69..d1c2c788a 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ plugins { id "org.jetbrains.kotlin.plugin.allopen" version "1.8.0" id 'androidx.navigation.safeargs' version '2.5.3' apply false id 'io.gitlab.arturbosch.detekt' version '1.23.1' + id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false } allprojects { diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/contracts/build.gradle b/contracts/build.gradle new file mode 100644 index 000000000..43c0b81c2 --- /dev/null +++ b/contracts/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' + id 'maven-publish' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +publishing { + publications { + aar(MavenPublication) { + groupId = 'foundation.e.apps' + artifactId = 'contracts' + version = '1.0.0' + + artifact("$buildDir/libs/${project.name}.jar") + + pom { + name = 'Contracts' + description = 'Constants to be used in App Lounge content provider' + + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } + + repositories { + maven { + name = "GitLab" + url = uri("https://gitlab.e.foundation/api/v4/projects/355/packages/maven") + credentials(HttpHeaderCredentials) { + name = "Job-Token" + value = System.getenv("CI_JOB_TOKEN") + } + authentication { + header(HttpHeaderAuthentication) + } + } + } +} diff --git a/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt b/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt new file mode 100644 index 000000000..05838bc5d --- /dev/null +++ b/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt @@ -0,0 +1,31 @@ +/* + * Copyright MURENA SAS 2024 + * 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.contracts + +object ProviderContracts { + const val COLUMN_PACKAGE_NAME = "package_name" + const val COLUMN_LOGIN_TYPE = "login_type" + + const val PATH_LOGIN_TYPE = "login_type" + const val PATH_BLOCKLIST = "block_list" + + fun getAppLoungeProviderAuthority(isDebug: Boolean = false) = + "foundation.e.apps${if (isDebug) ".debug" else ""}.provider" +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 4f3c63f18..58d198a86 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,3 +62,4 @@ dependencyResolutionManagement { } rootProject.name = "App Lounge" include ':app' +include ':contracts' -- GitLab From e572a27c22c67b3d1b55093be0b865e10c02305f Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 20:20:34 +0530 Subject: [PATCH 44/64] use contracts module --- app/build.gradle | 2 ++ .../java/foundation/e/apps/MainActivity.kt | 4 +-- .../e/apps/provider/AgeRatingProvider.kt | 17 +++++----- .../e/apps/provider/ProviderConstants.kt | 31 ------------------- .../e/apps/contracts/ProviderContracts.kt | 2 +- 5 files changed, 15 insertions(+), 41 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt diff --git a/app/build.gradle b/app/build.gradle index 3f2f48ee7..b6d07125b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,6 +154,8 @@ allOpen { dependencies { + implementation project(":contracts") + // TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 api files('libs/splitinstall-lib.jar') diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 729a46248..da53b3b21 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -41,6 +41,7 @@ import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import foundation.e.apps.contracts.ProviderContracts.COLUMN_LOGIN_TYPE import foundation.e.apps.data.Constants import foundation.e.apps.data.enums.User import foundation.e.apps.data.install.models.AppInstall @@ -50,7 +51,6 @@ import foundation.e.apps.data.login.PlayStoreAuthenticator import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.install.updates.UpdatesNotifier -import foundation.e.apps.provider.ProviderConstants.LOGIN_TYPE import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections @@ -377,7 +377,7 @@ class MainActivity : AppCompatActivity() { private fun broadcastGPlayLogin() { val intent = Intent(Constants.ACTION_PARENTAL_CONTROL_APP_LOUNGE_LOGIN).apply { setPackage(BuildConfig.PACKAGE_NAME_PARENTAL_CONTROL) - putExtra(LOGIN_TYPE, viewModel.getUser().name) + putExtra(COLUMN_LOGIN_TYPE, viewModel.getUser().name) } sendBroadcast(intent) } diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index c92916293..e8c721ed9 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -29,6 +29,12 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import foundation.e.apps.BuildConfig +import foundation.e.apps.contracts.ProviderContracts.COLUMN_LOGIN_TYPE +import foundation.e.apps.contracts.ProviderContracts.COLUMN_PACKAGE_NAME +import foundation.e.apps.contracts.ProviderContracts.PATH_BLOCKLIST +import foundation.e.apps.contracts.ProviderContracts.PATH_LOGIN_TYPE +import foundation.e.apps.contracts.ProviderContracts.getAppLoungeProviderAuthority import foundation.e.apps.data.blockedApps.ContentRatingsRepository import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.install.models.AppInstall @@ -36,11 +42,6 @@ import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.install.pkg.AppLoungePackageManager -import foundation.e.apps.provider.ProviderConstants.AUTHORITY -import foundation.e.apps.provider.ProviderConstants.LOGIN_TYPE -import foundation.e.apps.provider.ProviderConstants.PACKAGE_NAME -import foundation.e.apps.provider.ProviderConstants.PATH_BLOCKLIST -import foundation.e.apps.provider.ProviderConstants.PATH_LOGIN_TYPE import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -68,6 +69,8 @@ class AgeRatingProvider : ContentProvider() { private val CODE_LOGIN_TYPE = 1 private val CODE_AGE_RATING = 2 + private val AUTHORITY = getAppLoungeProviderAuthority(BuildConfig.DEBUG) + private val uriMatcher by lazy { UriMatcher(UriMatcher.NO_MATCH).apply { addURI(AUTHORITY, PATH_LOGIN_TYPE, CODE_LOGIN_TYPE) @@ -91,13 +94,13 @@ class AgeRatingProvider : ContentProvider() { } private fun getLoginType(): Cursor { - val cursor = MatrixCursor(arrayOf(LOGIN_TYPE)) + val cursor = MatrixCursor(arrayOf(COLUMN_LOGIN_TYPE)) cursor.addRow(arrayOf(dataStoreManager.getUserType())) return cursor } private fun getAgeRatings(): Cursor { - val cursor = MatrixCursor(arrayOf(PACKAGE_NAME)) + val cursor = MatrixCursor(arrayOf(COLUMN_PACKAGE_NAME)) val packagesNames = appLoungePackageManager.getAllUserApps().map { it.packageName } runBlocking { withContext(IO) { diff --git a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt b/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt deleted file mode 100644 index 56c692021..000000000 --- a/app/src/main/java/foundation/e/apps/provider/ProviderConstants.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * 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.provider - -import foundation.e.apps.BuildConfig - -object ProviderConstants { - const val PACKAGE_NAME = "package_name" - const val LOGIN_TYPE = "login_type" - - const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider" - const val PATH_LOGIN_TYPE = "login_type" - const val PATH_BLOCKLIST = "block_list" -} diff --git a/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt b/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt index 05838bc5d..6e03a8c71 100644 --- a/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt +++ b/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt @@ -28,4 +28,4 @@ object ProviderContracts { fun getAppLoungeProviderAuthority(isDebug: Boolean = false) = "foundation.e.apps${if (isDebug) ".debug" else ""}.provider" -} \ No newline at end of file +} -- GitLab From 8e93db0e9a2b9afa691b9143a125e5fb6c09edba Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 20:21:45 +0530 Subject: [PATCH 45/64] publish contracts module through .gitlab-ci.yml --- .gitlab-ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3293d44a1..1b45ef2c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -158,3 +158,15 @@ pushToPrebuilt: - git push # Sometimes a single push doesn't do all the job, so we have to push twice - git push + +publish-contracts: + stage: publish + needs: ["buildRelease"] + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: manual + - if: '$CI_COMMIT_TAG !~ "/^$/"' + when: always + script: + - ./gradlew :contracts:build + - ./gradlew :contracts:publish \ No newline at end of file -- GitLab From 52c63be42f47673a510600fdcba21af9f4b1a76a Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 20:49:28 +0530 Subject: [PATCH 46/64] move variables under MainActivity to viewmodel --- app/src/main/java/foundation/e/apps/MainActivity.kt | 11 ++++------- .../foundation/e/apps/ui/MainActivityViewModel.kt | 3 +++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index da53b3b21..16a8cf19e 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -77,9 +77,6 @@ class MainActivity : AppCompatActivity() { private const val SESSION_DIALOG_TAG = "session_dialog" } - private var gPlayLoginRequested = false - private var closeAfterLogin = false - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -140,10 +137,10 @@ class MainActivity : AppCompatActivity() { } private fun checkGPlayLoginRequest(intent: Intent?) { - gPlayLoginRequested = + viewModel.gPlayLoginRequested = intent?.getBooleanExtra(Constants.REQUEST_GPLAY_LOGIN, false) ?: false - if (!gPlayLoginRequested) return + if (!viewModel.gPlayLoginRequested) return if (!viewModel.getTocStatus()) return if (viewModel.getUser() !in listOf(User.GOOGLE, User.ANONYMOUS)) { loginViewModel.logout() @@ -340,7 +337,7 @@ class MainActivity : AppCompatActivity() { // Pop back stack to prevent showing TOSFragment on pressing back button. navController.popBackStack() navController.navigate(R.id.signInFragment) - if (gPlayLoginRequested) closeAfterLogin = true + if (viewModel.gPlayLoginRequested) viewModel.closeAfterLogin = true return@observe } @@ -368,7 +365,7 @@ class MainActivity : AppCompatActivity() { broadcastGPlayLogin() } - if (closeAfterLogin && it.isNotEmpty() && it.all { it.result.isSuccess() }) { + if (viewModel.closeAfterLogin && it.isNotEmpty() && it.all { it.result.isSuccess() }) { finishAndRemoveTask() } } 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 251568ce4..4787b8e91 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -82,6 +82,9 @@ class MainActivityViewModel @Inject constructor( private val _errorMessageStringResource = MutableLiveData() val errorMessageStringResource: LiveData = _errorMessageStringResource + var gPlayLoginRequested = false + var closeAfterLogin = false + lateinit var connectivityManager: ConnectivityManager var shouldIgnoreSessionError = false -- GitLab From 69040640a35c564366f781e77a488b21580cf59a Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 21:06:39 +0530 Subject: [PATCH 47/64] specify different provider permissions for debug and release --- app/build.gradle | 5 +++++ app/src/debug/AndroidManifest.xml | 6 ++++++ app/src/main/AndroidManifest.xml | 2 -- app/src/release/AndroidManifest.xml | 6 ++++++ 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 app/src/debug/AndroidManifest.xml create mode 100644 app/src/release/AndroidManifest.xml diff --git a/app/build.gradle b/app/build.gradle index b6d07125b..ee011f38f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,10 +85,15 @@ android { } sourceSets { + debug { + manifest.srcFile 'src/debug/AndroidManifest.xml' + } releaseDev { + manifest.srcFile 'src/release/AndroidManifest.xml' java.srcDirs = ['src/release/java'] } releaseStable { + manifest.srcFile 'src/release/AndroidManifest.xml' java.srcDirs = ['src/release/java'] } } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..06576182c --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4d730670a..8042f9d89 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,8 +55,6 @@ - - + + + + + \ No newline at end of file -- GitLab From 4092ba9028f4afbf35471bd1562998a414c8b676 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 21:06:55 +0530 Subject: [PATCH 48/64] remove lint suppression --- lint.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lint.xml b/lint.xml index 5a6bcbd59..bfd91922c 100644 --- a/lint.xml +++ b/lint.xml @@ -47,10 +47,6 @@ - - - - -- GitLab From 0919ab8668c862339965a900035fc1944f07ba7e Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 21:21:22 +0530 Subject: [PATCH 49/64] rename contracts module to parental_control_data --- .gitlab-ci.yml | 4 ++-- app/build.gradle | 3 +-- app/src/main/java/foundation/e/apps/MainActivity.kt | 2 +- .../foundation/e/apps/provider/AgeRatingProvider.kt | 10 +++++----- {contracts => parental_control_data}/.gitignore | 0 {contracts => parental_control_data}/build.gradle | 4 ++-- .../e/apps/contract/ParentalControlContract.kt | 4 ++-- settings.gradle | 2 +- 8 files changed, 14 insertions(+), 15 deletions(-) rename {contracts => parental_control_data}/.gitignore (100%) rename {contracts => parental_control_data}/build.gradle (92%) rename contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt => parental_control_data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt (94%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1b45ef2c8..8e3667f4a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -168,5 +168,5 @@ publish-contracts: - if: '$CI_COMMIT_TAG !~ "/^$/"' when: always script: - - ./gradlew :contracts:build - - ./gradlew :contracts:publish \ No newline at end of file + - ./gradlew :parental_control_data:build + - ./gradlew :parental_control_data:publish \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ee011f38f..681b1480d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -159,8 +159,7 @@ allOpen { dependencies { - implementation project(":contracts") - + implementation project(':parental_control_data') // TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 api files('libs/splitinstall-lib.jar') diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 16a8cf19e..88496dfac 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -41,7 +41,7 @@ import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint -import foundation.e.apps.contracts.ProviderContracts.COLUMN_LOGIN_TYPE +import foundation.e.apps.contract.ParentalControlContract.COLUMN_LOGIN_TYPE import foundation.e.apps.data.Constants import foundation.e.apps.data.enums.User import foundation.e.apps.data.install.models.AppInstall diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index e8c721ed9..965c305e2 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -30,11 +30,11 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import foundation.e.apps.BuildConfig -import foundation.e.apps.contracts.ProviderContracts.COLUMN_LOGIN_TYPE -import foundation.e.apps.contracts.ProviderContracts.COLUMN_PACKAGE_NAME -import foundation.e.apps.contracts.ProviderContracts.PATH_BLOCKLIST -import foundation.e.apps.contracts.ProviderContracts.PATH_LOGIN_TYPE -import foundation.e.apps.contracts.ProviderContracts.getAppLoungeProviderAuthority +import foundation.e.apps.contract.ParentalControlContract.COLUMN_LOGIN_TYPE +import foundation.e.apps.contract.ParentalControlContract.COLUMN_PACKAGE_NAME +import foundation.e.apps.contract.ParentalControlContract.PATH_BLOCKLIST +import foundation.e.apps.contract.ParentalControlContract.PATH_LOGIN_TYPE +import foundation.e.apps.contract.ParentalControlContract.getAppLoungeProviderAuthority import foundation.e.apps.data.blockedApps.ContentRatingsRepository import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.install.models.AppInstall diff --git a/contracts/.gitignore b/parental_control_data/.gitignore similarity index 100% rename from contracts/.gitignore rename to parental_control_data/.gitignore diff --git a/contracts/build.gradle b/parental_control_data/build.gradle similarity index 92% rename from contracts/build.gradle rename to parental_control_data/build.gradle index 43c0b81c2..c8091ae86 100644 --- a/contracts/build.gradle +++ b/parental_control_data/build.gradle @@ -13,13 +13,13 @@ publishing { publications { aar(MavenPublication) { groupId = 'foundation.e.apps' - artifactId = 'contracts' + artifactId = 'parental_control_data' version = '1.0.0' artifact("$buildDir/libs/${project.name}.jar") pom { - name = 'Contracts' + name = 'ParentalControlContract' description = 'Constants to be used in App Lounge content provider' licenses { diff --git a/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt b/parental_control_data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt similarity index 94% rename from contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt rename to parental_control_data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt index 6e03a8c71..016c92bbf 100644 --- a/contracts/src/main/java/foundation/e/apps/contracts/ProviderContracts.kt +++ b/parental_control_data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt @@ -17,9 +17,9 @@ * */ -package foundation.e.apps.contracts +package foundation.e.apps.contract -object ProviderContracts { +object ParentalControlContract { const val COLUMN_PACKAGE_NAME = "package_name" const val COLUMN_LOGIN_TYPE = "login_type" diff --git a/settings.gradle b/settings.gradle index 58d198a86..cb4785f54 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,4 +62,4 @@ dependencyResolutionManagement { } rootProject.name = "App Lounge" include ':app' -include ':contracts' +include ':parental_control_data' -- GitLab From 53fe911614ba00bf304826dee43cc68516a084bd Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Tue, 11 Jun 2024 21:26:09 +0530 Subject: [PATCH 50/64] change artifact Id --- parental_control_data/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parental_control_data/build.gradle b/parental_control_data/build.gradle index c8091ae86..fb872ef39 100644 --- a/parental_control_data/build.gradle +++ b/parental_control_data/build.gradle @@ -13,7 +13,7 @@ publishing { publications { aar(MavenPublication) { groupId = 'foundation.e.apps' - artifactId = 'parental_control_data' + artifactId = 'ParentalControlData' version = '1.0.0' artifact("$buildDir/libs/${project.name}.jar") -- GitLab From 952e34f05b8828bb91ad7fc10b0f54eb0e605504 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 13:25:28 +0530 Subject: [PATCH 51/64] change module name with hyphens instead of underscores --- .gitlab-ci.yml | 4 ++-- app/build.gradle | 4 ++-- {parental_control_data => parental-control-data}/.gitignore | 0 {parental_control_data => parental-control-data}/build.gradle | 0 .../foundation/e/apps/contract/ParentalControlContract.kt | 0 settings.gradle | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename {parental_control_data => parental-control-data}/.gitignore (100%) rename {parental_control_data => parental-control-data}/build.gradle (100%) rename {parental_control_data => parental-control-data}/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt (100%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8e3667f4a..760a4491d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -168,5 +168,5 @@ publish-contracts: - if: '$CI_COMMIT_TAG !~ "/^$/"' when: always script: - - ./gradlew :parental_control_data:build - - ./gradlew :parental_control_data:publish \ No newline at end of file + - ./gradlew :parental-control-data:build + - ./gradlew :parental-control-data:publish \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 681b1480d..cd6bdee21 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -159,8 +159,8 @@ allOpen { dependencies { - implementation project(':parental_control_data') - // TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 + implementation project(':parental-control-data') +// TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 api files('libs/splitinstall-lib.jar') implementation 'foundation.e.lib:telemetry:0.0.11-alpha' diff --git a/parental_control_data/.gitignore b/parental-control-data/.gitignore similarity index 100% rename from parental_control_data/.gitignore rename to parental-control-data/.gitignore diff --git a/parental_control_data/build.gradle b/parental-control-data/build.gradle similarity index 100% rename from parental_control_data/build.gradle rename to parental-control-data/build.gradle diff --git a/parental_control_data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt b/parental-control-data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt similarity index 100% rename from parental_control_data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt rename to parental-control-data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt diff --git a/settings.gradle b/settings.gradle index cb4785f54..31d7da316 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,4 +62,4 @@ dependencyResolutionManagement { } rootProject.name = "App Lounge" include ':app' -include ':parental_control_data' +include ':parental-control-data' -- GitLab From 4ebcb55ee8a6f1cd8cfd0e0e0ee50321a1ff6fe8 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 14:03:32 +0530 Subject: [PATCH 52/64] AgeRatingProvider - separation of concerns --- .../e/apps/provider/AgeRatingProvider.kt | 78 +++++++++++++------ 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 965c305e2..76ca6daac 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import timber.log.Timber class AgeRatingProvider : ContentProvider() { @@ -101,42 +102,71 @@ class AgeRatingProvider : ContentProvider() { private fun getAgeRatings(): Cursor { val cursor = MatrixCursor(arrayOf(COLUMN_PACKAGE_NAME)) - val packagesNames = appLoungePackageManager.getAllUserApps().map { it.packageName } + val packageNames = appLoungePackageManager.getAllUserApps().map { it.packageName } runBlocking { withContext(IO) { + try { + if (packageNames.isEmpty()) return@withContext cursor - if (contentRatingsRepository.contentRatingGroups.isEmpty()) { - contentRatingsRepository.fetchContentRatingData() - } + ensureAgeGroupDataExists() - val contentRatingsDeferred = packagesNames.map { packageName -> - async { - val authData = dataStoreManager.getAuthData() - if (authData.email.isBlank() && authData.aasToken.isBlank()) { - return@async null - } else { - authenticatorRepository.gplayAuth = authData + val ageValidityDeferred = packageNames.map { packageName -> + async { + if (!setupAuthDataIfExists()) return@async null + getAppAgeValidity(packageName) } - val fakeAppInstall = AppInstall( - packageName = packageName, - origin = Origin.GPLAY - ) - val validateResult = validateAppAgeLimitUseCase(fakeAppInstall) - validateResult.data ?: false - } - } - val contentsRatings = contentRatingsDeferred.awaitAll() - contentsRatings.forEachIndexed { index: Int, isValid: Boolean? -> - if (isValid == false) { - // Collect package names for blocklist - cursor.addRow(arrayOf(packagesNames[index])) } + val validityList = ageValidityDeferred.awaitAll() + compileAppBlockList(cursor, validityList, packageNames) + } catch (e: Exception) { + Timber.e("AgeRatingProvider", "Error fetching age ratings", e) } } } return cursor } + private suspend fun ensureAgeGroupDataExists() { + if (contentRatingsRepository.contentRatingGroups.isEmpty()) { + contentRatingsRepository.fetchContentRatingData() + } + } + + /** + * Return true if valid AuthData could be fetched from data store, false otherwise. + */ + private fun setupAuthDataIfExists(): Boolean { + val authData = dataStoreManager.getAuthData() + if (authData.email.isNotBlank() || authData.aasToken.isNotBlank()) { + authenticatorRepository.gplayAuth = authData + return true + } + Timber.e("Blank AuthData, cannot fetch ratings from provider.") + return false + } + + private suspend fun getAppAgeValidity(packageName: String): Boolean { + val fakeAppInstall = AppInstall( + packageName = packageName, + origin = Origin.GPLAY + ) + val validateResult = validateAppAgeLimitUseCase(fakeAppInstall) + return validateResult.data ?: false + } + + private fun compileAppBlockList( + cursor: MatrixCursor, + validityList: List, + packageNames: List, + ) { + validityList.forEachIndexed { index: Int, isValid: Boolean? -> + if (isValid != true) { + // Collect package names for blocklist + cursor.addRow(arrayOf(packageNames[index])) + } + } + } + override fun onCreate(): Boolean { val appContext = context?.applicationContext ?: error("Null context in ${this::class.java.name}") val hiltEntryPoint = -- GitLab From 03d8f48bfcf4aca9e29a909b0509743b99734e99 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 14:03:57 +0530 Subject: [PATCH 53/64] change aar publishing to jar publishing --- parental-control-data/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parental-control-data/build.gradle b/parental-control-data/build.gradle index fb872ef39..b65ce5506 100644 --- a/parental-control-data/build.gradle +++ b/parental-control-data/build.gradle @@ -11,7 +11,7 @@ java { publishing { publications { - aar(MavenPublication) { + jar(MavenPublication) { groupId = 'foundation.e.apps' artifactId = 'ParentalControlData' version = '1.0.0' -- GitLab From d3531a0fb314b4ad88c76c4e6c0b21e0a5ebd047 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 14:04:34 +0530 Subject: [PATCH 54/64] log when updating content rating --- .../java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index 5094fa4e3..c8797ecda 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -76,6 +76,7 @@ class ValidateAppAgeLimitUseCase @Inject constructor( if (app.contentRating.id.isEmpty()) { contentRatingRepository.getEnglishContentRating(app.packageName)?.run { + Timber.d("Updating content rating for package: ${app.packageName}") app.contentRating = this } } -- GitLab From deeffd20c1eba1c2e2281a04bbb80210bb232a9f Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 15:14:03 +0530 Subject: [PATCH 55/64] change authData check condition --- .../main/java/foundation/e/apps/provider/AgeRatingProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 76ca6daac..a805894e5 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -137,7 +137,7 @@ class AgeRatingProvider : ContentProvider() { */ private fun setupAuthDataIfExists(): Boolean { val authData = dataStoreManager.getAuthData() - if (authData.email.isNotBlank() || authData.aasToken.isNotBlank()) { + if (authData.email.isNotBlank() && authData.authToken.isNotBlank()) { authenticatorRepository.gplayAuth = authData return true } -- GitLab From 9fbac45afd5355b309de5fd88125adf3193f1949 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 15:50:06 +0530 Subject: [PATCH 56/64] use enum for uri codes --- .../e/apps/provider/AgeRatingProvider.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index a805894e5..72f6acb37 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -67,15 +67,18 @@ class AgeRatingProvider : ContentProvider() { private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase private lateinit var dataStoreManager: DataStoreManager - private val CODE_LOGIN_TYPE = 1 - private val CODE_AGE_RATING = 2 + private enum class UriCodes(val code: Int) { + CODE_LOGIN_TYPE(1), + CODE_AGE_RATING(2), + ; + } private val AUTHORITY = getAppLoungeProviderAuthority(BuildConfig.DEBUG) private val uriMatcher by lazy { UriMatcher(UriMatcher.NO_MATCH).apply { - addURI(AUTHORITY, PATH_LOGIN_TYPE, CODE_LOGIN_TYPE) - addURI(AUTHORITY, PATH_BLOCKLIST, CODE_AGE_RATING) + addURI(AUTHORITY, PATH_LOGIN_TYPE, UriCodes.CODE_LOGIN_TYPE.code) + addURI(AUTHORITY, PATH_BLOCKLIST, UriCodes.CODE_AGE_RATING.code) } } @@ -88,8 +91,8 @@ class AgeRatingProvider : ContentProvider() { ): Cursor? { val code = uriMatcher.match(uri) return when (code) { - CODE_LOGIN_TYPE -> getLoginType() - CODE_AGE_RATING -> getAgeRatings() + UriCodes.CODE_LOGIN_TYPE.code -> getLoginType() + UriCodes.CODE_AGE_RATING.code -> getAgeRatings() else -> null } } @@ -193,8 +196,10 @@ class AgeRatingProvider : ContentProvider() { override fun getType(uri: Uri): String { return when (uriMatcher.match(uri)) { - CODE_LOGIN_TYPE -> "vnd.android.cursor.item/${AUTHORITY}.$CODE_LOGIN_TYPE" - CODE_AGE_RATING -> "vnd.android.cursor.item/${AUTHORITY}.$CODE_AGE_RATING" + UriCodes.CODE_LOGIN_TYPE.code -> + "vnd.android.cursor.item/${AUTHORITY}.${UriCodes.CODE_LOGIN_TYPE.code}" + UriCodes.CODE_AGE_RATING.code -> + "vnd.android.cursor.item/${AUTHORITY}.${UriCodes.CODE_AGE_RATING.code}" else -> throw IllegalArgumentException("Unknown URI: $uri") } } -- GitLab From 93bdef0b47129927dce6a790238485e87bd5dde2 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 15:57:55 +0530 Subject: [PATCH 57/64] more refactors --- .../e/apps/provider/AgeRatingProvider.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 72f6acb37..abe873468 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -112,15 +112,9 @@ class AgeRatingProvider : ContentProvider() { if (packageNames.isEmpty()) return@withContext cursor ensureAgeGroupDataExists() + if (!setupAuthDataIfExists()) return@withContext null - val ageValidityDeferred = packageNames.map { packageName -> - async { - if (!setupAuthDataIfExists()) return@async null - getAppAgeValidity(packageName) - } - } - val validityList = ageValidityDeferred.awaitAll() - compileAppBlockList(cursor, validityList, packageNames) + compileAppBlockList(cursor, packageNames) } catch (e: Exception) { Timber.e("AgeRatingProvider", "Error fetching age ratings", e) } @@ -157,15 +151,21 @@ class AgeRatingProvider : ContentProvider() { return validateResult.data ?: false } - private fun compileAppBlockList( + private suspend fun compileAppBlockList( cursor: MatrixCursor, - validityList: List, packageNames: List, ) { - validityList.forEachIndexed { index: Int, isValid: Boolean? -> - if (isValid != true) { - // Collect package names for blocklist - cursor.addRow(arrayOf(packageNames[index])) + withContext(IO) { + val validityList = packageNames.map { packageName -> + async { + getAppAgeValidity(packageName) + } + }.awaitAll() + validityList.forEachIndexed { index: Int, isValid: Boolean? -> + if (isValid != true) { + // Collect package names for blocklist + cursor.addRow(arrayOf(packageNames[index])) + } } } } -- GitLab From b9312050b55e0702e633893dc854334680d24bc5 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 17:12:59 +0530 Subject: [PATCH 58/64] even more refactors --- .../e/apps/provider/AgeRatingProvider.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index abe873468..ab01329e8 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -67,18 +67,18 @@ class AgeRatingProvider : ContentProvider() { private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase private lateinit var dataStoreManager: DataStoreManager - private enum class UriCodes(val code: Int) { - CODE_LOGIN_TYPE(1), - CODE_AGE_RATING(2), + private enum class UriCode(val code: Int) { + LoginType(1), + AgeRating(2), ; } - private val AUTHORITY = getAppLoungeProviderAuthority(BuildConfig.DEBUG) + private val authority = getAppLoungeProviderAuthority(BuildConfig.DEBUG) private val uriMatcher by lazy { UriMatcher(UriMatcher.NO_MATCH).apply { - addURI(AUTHORITY, PATH_LOGIN_TYPE, UriCodes.CODE_LOGIN_TYPE.code) - addURI(AUTHORITY, PATH_BLOCKLIST, UriCodes.CODE_AGE_RATING.code) + addURI(authority, PATH_LOGIN_TYPE, UriCode.LoginType.code) + addURI(authority, PATH_BLOCKLIST, UriCode.AgeRating.code) } } @@ -91,8 +91,8 @@ class AgeRatingProvider : ContentProvider() { ): Cursor? { val code = uriMatcher.match(uri) return when (code) { - UriCodes.CODE_LOGIN_TYPE.code -> getLoginType() - UriCodes.CODE_AGE_RATING.code -> getAgeRatings() + UriCode.LoginType.code -> getLoginType() + UriCode.AgeRating.code -> getAgeRatings() else -> null } } @@ -196,10 +196,10 @@ class AgeRatingProvider : ContentProvider() { override fun getType(uri: Uri): String { return when (uriMatcher.match(uri)) { - UriCodes.CODE_LOGIN_TYPE.code -> - "vnd.android.cursor.item/${AUTHORITY}.${UriCodes.CODE_LOGIN_TYPE.code}" - UriCodes.CODE_AGE_RATING.code -> - "vnd.android.cursor.item/${AUTHORITY}.${UriCodes.CODE_AGE_RATING.code}" + UriCode.LoginType.code -> + "vnd.android.cursor.item/${authority}.${UriCode.LoginType.code}" + UriCode.AgeRating.code -> + "vnd.android.cursor.item/${authority}.${UriCode.AgeRating.code}" else -> throw IllegalArgumentException("Unknown URI: $uri") } } -- GitLab From b8a1d4ae73e0a4a85e39fc9a3da66aa40b56d00f Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 12 Jun 2024 12:31:22 +0000 Subject: [PATCH 59/64] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Jonathan Klee --- .../main/java/foundation/e/apps/provider/AgeRatingProvider.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index ab01329e8..6be7fd3e2 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -181,7 +181,6 @@ class AgeRatingProvider : ContentProvider() { validateAppAgeLimitUseCase = hiltEntryPoint.provideValidateAppAgeLimitUseCase() dataStoreManager = hiltEntryPoint.provideDataStoreManager() - return true } -- GitLab From e8826ced3247104689317461e64dad30784ebb86 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 12 Jun 2024 18:24:48 +0530 Subject: [PATCH 60/64] better error message --- .../java/foundation/e/apps/provider/AgeRatingProvider.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 6be7fd3e2..57164db09 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -190,7 +190,7 @@ class AgeRatingProvider : ContentProvider() { selection: String?, selectionArgs: Array? ): Int { - throw UnsupportedOperationException("Not supported") + throw UnsupportedOperationException("Update operation is not supported by AgeRatingProvider") } override fun getType(uri: Uri): String { @@ -204,11 +204,11 @@ class AgeRatingProvider : ContentProvider() { } override fun insert(uri: Uri, values: ContentValues?): Uri? { - throw UnsupportedOperationException("Not supported") + throw UnsupportedOperationException("Insert operation is not supported by AgeRatingProvider") } override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - throw UnsupportedOperationException("Not supported") + throw UnsupportedOperationException("Delete operation is not supported by AgeRatingProvider") } } -- GitLab From 0fb99bdbae57f9173a7b8d28fb0fdcd14c3539a2 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 13 Jun 2024 14:19:56 +0600 Subject: [PATCH 61/64] refactor: merge more changes from 2203-paco --- .../apps/data/application/data/Application.kt | 2 +- .../data/playstore/PlayStoreRepository.kt | 4 ++ .../foundation/e/apps/di/AgeRatingModule.kt | 47 --------------- .../e/apps/provider/AgeRatingProvider.kt | 24 ++++---- app/src/main/res/values/strings.xml | 2 +- ...GetAppInstallationPermissionUseCaseTest.kt | 60 ++++++++++++++----- 6 files changed, 62 insertions(+), 77 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index aa55142aa..a67c3a9e3 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -120,7 +120,7 @@ data class Application( } } -private fun isFDroid(type: Type, origin: Origin) = +internal fun isFDroid(type: Type, origin: Origin) = (type == NATIVE && origin == Origin.CLEANAPK) private fun buildFDroidUri(packageName: String): Uri { diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt index 0239ce1f3..5a0dbc61f 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt @@ -49,4 +49,8 @@ interface PlayStoreRepository : StoreRepository { appPackage: String, contentRating: ContentRating ): ContentRating + + suspend fun getEnglishContentRating( + appPackage: String, + ): ContentRating } diff --git a/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt b/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt deleted file mode 100644 index 1cb262678..000000000 --- a/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * 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.di - -import com.squareup.moshi.Moshi -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import foundation.e.apps.data.ageRating.AgeGroupApi -import javax.inject.Singleton -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory - -@Module -@InstallIn(SingletonComponent::class) -object AgeRatingModule { - - @Singleton - @Provides - fun provideAgeGroupApi(okHttpClient: OkHttpClient, moshi: Moshi): AgeGroupApi { - return Retrofit.Builder() - .baseUrl(AgeGroupApi.BASE_URL) - .client(okHttpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - .create(AgeGroupApi::class.java) - } -} diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 230260849..c8e8a0396 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -34,16 +34,14 @@ import foundation.e.apps.contract.ParentalControlContract.COLUMN_PACKAGE_NAME import foundation.e.apps.contract.ParentalControlContract.PATH_BLOCKLIST import foundation.e.apps.contract.ParentalControlContract.PATH_LOGIN_TYPE import foundation.e.apps.contract.ParentalControlContract.getAppLoungeProviderAuthority -import foundation.e.apps.data.blockedApps.ContentRatingsRepository -import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository -import foundation.e.apps.data.playstore.PlayStoreRepository import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase import foundation.e.apps.install.pkg.AppLoungePackageManager @@ -62,7 +60,7 @@ class AgeRatingProvider : ContentProvider() { fun provideAuthenticationRepository(): AuthenticatorRepository fun providePackageManager(): AppLoungePackageManager fun provideContentRatingsRepository(): GooglePlayContentRatingsRepository - fun provideValidateAppAgeLimitUseCase(): GetAppInstallationPermissionUseCase + fun provideGetAppInstallationPermissionUseCase(): GetAppInstallationPermissionUseCase fun provideDataStoreManager(): DataStoreManager } @@ -147,13 +145,13 @@ class AgeRatingProvider : ContentProvider() { return false } - private suspend fun getAppAgeValidity(packageName: String): Boolean { + private suspend fun getAppAgeValidity(packageName: String): AppInstallationPermissionState { val fakeAppInstall = AppInstall( packageName = packageName, origin = Origin.GPLAY ) - val validateResult = validateAppAgeLimitUseCase(fakeAppInstall) - return validateResult.data ?: false + val appInstallationPermissionState = getAppInstallationPermissionUseCase(fakeAppInstall) + return appInstallationPermissionState } private suspend fun compileAppBlockList( @@ -162,15 +160,14 @@ class AgeRatingProvider : ContentProvider() { ) { withContext(IO) { val validityList = packageNames.map { packageName -> - async { - getAppAgeValidity(packageName) - } + async { getAppAgeValidity(packageName) } }.awaitAll() - validityList.forEachIndexed { index: Int, permission -> + + validityList.forEachIndexed { index: Int, permission: AppInstallationPermissionState -> when (permission) { is Denied, DeniedOnDataLoadError -> { // Collect package names for blocklist - cursor.addRow(arrayOf(packagesNames[index])) + cursor.addRow(arrayOf(packageNames[index])) } Allowed -> { @@ -191,7 +188,8 @@ class AgeRatingProvider : ContentProvider() { authenticatorRepository = hiltEntryPoint.provideAuthenticationRepository() appLoungePackageManager = hiltEntryPoint.providePackageManager() contentRatingsRepository = hiltEntryPoint.provideContentRatingsRepository() - getAppInstallationPermissionUseCase = hiltEntryPoint.getValidateAppAgeLimitUseCase() + getAppInstallationPermissionUseCase = + hiltEntryPoint.provideGetAppInstallationPermissionUseCase() dataStoreManager = hiltEntryPoint.provideDataStoreManager() return true diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d5c65ee0f..fa8c845b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -125,7 +125,7 @@ https://doc.e.foundation/support-topics/app_lounge_troubleshooting Share [%1$s] Restricted App - You are too young to be able to install %1$s. Please check with your parent your age group is correct or disable Parental Control to be able to install it. + You are too young to be able to install %1$s. Please check with your parent your age group is correct or disable parental control to be able to install it. This app may contain inappropriate content. diff --git a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt index 8855cfec0..33ac4f6c6 100644 --- a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -23,6 +23,7 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository @@ -39,6 +40,8 @@ import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState import foundation.e.apps.util.MainCoroutineRule +import javax.inject.Named +import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -47,7 +50,6 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import kotlin.test.assertEquals class GetAppInstallationPermissionUseCaseTest { @@ -71,6 +73,10 @@ class GetAppInstallationPermissionUseCaseTest { @Mock private lateinit var authenticatorRepository: AuthenticatorRepository + @Mock + @Named("cleanApkAppsRepository") + private lateinit var cleanApkRepository: CleanApkRepository + @Before fun setup() { MockitoAnnotations.openMocks(this) @@ -78,8 +84,8 @@ class GetAppInstallationPermissionUseCaseTest { GetAppInstallationPermissionUseCaseImpl( contentRatingsRepository, getParentalControlStateUseCase, - playStoreRepository - ) + playStoreRepository, + cleanApkRepository) } @Test @@ -139,19 +145,29 @@ class GetAppInstallationPermissionUseCaseTest { @Test fun `allow app installation when parental control is enabled and F-Droid app has anti-features other than NSFW`() { runTest { - val appPendingInstallation = - AppInstall().apply { - isFDroidApp = true - antiFeatures = - listOf( - mapOf( - "NonFreeAssets" to - "Artwork, layouts and prerecorded voices are under a non-commercial license")) - } + val appId = "appId" + val isFDroidApp = true + val antiFeatures = + listOf( + mapOf( + "NonFreeAssets" to + "Artwork, layouts and prerecorded voices are under a non-commercial license")) + val application = + Application(_id = appId, isFDroidApp = isFDroidApp, antiFeatures = antiFeatures) Mockito.`when`(getParentalControlStateUseCase.invoke()) .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + Mockito.`when`(cleanApkRepository.getAppDetailsById(appId)) + .thenReturn( + Result.success( + foundation.e.apps.data.cleanapk.data.app.Application(app = application))) + + val appPendingInstallation = + AppInstall(id = appId).apply { + this.isFDroidApp = application.isFDroidApp + this.antiFeatures = application.antiFeatures + } val installationPermissionState = useCase.invoke(appPendingInstallation) assertEquals(Allowed, installationPermissionState) @@ -161,10 +177,24 @@ class GetAppInstallationPermissionUseCaseTest { @Test fun `deny app installation when parental control is enabled and F-Droid app has NSFW anti-features`() { runTest { + val appId = "appId" + val isFDroidApp = true + val antiFeatures = listOf(mapOf("NSFW" to "Shows explicit content.")) + val application = + Application(_id = appId, isFDroidApp = isFDroidApp, antiFeatures = antiFeatures) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(cleanApkRepository.getAppDetailsById(appId)) + .thenReturn( + Result.success( + foundation.e.apps.data.cleanapk.data.app.Application(app = application))) + val appPendingInstallation = - AppInstall().apply { - isFDroidApp = true - antiFeatures = listOf(mapOf("NSFW" to "Shows explicit content.")) + AppInstall(id = appId).apply { + this.isFDroidApp = application.isFDroidApp + this.antiFeatures = application.antiFeatures } Mockito.`when`(getParentalControlStateUseCase.invoke()) -- GitLab From 31af2ae42f82569fca99ef21b609dcdfaf4f3a0f Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 13 Jun 2024 15:33:34 +0600 Subject: [PATCH 62/64] refactor: merge more changes from 2203-paco --- .../e/apps/data/ageRating/AgeGroupApi.kt | 35 -------------- .../foundation/e/apps/di/AgeRatingModule.kt | 47 ------------------- 2 files changed, 82 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt delete mode 100644 app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt diff --git a/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt b/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt deleted file mode 100644 index 9a3d04694..000000000 --- a/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * 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.data.ageRating - -import foundation.e.apps.data.blockedApps.ContentRatingGroup -import retrofit2.Response -import retrofit2.http.GET - -interface AgeGroupApi { - - companion object { - const val BASE_URL = "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" - } - - @GET("content_ratings.json?ref_type=heads") - suspend fun getDefinedAgeGroups(): Response> - -} diff --git a/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt b/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt deleted file mode 100644 index 1cb262678..000000000 --- a/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * 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.di - -import com.squareup.moshi.Moshi -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import foundation.e.apps.data.ageRating.AgeGroupApi -import javax.inject.Singleton -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory - -@Module -@InstallIn(SingletonComponent::class) -object AgeRatingModule { - - @Singleton - @Provides - fun provideAgeGroupApi(okHttpClient: OkHttpClient, moshi: Moshi): AgeGroupApi { - return Retrofit.Builder() - .baseUrl(AgeGroupApi.BASE_URL) - .client(okHttpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - .create(AgeGroupApi::class.java) - } -} -- GitLab From 6fe0d19a3b6eda04f52b1f1ad2c0e9600e01105e Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 13 Jun 2024 16:53:16 +0600 Subject: [PATCH 63/64] test: add test for data load error for F-Droid app --- ...GetAppInstallationPermissionUseCaseTest.kt | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt index 33ac4f6c6..211ca7538 100644 --- a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -23,6 +23,7 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.cleanapk.data.app.Application as AppLoungeApplication import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.models.AppInstall @@ -50,6 +51,8 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import java.lang.Exception class GetAppInstallationPermissionUseCaseTest { @@ -159,9 +162,7 @@ class GetAppInstallationPermissionUseCaseTest { .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) Mockito.`when`(cleanApkRepository.getAppDetailsById(appId)) - .thenReturn( - Result.success( - foundation.e.apps.data.cleanapk.data.app.Application(app = application))) + .thenReturn(Result.success(AppLoungeApplication(app = application))) val appPendingInstallation = AppInstall(id = appId).apply { @@ -187,9 +188,7 @@ class GetAppInstallationPermissionUseCaseTest { .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) Mockito.`when`(cleanApkRepository.getAppDetailsById(appId)) - .thenReturn( - Result.success( - foundation.e.apps.data.cleanapk.data.app.Application(app = application))) + .thenReturn(Result.success(AppLoungeApplication(app = application))) val appPendingInstallation = AppInstall(id = appId).apply { @@ -206,6 +205,36 @@ class GetAppInstallationPermissionUseCaseTest { } } + @Test + fun `deny app installation when parental control is enabled and App Lounge fails to load F-Droid app's data`() { + runTest { + val appId = "appId" + val isFDroidApp = true + val antiFeatures = listOf(mapOf("NSFW" to "Shows explicit content.")) + val application = + Application(_id = appId, isFDroidApp = isFDroidApp, antiFeatures = antiFeatures) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(cleanApkRepository.getAppDetailsById(appId)) + .thenReturn(Result.failure(Exception())) + + val appPendingInstallation = + AppInstall(id = appId).apply { + this.isFDroidApp = application.isFDroidApp + this.antiFeatures = application.antiFeatures + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(DeniedOnDataLoadError, installationPermissionState) + } + } + @Test fun `allow app installation when parental control is enabled and Google Play app's rating is equal to child's age group`() { runTest { -- GitLab From 05128d979a1f0f1be3966b8ae14fb3b0c1d865bd Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 13 Jun 2024 16:53:44 +0600 Subject: [PATCH 64/64] test: add test for data load error for F-Droid app --- .../GetAppInstallationPermissionUseCaseTest.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt index 211ca7538..b58693331 100644 --- a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -23,7 +23,6 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.cleanapk.data.app.Application as AppLoungeApplication import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.models.AppInstall @@ -41,8 +40,6 @@ import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState import foundation.e.apps.util.MainCoroutineRule -import javax.inject.Named -import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -51,8 +48,9 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import org.mockito.kotlin.any -import java.lang.Exception +import javax.inject.Named +import kotlin.test.assertEquals +import foundation.e.apps.data.cleanapk.data.app.Application as AppLoungeApplication class GetAppInstallationPermissionUseCaseTest { -- GitLab