From 21ca6d77c1e0cb8777bff7313334a56657b5363f Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 13 Jun 2024 19:06:01 +0600 Subject: [PATCH 1/5] feat: implement fetching NSFW anti-features from F-Droid --- app/build.gradle | 1 + app/detekt-baseline.xml | 2 +- .../foundation/e/apps/data/NetworkHandler.kt | 4 +- .../e/apps/data/ageRating/AgeGroupApi.kt | 35 -- .../apps/data/application/data/Application.kt | 26 +- .../data/blockedApps/ContentRatingGroup.kt | 29 - .../blockedApps/ContentRatingsRepository.kt | 55 -- .../blockedApps/ParentalControlRepository.kt | 60 -- .../cleanapk/CleanApkAppDetailsRetrofit.kt | 33 +- .../e/apps/data/cleanapk/CleanApkRetrofit.kt | 8 +- .../e/apps/data/cleanapk/InterceptorModule.kt | 73 +++ .../e/apps/data/cleanapk/NetworkModule.kt | 90 +++ ...RetrofitModule.kt => RetrofitApiModule.kt} | 309 +++++----- .../CleanApkAppsRepositoryImpl.kt | 8 +- .../repositories/CleanApkPWARepository.kt | 8 +- .../repositories/CleanApkRepository.kt | 6 +- .../e/apps/data/fdroid/FdroidApiInterface.kt | 20 +- .../e/apps/data/install/models/AppInstall.kt | 24 + .../apps/data/parentalcontrol/AgeGroupApi.kt | 35 ++ .../AppInstallationPermissionState.kt | 25 + ...GetAppInstallationPermissionUseCaseImpl.kt | 138 +++++ .../GetParentalControlStateUseCaseImpl.kt | 53 ++ .../GooglePlayContentRatingGroup.kt | 28 + .../GooglePlayContentRatingParser.kt} | 37 +- .../GooglePlayContentRatingsRepository.kt | 41 ++ .../data/playstore/PlayStoreRepository.kt | 4 + .../data/playstore/PlayStoreRepositoryImpl.kt | 12 +- .../foundation/e/apps/di/AgeRatingModule.kt | 47 -- .../foundation/e/apps/di/UseCaseModule.kt | 45 ++ .../apps/domain/ValidateAppAgeLimitUseCase.kt | 87 --- .../GetAppInstallationPermissionUseCase.kt | 26 + .../GetParentalControlStateUseCase.kt | 25 + .../parentalcontrol/model/AgeGroupValue.kt | 27 + .../model/ParentalControlState.kt | 28 + .../workmanager/AppInstallProcessor.kt | 50 +- .../e/apps/provider/AgeRatingProvider.kt | 69 ++- .../e/apps/ui/MainActivityViewModel.kt | 6 +- .../ui/application/ApplicationFragment.kt | 36 +- .../main/res/drawable/ic_visibility_off.xml | 27 + .../res/layout/fragment_application_title.xml | 56 +- app/src/main/res/values-night/colors.xml | 10 +- app/src/main/res/values/colors.xml | 10 +- app/src/main/res/values/strings.xml | 8 +- .../AppInstallProcessorTest.kt | 32 +- ...GetAppInstallationPermissionUseCaseTest.kt | 536 ++++++++++++++++++ .../apps/contract/ParentalControlContract.kt | 23 +- 46 files changed, 1667 insertions(+), 645 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/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/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} (61%) create mode 100644 app/src/main/java/foundation/e/apps/data/parentalcontrol/AgeGroupApi.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 delete mode 100644 app/src/main/java/foundation/e/apps/di/AgeRatingModule.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 create mode 100644 app/src/main/res/drawable/ic_visibility_off.xml create mode 100644 app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt diff --git a/app/build.gradle b/app/build.gradle index cd6bdee21..66932d301 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -217,6 +217,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/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/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index f5c5cdd50..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 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/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index 98d8d56c9..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 @@ -101,8 +101,11 @@ data class Application( var filterLevel: FilterLevel = FilterLevel.UNKNOWN, var isGplayReplaced: Boolean = false, @SerializedName(value = "on_fdroid") - val isFDroidApp: Boolean = false, - var contentRating: ContentRating = ContentRating() + private val isFDroidAppBackingField: Boolean? = null, + val isFDroidApp: Boolean = isFDroidAppBackingField ?: isFDroid(type, origin), + var contentRating: ContentRating = ContentRating(), + @SerializedName(value = "antifeatures") + val antiFeatures: List> = emptyList() ) { fun updateType() { this.type = if (this.is_pwa) PWA else NATIVE @@ -117,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) - } - } +internal fun isFDroid(type: Type, origin: Origin) = + (type == NATIVE && origin == Origin.CLEANAPK) private fun buildFDroidUri(packageName: String): Uri { return Uri.Builder() @@ -134,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/blockedApps/ContentRatingGroup.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt deleted file mode 100644 index ec2be627e..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.squareup.moshi.Json - -data class ContentRatingGroup( - val id: String, - @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 deleted file mode 100644 index 595904bbe..000000000 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ /dev/null @@ -1,55 +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.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() - val contentRatingGroups: List - get() = _contentRatingGroups - - suspend fun fetchContentRatingData() { - val response = ageGroupApi.getDefinedAgeGroups() - if (response.isSuccessful) { - _contentRatingGroups = response.body() ?: emptyList() - } - } - - suspend fun getEnglishContentRating(packageName: String): ContentRating? { - val authData = authenticatorRepository.gplayAuth ?: return null - val contentRatingHelper = ContentRatingHelper(authData) - - return handleNetworkResult { - contentRatingHelper.getEnglishContentRating(packageName) - }.data - } -} 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/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/CleanApkRetrofit.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt index 3da3d4014..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 @@ -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 @@ -34,6 +34,10 @@ interface CleanApkRetrofit { 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 = "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" const val APP_SOURCE_ANY = "any" 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 61% 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 3a2d832d8..adbd10209 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,181 +1,128 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.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.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.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 - -@Module -@InstallIn(SingletonComponent::class) -object RetrofitModule { - - private const val HTTP_TIMEOUT_IN_SECOND = 10L - - /** - * 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() - 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()) - } - } - - @Singleton - @Provides - fun provideOkHttpClient(cache: Cache, interceptor: Interceptor): OkHttpClient { - return OkHttpClient.Builder() - .addInterceptor(interceptor) - .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.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.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) + } + + @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/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/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 { 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/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/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..059db6c74 --- /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 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 +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.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 +import javax.inject.Named + +class GetAppInstallationPermissionUseCaseImpl +@Inject +constructor( + private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, + private val getParentalControlStateUseCase: GetParentalControlStateUseCase, + private val playStoreRepository: PlayStoreRepository, + @Named("cleanApkAppsRepository") private val cleanApkRepository: CleanApkRepository +) : GetAppInstallationPermissionUseCase { + + companion object { + private const val KEY_ANTI_FEATURES_NSFW = "NSFW" + } + + override suspend operator fun invoke(app: AppInstall): AppInstallationPermissionState { + return when (val parentalControl = getParentalControlStateUseCase.invoke()) { + is Disabled -> Allowed + is AgeGroup -> + when { + isFDroidApp(app) -> validateNsfwAntiFeature(app, parentalControl) + else -> validateGooglePlayContentRating(app, parentalControl) + } + } + } + + 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 + else -> Allowed + } + } + + private fun hasNoAntiFeatures(app: Application) = app.antiFeatures.isEmpty() + + private fun isNsfwFDroidApp(app: Application) = + app.antiFeatures.any { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } + + private suspend fun validateGooglePlayContentRating( + app: AppInstall, + parentalControlState: AgeGroup + ): AppInstallationPermissionState { + + return when { + isGooglePlayApp(app) && hasNoContentRating(app) -> DeniedOnDataLoadError + hasValidContentRating(app, parentalControlState) -> Allowed + else -> Denied + } + } + + private fun isGooglePlayApp(app: AppInstall): Boolean { + return !isFDroidApp(app) && app.type != Type.PWA + } + + private fun isFDroidApp(app: AppInstall): Boolean { + return app.isFDroidApp + } + + private suspend fun hasNoContentRating(app: AppInstall): Boolean { + return !verifyContentRatingExists(app) + } + + private fun hasValidContentRating( + app: AppInstall, + parentalControlState: AgeGroup, + ): Boolean { + 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) + } + } + } + + 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/parentalcontrol/GetParentalControlStateUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt new file mode 100644 index 000000000..21107206a --- /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 suspend fun invoke(): 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..8a0362cfb --- /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.squareup.moshi.Json + +data class GooglePlayContentRatingGroup( + val id: String, + @Json(name = "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..8925d3048 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.parentalcontrol.AgeGroupApi +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GooglePlayContentRatingsRepository @Inject constructor(private val ageGroupApi: AgeGroupApi) { + + private var _contentRatingGroups = listOf() + val contentRatingGroups: List + get() = + _contentRatingGroups.map { ratingGroup -> + ratingGroup.copy(ratings = ratingGroup.ratings.map { rating -> rating.lowercase() }) + } // Ratings need to be converted to lowercase + + 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/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..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 @@ -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) { @@ -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/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/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 c8797ecda..000000000 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ /dev/null @@ -1,87 +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 foundation.e.apps.data.ResultSupreme -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.install.models.AppInstall -import timber.log.Timber -import javax.inject.Inject - -class ValidateAppAgeLimitUseCase @Inject constructor( - private val contentRatingRepository: ContentRatingsRepository, - private val parentalControlRepository: ParentalControlRepository, -) { - - suspend operator fun invoke(app: AppInstall): ResultSupreme { - val ageGroup = parentalControlRepository.getSelectedAgeGroup() - - return when { - isParentalControlDisabled(ageGroup) -> ResultSupreme.Success(data = true) - hasNoContentRating(app) -> 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) = - !verifyContentRatingExists(app) - - private fun isValidAppAgeRating( - app: AppInstall, - allowedContentRating: ContentRatingGroup? - ): 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 - - private suspend fun verifyContentRatingExists(app: AppInstall): Boolean { - - if (app.contentRating.id.isEmpty()) { - contentRatingRepository.getEnglishContentRating(app.packageName)?.run { - Timber.d("Updating content rating for package: ${app.packageName}") - app.contentRating = this - } - } - - 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..3f24c4cc2 --- /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 { + suspend operator fun invoke(): 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..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 @@ -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.playstore.utils.GplayHttpRequestException 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.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 getAppInstallationPermissionUseCase: GetAppInstallationPermissionUseCase, 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,28 @@ 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 = + getAppInstallationPermissionUseCase.invoke(appInstall) + when (installationPermission) { + Allowed -> { + Timber.i("${appInstall.name} is allowed to be installed.") + // no operation, allow installation + } + + Denied -> { + Timber.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.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/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 57164db09..c8e8a0396 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 . * */ @@ -35,12 +34,16 @@ 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 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.preference.DataStoreManager -import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase import foundation.e.apps.install.pkg.AppLoungePackageManager import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async @@ -56,15 +59,15 @@ class AgeRatingProvider : ContentProvider() { interface ContentProviderEntryPoint { fun provideAuthenticationRepository(): AuthenticatorRepository fun providePackageManager(): AppLoungePackageManager - fun provideContentRatingsRepository(): ContentRatingsRepository - fun provideValidateAppAgeLimitUseCase(): ValidateAppAgeLimitUseCase + fun provideContentRatingsRepository(): GooglePlayContentRatingsRepository + fun provideGetAppInstallationPermissionUseCase(): GetAppInstallationPermissionUseCase fun provideDataStoreManager(): DataStoreManager } private lateinit var authenticatorRepository: AuthenticatorRepository 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 enum class UriCode(val code: Int) { @@ -142,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( @@ -157,14 +160,21 @@ class AgeRatingProvider : ContentProvider() { ) { withContext(IO) { val validityList = packageNames.map { packageName -> - async { - getAppAgeValidity(packageName) - } + async { getAppAgeValidity(packageName) } }.awaitAll() - validityList.forEachIndexed { index: Int, isValid: Boolean? -> - if (isValid != true) { - // Collect package names for blocklist - cursor.addRow(arrayOf(packageNames[index])) + + validityList.forEachIndexed { index: Int, permission: AppInstallationPermissionState -> + when (permission) { + is Denied, DeniedOnDataLoadError -> { + // Collect package names for blocklist + cursor.addRow(arrayOf(packageNames[index])) + } + + Allowed -> { + // no-op + } + + else -> error("Invalid application permission state.") } } } @@ -178,7 +188,8 @@ class AgeRatingProvider : ContentProvider() { authenticatorRepository = hiltEntryPoint.provideAuthenticationRepository() appLoungePackageManager = hiltEntryPoint.providePackageManager() contentRatingsRepository = hiltEntryPoint.provideContentRatingsRepository() - validateAppAgeLimitUseCase = hiltEntryPoint.provideValidateAppAgeLimitUseCase() + getAppInstallationPermissionUseCase = + hiltEntryPoint.provideGetAppInstallationPermissionUseCase() dataStoreManager = hiltEntryPoint.provideDataStoreManager() return true 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 4787b8e91..e5232c708 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() { @@ -240,7 +240,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 bd9525936..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 @@ -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?) { @@ -234,6 +235,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { stopLoadingUI() collectState() + } private fun collectState() { @@ -284,7 +286,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } appTrackers.setOnClickListener { val fusedApp = applicationViewModel.getFusedApp() - var trackers = + val trackers = buildTrackersString(fusedApp) ApplicationDialogFragment( @@ -433,6 +435,28 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { appIcon.load(it.icon_image_path) } } + + updateAntiFeaturesUi(it) + } + + private fun updateAntiFeaturesUi(app: Application) { + val isNsfwApp = + app.antiFeatures.find { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } != null + + if (isNsfwApp) { + with(binding.titleInclude) { + 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) { @@ -444,11 +468,19 @@ 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(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) 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/layout/fragment_application_title.xml b/app/src/main/res/layout/fragment_application_title.xml index 96ec06c1f..bb6c0ce04 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" /> + + + + + + + + - + tools:text="Open Source" + tools:visibility="visible" /> + tools:text="Racing" + tools:visibility="visible" /> - - \ No newline at end of file + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index f708dbec7..454425315 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 - \ No newline at end of file + + + #99FFFFFF + #99FFFFFF + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d1fae8b16..4985df2e5 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 - \ No newline at end of file + + #99000000 + #000000 + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d01d12f84..fa8c845b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -125,8 +125,9 @@ 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. Update All @@ -226,4 +227,7 @@ Split Install channel Sign in Ignore - \ No newline at end of file + + 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. + 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..5fa855884 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.domain.parentalcontrol.GetAppInstallationPermissionUseCase 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: GetAppInstallationPermissionUseCase @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) 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..b58693331 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -0,0 +1,536 @@ +/* + * 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.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 +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 +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 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 +import javax.inject.Named +import kotlin.test.assertEquals +import foundation.e.apps.data.cleanapk.data.app.Application as AppLoungeApplication + +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 + + @Mock + @Named("cleanApkAppsRepository") + private lateinit var cleanApkRepository: CleanApkRepository + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + useCase = + GetAppInstallationPermissionUseCaseImpl( + contentRatingsRepository, + getParentalControlStateUseCase, + playStoreRepository, + cleanApkRepository) + } + + @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 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(AppLoungeApplication(app = application))) + + val appPendingInstallation = + AppInstall(id = appId).apply { + this.isFDroidApp = application.isFDroidApp + this.antiFeatures = application.antiFeatures + } + 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 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(AppLoungeApplication(app = application))) + + 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(Denied, installationPermissionState) + } + } + + @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 { + 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.getEnglishContentRating(appPackage)) + .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.getEnglishContentRating(appPackage)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + 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.getEnglishContentRating(appPackage)) + .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.getEnglishContentRating(appPackage)) + .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.getEnglishContentRating(appPackage)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Denied, installationPermissionState) + } + } +} 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 index 016c92bbf..c07f2f930 100644 --- 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 @@ -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 . * */ -- GitLab From 9e227fc82e3aabf3be1bb958ac83f5cf430647d3 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 13 Jun 2024 20:38:32 +0600 Subject: [PATCH 2/5] feat: refactor GetAppInstallationPermissionUseCase Refactored GetAppInstallationPermissionUseCase. Removed its interface and renamed GetAppInstallationPermissionUseCaseImpl to GetAppInstallationPermissionUseCase. Also updated all the references accordingly. Removed commented out code in CleanApkRetrofit class and removed unused binding in UseCaseModule. --- .../e/apps/data/cleanapk/CleanApkRetrofit.kt | 4 --- ...=> GetAppInstallationPermissionUseCase.kt} | 7 +++-- .../foundation/e/apps/di/UseCaseModule.kt | 7 ----- .../GetAppInstallationPermissionUseCase.kt | 26 ------------------- .../workmanager/AppInstallProcessor.kt | 7 ++--- .../e/apps/provider/AgeRatingProvider.kt | 2 +- .../AppInstallProcessorTest.kt | 2 +- ...GetAppInstallationPermissionUseCaseTest.kt | 5 ++-- 8 files changed, 9 insertions(+), 51 deletions(-) rename app/src/main/java/foundation/e/apps/data/parentalcontrol/{GetAppInstallationPermissionUseCaseImpl.kt => GetAppInstallationPermissionUseCase.kt} (95%) delete mode 100644 app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetAppInstallationPermissionUseCase.kt 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 1d48cf6d0..b3f60267f 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 @@ -34,10 +34,6 @@ interface CleanApkRetrofit { 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 = "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" const val APP_SOURCE_ANY = "any" diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCase.kt similarity index 95% rename from app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt rename to app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCase.kt index 059db6c74..0e2d78faa 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCase.kt @@ -27,7 +27,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.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 @@ -36,20 +35,20 @@ import foundation.e.apps.domain.parentalcontrol.model.isEnabled import javax.inject.Inject import javax.inject.Named -class GetAppInstallationPermissionUseCaseImpl +class GetAppInstallationPermissionUseCase @Inject constructor( private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val getParentalControlStateUseCase: GetParentalControlStateUseCase, private val playStoreRepository: PlayStoreRepository, @Named("cleanApkAppsRepository") private val cleanApkRepository: CleanApkRepository -) : GetAppInstallationPermissionUseCase { +) { companion object { private const val KEY_ANTI_FEATURES_NSFW = "NSFW" } - override suspend operator fun invoke(app: AppInstall): AppInstallationPermissionState { + suspend operator fun invoke(app: AppInstall): AppInstallationPermissionState { return when (val parentalControl = getParentalControlStateUseCase.invoke()) { is Disabled -> Allowed is AgeGroup -> diff --git a/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt b/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt index 80cf276dd..db5e90785 100644 --- a/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt +++ b/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt @@ -22,20 +22,13 @@ 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 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 deleted file mode 100644 index f6c509b5e..000000000 --- a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetAppInstallationPermissionUseCase.kt +++ /dev/null @@ -1,26 +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.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/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index c12733383..0805db8e8 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.GetAppInstallationPermissionUseCase 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 @@ -136,10 +136,7 @@ class AppInstallProcessor @Inject constructor( return } - - val installationPermission = - getAppInstallationPermissionUseCase.invoke(appInstall) - when (installationPermission) { + when (getAppInstallationPermissionUseCase.invoke(appInstall)) { Allowed -> { Timber.i("${appInstall.name} is allowed to be installed.") // no operation, allow installation 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 c8e8a0396..db9e64b36 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -41,9 +41,9 @@ 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.GetAppInstallationPermissionUseCase import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository import foundation.e.apps.data.preference.DataStoreManager -import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase import foundation.e.apps.install.pkg.AppLoungePackageManager import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async 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 5fa855884..9ac546274 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -28,8 +28,8 @@ 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.parentalcontrol.GetAppInstallationPermissionUseCase 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.notification.StorageNotificationManager import foundation.e.apps.install.workmanager.AppInstallProcessor 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 b58693331..4f259d752 100644 --- a/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -30,12 +30,11 @@ 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.GetAppInstallationPermissionUseCase 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 @@ -82,7 +81,7 @@ class GetAppInstallationPermissionUseCaseTest { fun setup() { MockitoAnnotations.openMocks(this) useCase = - GetAppInstallationPermissionUseCaseImpl( + GetAppInstallationPermissionUseCase( contentRatingsRepository, getParentalControlStateUseCase, playStoreRepository, -- GitLab From 00cf6431741cff6a9c13203a0053a2e874ae3784 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 25 Jun 2024 12:39:29 +0600 Subject: [PATCH 3/5] refactor: improve error handling in GetParentalControlStateUseCaseImpl --- .../GetParentalControlStateUseCaseImpl.kt | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) 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 21107206a..f2f4d650a 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 @@ -28,26 +28,35 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class GetParentalControlStateUseCaseImpl @Inject constructor( - @ApplicationContext private val context: Context -) : GetParentalControlStateUseCase { +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" + private const val COLUMN_NAME = "age" } override suspend fun invoke(): 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) + + val cursor = + context.contentResolver.query(uri, null, null, null, null) + ?: return ParentalControlState.Disabled + + return try { + cursor.use { + when { + it.moveToFirst() -> { + val ageOrdinal = it.getColumnIndexOrThrow(COLUMN_NAME).let(it::getInt) + val ageGroup = AgeGroupValue.values()[ageOrdinal] + ParentalControlState.AgeGroup(ageGroup) + } + else -> ParentalControlState.Disabled + } } + } catch (e: IllegalArgumentException) { + ParentalControlState.Disabled } - - return ParentalControlState.Disabled } } -- GitLab From f279653284c19282de69cb630d3c635e7b3901ce Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 25 Jun 2024 12:49:58 +0600 Subject: [PATCH 4/5] refactor: refactor parental control use case classes This commit includes the following changes: - Renamed GetParentalControlStateUseCaseImpl.kt to GetParentalControlStateUseCase.kt, and updated its usage in other classes. - Updated import statements in GetAppInstallationPermissionUseCaseTest.kt and GetAppInstallationPermissionUseCase.kt to reflect the new class name. - Removed an unused import from GetAppInstallationPermissionUseCase.kt. --- .../GetAppInstallationPermissionUseCase.kt | 1 - ...l.kt => GetParentalControlStateUseCase.kt} | 7 ++-- .../foundation/e/apps/di/UseCaseModule.kt | 38 ------------------- .../GetParentalControlStateUseCase.kt | 25 ------------ ...GetAppInstallationPermissionUseCaseTest.kt | 8 ++-- 5 files changed, 7 insertions(+), 72 deletions(-) rename app/src/main/java/foundation/e/apps/data/parentalcontrol/{GetParentalControlStateUseCaseImpl.kt => GetParentalControlStateUseCase.kt} (88%) delete mode 100644 app/src/main/java/foundation/e/apps/di/UseCaseModule.kt delete mode 100644 app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCase.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCase.kt index 0e2d78faa..1c920c6ee 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCase.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCase.kt @@ -27,7 +27,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.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 diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCase.kt similarity index 88% rename from app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt rename to app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCase.kt index f2f4d650a..4db48c786 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCase.kt @@ -21,23 +21,22 @@ 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 +class GetParentalControlStateUseCase @Inject -constructor(@ApplicationContext private val context: Context) : GetParentalControlStateUseCase { +constructor(@ApplicationContext private val context: Context) { companion object { private const val URI_PARENTAL_CONTROL_PROVIDER = "content://foundation.e.parentalcontrol.provider/age" private const val COLUMN_NAME = "age" } - override suspend fun invoke(): ParentalControlState { + fun invoke(): ParentalControlState { val uri = Uri.parse(URI_PARENTAL_CONTROL_PROVIDER) val cursor = diff --git a/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt b/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt deleted file mode 100644 index db5e90785..000000000 --- a/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt +++ /dev/null @@ -1,38 +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.di - -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import foundation.e.apps.data.parentalcontrol.GetParentalControlStateUseCaseImpl -import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -interface UseCaseModule { - - @Singleton - @Binds - fun bindGetParentalControlStateUseCase( - useCase: GetParentalControlStateUseCaseImpl - ): GetParentalControlStateUseCase -} 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 deleted file mode 100644 index 3f24c4cc2..000000000 --- a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetParentalControlStateUseCase.kt +++ /dev/null @@ -1,25 +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.parentalcontrol - -import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState - -interface GetParentalControlStateUseCase { - 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 index 4f259d752..68913d434 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 @@ -31,14 +32,16 @@ import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.All import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCase +import foundation.e.apps.data.parentalcontrol.GetParentalControlStateUseCase 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.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,9 +50,6 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import javax.inject.Named -import kotlin.test.assertEquals -import foundation.e.apps.data.cleanapk.data.app.Application as AppLoungeApplication class GetAppInstallationPermissionUseCaseTest { -- GitLab From 50a7906da7b483c913cef580be04e3475a7c21db Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 25 Jun 2024 13:01:46 +0600 Subject: [PATCH 5/5] refactor: update NSFW warning feature code organization Refactored the code for displaying NSFW warnings on the ApplicationFragment. The warning is now handled by a dedicated function, improving code readability and maintainability. --- .../ui/application/ApplicationFragment.kt | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) 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 8c1a60aa8..e1d34c485 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 @@ -440,25 +440,21 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } private fun updateAntiFeaturesUi(app: Application) { - val isNsfwApp = - app.antiFeatures.find { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } != null - - if (isNsfwApp) { - with(binding.titleInclude) { - 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) - } - } - } + with(binding.titleInclude.antiFeatureInfoLayout) { + isVisible = + app.antiFeatures.any { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } + setOnClickListener { showNsfwWarning() } } } + private fun showNsfwWarning() { + 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) { binding.titleInclude.apply { var catText = app.category.ifBlank { args.category } -- GitLab