diff --git a/app/build.gradle b/app/build.gradle index cd6bdee213a3fb3fe117feac5af11f9627e2acf6..66932d30125cf33bf1597c537af203b8c19ecbf1 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 3c941c851105b6a522a6f12748266eb03c1da7ea..3ba5196c416ba9b67de5ae7644f64f9e9d9e1466 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 f5c5cdd50a9099331500746b5aa7f51ba3e1757e..fab3d0367bb4f38724f4fd57058c5ca88041e748 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 9a3d04694d775fa277161fb51443ca9f1f70c927..0000000000000000000000000000000000000000 --- 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 98d8d56c9b2780c6718271ed10d6222b3260ec75..a67c3a9e3bfb44c649d7ebccb9413e139bb86cc5 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 ec2be627ee096d09fc6ee33f6404be321f8f1dfe..0000000000000000000000000000000000000000 --- 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 595904bbe3a5bfaac5bf84efa45b4ecc5a934263..0000000000000000000000000000000000000000 --- 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 6cab641e34873363211bef205395976b37c97fff..0000000000000000000000000000000000000000 --- 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 69574481db52aeda77f3c62983461a499f8a35f8..a5e162cf2556779e5a4e576a32318f51c42567f6 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 3da3d4014c5d119b0b23b1d3bfb72832fe78f6e8..1d48cf6d0b2b26de13975082b29c38c5f80eded2 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 0000000000000000000000000000000000000000..1dc6bfd2b513f26db92ddf1bf173258519da675e --- /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 0000000000000000000000000000000000000000..7d56a43f9ed937c0a1acaf6a1c5f25807e8ffeb6 --- /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 3a2d832d82af8673c0d4bf8206cfe30c1bfedb85..adbd102095f7c86e1adebeaba9828b51e1a537ac 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 e51a82823645d903fa0a85179db5745242cd9e48..6c7f6809549bceabcfdacd9376825d0c589fd2dd 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 8e8f84f6aac496bda4c4d5cfe5dc5f4588ee35d9..2346161429187436ffb575b757bff247c69b1ea9 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 2a9388f0240dcec8c8923ab72db9c7466a3942ec..8a12327ceb8d9dca4b4890f120df471a3e67778f 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 0480873f13dbdd22f8cb35482510aba7f32194eb..1d893461b28ee65d33e3594cf0b579fc054ff241 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 28f92b4d64e5be5481ff3b7a5a6a92cfc5442a44..8d4f8a7e5888ed580e85ee1e65b9485b6a3a44e3 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 0000000000000000000000000000000000000000..7e727318e08432261e9b9bce8f8bce0ac49e50b5 --- /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 0000000000000000000000000000000000000000..3821d3403f0f805a31948b3bf52a822e9d08ce4a --- /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 0000000000000000000000000000000000000000..059db6c74c9da6d23c667f3a2d54aa6ebfd233c2 --- /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 0000000000000000000000000000000000000000..21107206aaa0c123fdaaeefad1a14e570ca18bc0 --- /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 0000000000000000000000000000000000000000..8a0362cfb957c923cc811fb1ba50518d3153df8c --- /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 cde87e63857918a0678281d366f0061905bc1954..936675b79cbc964415f02af75a91809d86a3149e 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 0000000000000000000000000000000000000000..8925d30489e82315ff64455b4d47550e93a3d63f --- /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 0239ce1f3366e1819c763876af65e8ffd3269af0..5a0dbc61f396c33ec5f5810eab555483623affba 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 da884772134504403fa3cc1bdf52ffec02d5fe0c..2fd2765e6571545174a806edb61b359f4fc03981 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 1cb26267812ce529f8a023ae8df15bf0b899ee64..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..80cf276dd134f4b9016310305d70756a746582e9 --- /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 c8797ecda0f17493c191b623311d3ea60d5a3dc2..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..f6c509b5eb5596f41eff9983cb67065b0ea7a8c3 --- /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 0000000000000000000000000000000000000000..3f24c4cc2333b576f1166dfbb26b196e65589ca5 --- /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 0000000000000000000000000000000000000000..3df25d1535ac8c503b32232290b4eb6cabbfa07a --- /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 0000000000000000000000000000000000000000..421970ed22fb8f56d17570e5285fca866649f985 --- /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 4a269e55585730549c684f67ab5b6e732a691daa..c127333838bd151ed37d6b890470999b82a936cb 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 57164db099842b096ecee2aad1e04ff39dc75912..c8e8a03963ffdd0f5496f97fdbd45b3574b3d450 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 4787b8e912aa33c661e98315c2cf3e5603a648b0..e5232c708fe2a20c8c21e52b5b405f6c2af50fbf 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 bd95259361d53ad4eb9f6b0a479b7ccc0d424c91..8c1a60aa838db69036b93876bb238b2bea538d95 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 0000000000000000000000000000000000000000..9951bd0cdfd481e85378e0ae2de7c5e9eb2a7a8f --- /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 96ec06c1f3a50de7f8ac28f892ff122d7158f22d..bb6c0ce04fe301a1df3ae8e7a6dcd35147a5c572 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 f708dbec7838cdeec06010e44a65c49a907c7624..454425315f1bd50f0a233fd073e48e1e9a38b91e 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 d1fae8b162b244db612890ec48336304d9e55537..4985df2e5351c1e1fa8795933e392f4e15ded481 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 d01d12f84f06766118ec092fdefe02f87b22c4c9..fa8c845b0a779d9e6ce120d748a613a43902a3c6 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 480eeadda84da678d53a30e92c3f8adc0215ad4e..5fa85588493549e58550f2ee9bc38918a9b36908 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 0000000000000000000000000000000000000000..b58693331b5805876d799cc319087009334660ed --- /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 016c92bbf71649618ecb8a108a26a09b5ba9a538..c07f2f93007cca0ad5f082b5af56fad46a2f96b3 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 . * */