diff --git a/app/build.gradle b/app/build.gradle index c324b36834b93086c4ae9296d977f19915afdaa4..6691da9312f66b7c2684a127f4d6a0c1ed3a5acb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -206,6 +206,7 @@ dependencies { implementation "com.squareup.moshi:moshi-kotlin:1.13.0" // implementation "com.squareup.moshi:moshi-adapters:1.5.0" implementation "com.squareup.okhttp3:okhttp:4.9.2" + implementation "com.squareup.okhttp3:logging-interceptor:4.9.2" // JSON Converter implementation 'com.squareup.retrofit2:converter-gson:2.5.0' diff --git a/app/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/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index 98d8d56c9b2780c6718271ed10d6222b3260ec75..328c53ac6b704c150f155d4c96bfe9c10b85184b 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -102,7 +102,9 @@ data class Application( var isGplayReplaced: Boolean = false, @SerializedName(value = "on_fdroid") val isFDroidApp: Boolean = false, - var contentRating: ContentRating = ContentRating() + var contentRating: ContentRating = ContentRating(), + @SerializedName(value = "antifeatures") + val antiFeatures: List> = emptyList() ) { fun updateType() { this.type = if (this.is_pwa) PWA else NATIVE 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 cfb734abcef8f7e38af4c8bb26a82917e81f762a..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.google.gson.annotations.SerializedName - -data class ContentRatingGroup( - val id: String, - @SerializedName("age_group") - val ageGroup: String, - var ratings: List -) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt deleted file mode 100644 index 44f17df51e5ed2035e548ab30f991f084701d00a..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * Apps Quickly and easily install Android apps onto your device! - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package foundation.e.apps.data.blockedApps - -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import com.google.gson.reflect.TypeToken -import foundation.e.apps.data.DownloadManager -import foundation.e.apps.data.install.FileManager -import timber.log.Timber -import java.io.File -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class ContentRatingsRepository @Inject constructor( - private val downloadManager: DownloadManager, - private val contentRatingParser: ContentRatingParser -) { - - private var _contentRatingGroups = listOf() - val contentRatingGroups: List - get() = _contentRatingGroups - - companion object { - private const val CONTENT_RATINGS_FILE_URL = - "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + - "content_ratings.json?ref_type=heads&inline=false" - private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" - } - - fun fetchContentRatingData() { - downloadManager.downloadFileInCache( - CONTENT_RATINGS_FILE_URL, - fileName = CONTENT_RATINGS_FILE_NAME - ) { success, _ -> - if (success) { - contentRatingParser.parseContentRatingData() - } - } - } -} diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt deleted file mode 100644 index 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/CleanApkRetrofit.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt index 3da3d4014c5d119b0b23b1d3bfb72832fe78f6e8..7c0ec7dde08baf23bdade26b719a396629ef5291 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_DEV = "https://api.dev.cleanapk.org/v2/" + // const val ASSET_URL_DEV = "https://api.dev.cleanapk.org/v2/media/" + // Application sources const val APP_SOURCE_FOSS = "open" 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 59% 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 8744d01df3964066fe9d3557946072bec0a69f44..662ba206b6ccb90217516c82ba90bc2291b1b308 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,186 +1,115 @@ -/* - * 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.cleanapk.data.app.Application -import foundation.e.apps.data.ecloud.EcloudApiInterface -import foundation.e.apps.data.exodus.ExodusTrackerApi -import foundation.e.apps.data.fdroid.FdroidApiInterface -import okhttp3.Cache -import okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient -import okhttp3.Protocol -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.converter.jackson.JacksonConverterFactory -import retrofit2.converter.moshi.MoshiConverterFactory -import timber.log.Timber -import java.net.ConnectException -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Named -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.ecloud.EcloudApiInterface +import foundation.e.apps.data.exodus.ExodusTrackerApi +import foundation.e.apps.data.fdroid.FdroidApiInterface +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.jackson.JacksonConverterFactory +import retrofit2.converter.moshi.MoshiConverterFactory +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class RetrofitApiModule { + /** + * Provides an instance of Retrofit to work with CleanAPK API + * @return instance of [CleanApkRetrofit] + */ + @Singleton + @Provides + fun provideCleanApkInterface(okHttpClient: OkHttpClient, moshi: Moshi): CleanApkRetrofit { + return Retrofit.Builder() + .baseUrl(CleanApkRetrofit.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(CleanApkRetrofit::class.java) + } + + /** + * Provides an instance of Retrofit to work with CleanAPK API + * @return instance of [CleanApkAppDetailsRetrofit] + */ + @Singleton + @Provides + fun provideCleanApkDetailApi( + okHttpClient: OkHttpClient, + @Named("gsonCustomAdapter") gson: Gson + ): CleanApkAppDetailsRetrofit { + return Retrofit.Builder() + .baseUrl(CleanApkRetrofit.BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + .create(CleanApkAppDetailsRetrofit::class.java) + } + + @Singleton + @Provides + fun provideExodusApi(okHttpClient: OkHttpClient, moshi: Moshi): ExodusTrackerApi { + return Retrofit.Builder() + .baseUrl(ExodusTrackerApi.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(ExodusTrackerApi::class.java) + } + + /** + * The fdroid api returns results in .yaml format. + * Hence we need a yaml convertor. + * Convertor is being provided by [getYamlFactory]. + */ + @Singleton + @Provides + fun provideFdroidApi( + okHttpClient: OkHttpClient, + @Named("yamlFactory") yamlFactory: JacksonConverterFactory + ): FdroidApiInterface { + return Retrofit.Builder() + .baseUrl(FdroidApiInterface.BASE_URL) + .client(okHttpClient) + .addConverterFactory(yamlFactory) + .build() + .create(FdroidApiInterface::class.java) + } + + @Singleton + @Provides + fun provideEcloudApi(okHttpClient: OkHttpClient, moshi: Moshi): EcloudApiInterface { + return Retrofit.Builder() + .baseUrl(EcloudApiInterface.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(EcloudApiInterface::class.java) + } +} diff --git a/app/src/main/java/foundation/e/apps/data/fdroid/FdroidApiInterface.kt b/app/src/main/java/foundation/e/apps/data/fdroid/FdroidApiInterface.kt index 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/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..9006b9242ca5342eb38ac1ff6aef977dc13dd187 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.parentalcontrol + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError +import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository +import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.parentalcontrol.GetAppInstallationPermissionUseCase +import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState.AgeGroup +import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState.Disabled +import foundation.e.apps.domain.parentalcontrol.model.isEnabled +import javax.inject.Inject + +class GetAppInstallationPermissionUseCaseImpl +@Inject +constructor( + private val applicationRepository: ApplicationRepository, + private val dataStoreManager: DataStoreManager, + private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, + private val getParentalControlStateUseCase: GetParentalControlStateUseCase, + private val playStoreRepository: PlayStoreRepository +) : GetAppInstallationPermissionUseCase { + + companion object { + private const val KEY_ANTI_FEATURES_NSFW = "NSFW" + } + + override suspend operator fun invoke(app: AppInstall): AppInstallationPermissionState { + return when (val parentalControl = getParentalControlStateUseCase.invoke()) { + is Disabled -> Allowed + is AgeGroup -> + when { + isFDroidApp(app) -> validateNsfwAntiFeature(app, parentalControl) + else -> validateGooglePlayContentRating(app, parentalControl) + } + } + } + + private fun validateNsfwAntiFeature( + app: AppInstall, + parentalControl: ParentalControlState + ): AppInstallationPermissionState { + return when { + hasNoAntiFeatures(app) -> Allowed + isNsfwFDroidApp(app) && parentalControl.isEnabled -> Denied + else -> Allowed + } + } + + private fun hasNoAntiFeatures(app: AppInstall) = app.antiFeatures.isEmpty() + + private fun isNsfwFDroidApp(app: AppInstall) = + app.antiFeatures.any { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } + + private suspend fun 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 = app.isFDroidApp + + private suspend fun hasNoContentRating(app: AppInstall): Boolean { + val authData = dataStoreManager.getAuthData() + return !verifyContentRatingExists(app, authData) + } + + 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, authData: AuthData): Boolean { + if (app.contentRating.title.isEmpty()) { + applicationRepository + .getApplicationDetails(app.id, app.packageName, authData, app.origin) + .let { (appDetails, resultStatus) -> + if (resultStatus == ResultStatus.OK) { + app.contentRating = appDetails.contentRating + } else { + return false + } + } + } + + if (app.contentRating.id.isEmpty()) { + app.contentRating = + playStoreRepository.getContentRatingWithId(app.packageName, app.contentRating) + } + + return app.contentRating.title.isNotEmpty() && app.contentRating.id.isNotEmpty() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/GetParentalControlStateUseCaseImpl.kt new file mode 100644 index 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..6b59d5ba2757c67c67038668051c2fd2d2d4c35d --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingGroup.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.parentalcontrol.gplayrating + +import com.google.gson.annotations.SerializedName + +data class GooglePlayContentRatingGroup( + val id: String, + @SerializedName("age_group") + val ageGroup: String, + var ratings: List +) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingParser.kt similarity index 63% rename from app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt rename to app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingParser.kt index 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..9246609bb2263503489e28aebbe39d314dea3ce4 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/gplayrating/GooglePlayContentRatingsRepository.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.parentalcontrol.gplayrating + +import foundation.e.apps.data.DownloadManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GooglePlayContentRatingsRepository +@Inject +constructor( + private val downloadManager: DownloadManager, + private val googlePlayContentRatingParser: GooglePlayContentRatingParser +) { + + private var _contentRatingGroups = listOf() + val contentRatingGroups: List + get() = _contentRatingGroups + + companion object { + private const val CONTENT_RATINGS_FILE_URL = + "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + + "content_ratings.json?ref_type=heads&inline=false" + private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" + } + + fun fetchContentRatingData() { + downloadManager.downloadFileInCache( + CONTENT_RATINGS_FILE_URL, fileName = CONTENT_RATINGS_FILE_NAME) { success, _ -> + _contentRatingGroups = + if (success) { + googlePlayContentRatingParser.parseContentRatingData() + } else { + emptyList() + } + } + } +} 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..9c1703cb8bc88e1e335cde4f9d1c4ff514ac1680 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt @@ -218,7 +218,7 @@ class PlayStoreRepositoryImpl @Inject constructor( appPackage: String, contentRating: ContentRating ): ContentRating { - val authData = authenticatorRepository.gplayAuth!! + val authData = authenticatorRepository.gplayAuth ?: return contentRating val contentRatingHelper = ContentRatingHelper(authData) return withContext(Dispatchers.IO) { diff --git a/app/src/main/java/foundation/e/apps/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 c859fe5e1d5d40511f2d78bbae8cb9a4322f729d..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2024 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package foundation.e.apps.domain - -import com.aurora.gplayapi.data.models.AuthData -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.blockedApps.Age -import foundation.e.apps.data.blockedApps.ContentRatingGroup -import foundation.e.apps.data.blockedApps.ContentRatingsRepository -import foundation.e.apps.data.blockedApps.ParentalControlRepository -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.playstore.PlayStoreRepository -import foundation.e.apps.data.preference.DataStoreManager -import timber.log.Timber -import javax.inject.Inject - -class ValidateAppAgeLimitUseCase @Inject constructor( - private val applicationRepository: ApplicationRepository, - private val dataStoreManager: DataStoreManager, - private val contentRatingRepository: ContentRatingsRepository, - private val parentalControlRepository: ParentalControlRepository, - private val playStoreRepository: PlayStoreRepository -) { - - suspend operator fun invoke(app: AppInstall): ResultSupreme { - val authData = dataStoreManager.getAuthData() - val ageGroup = parentalControlRepository.getSelectedAgeGroup() - - return when { - isParentalControlDisabled(ageGroup) -> ResultSupreme.Success(data = true) - hasNoContentRating(app, authData) -> ResultSupreme.Error(data = false) - else -> validateAgeLimit(ageGroup, app) - } - } - - private fun validateAgeLimit( - ageGroup: Age, - app: AppInstall - ): ResultSupreme.Success { - val allowedContentRating = - contentRatingRepository.contentRatingGroups.find { it.id == ageGroup.toString() } - - Timber.d( - "Selected age group: $ageGroup \n" + - "Content rating: ${app.contentRating.id} \n" + - "Allowed content rating: $allowedContentRating" - ) - - return ResultSupreme.Success(isValidAppAgeRating(app, allowedContentRating)) - } - - private suspend fun hasNoContentRating(app: AppInstall, authData: AuthData) = - !verifyContentRatingExists(app, authData) - - private fun isValidAppAgeRating( - app: AppInstall, - allowedContentRating: ContentRatingGroup? - ) = (app.contentRating.id.isNotEmpty() - && allowedContentRating?.ratings?.contains(app.contentRating.id) == true) - - private fun isParentalControlDisabled(ageGroup: Age) = ageGroup == Age.PARENTAL_CONTROL_DISABLED - - private suspend fun verifyContentRatingExists( - app: AppInstall, - authData: AuthData - ): Boolean { - if (app.contentRating.title.isEmpty()) { - applicationRepository - .getApplicationDetails( - app.id, app.packageName, authData, app.origin - ).let { (appDetails, resultStatus) -> - if (resultStatus == ResultStatus.OK) { - app.contentRating = appDetails.contentRating - } else { - return false - } - } - } - - if (app.contentRating.id.isEmpty()) { - app.contentRating = - playStoreRepository.getContentRatingWithId( - app.packageName, - app.contentRating - ) - } - - return app.contentRating.title.isNotEmpty() && - app.contentRating.id.isNotEmpty() - } -} diff --git a/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetAppInstallationPermissionUseCase.kt b/app/src/main/java/foundation/e/apps/domain/parentalcontrol/GetAppInstallationPermissionUseCase.kt new file mode 100644 index 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/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index d3976c568f6960c5117eeb665456e63c04e696b6..981cd623d7120863a9d7f0ceed3e80ac673512b5 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -35,7 +35,7 @@ import foundation.e.apps.R import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.blockedApps.BlockedAppRepository -import foundation.e.apps.data.blockedApps.ContentRatingsRepository +import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository import foundation.e.apps.data.ecloud.EcloudRepository import foundation.e.apps.data.enums.User import foundation.e.apps.data.enums.isInitialized @@ -60,7 +60,7 @@ class MainActivityViewModel @Inject constructor( private val pwaManager: PWAManager, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, - private val contentRatingsRepository: ContentRatingsRepository, + private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val appInstallProcessor: AppInstallProcessor, ) : ViewModel() { @@ -233,7 +233,7 @@ class MainActivityViewModel @Inject constructor( fun updateContentRatings() { viewModelScope.launch { - contentRatingsRepository.fetchContentRatingData() + googlePlayContentRatingsRepository.fetchContentRatingData() } } diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index 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..d5c65ee0f4c77150e5e132d80c2b29b98c577cf8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,6 +127,7 @@ [%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. + 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..7f46db25168156a56d01fb162ee70ab4c8fc0e1e --- /dev/null +++ b/app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.parentalcontrol + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.ContentRating +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed +import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied +import foundation.e.apps.data.parentalcontrol.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 kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +class GetAppInstallationPermissionUseCaseTest { + + // Run tasks synchronously + @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() + + // Sets the main coroutines dispatcher to a TestCoroutineScope for unit testing. + @ExperimentalCoroutinesApi @get:Rule var mainCoroutineRule = MainCoroutineRule() + + private lateinit var useCase: GetAppInstallationPermissionUseCase + + @Mock private lateinit var applicationRepository: ApplicationRepository + + @Mock private lateinit var dataStoreManager: DataStoreManager + + @Mock private lateinit var contentRatingsRepository: GooglePlayContentRatingsRepository + + @Mock private lateinit var getParentalControlStateUseCase: GetParentalControlStateUseCase + + @Mock private lateinit var playStoreRepository: PlayStoreRepository + + @Mock private lateinit var authenticatorRepository: AuthenticatorRepository + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + useCase = + GetAppInstallationPermissionUseCaseImpl( + applicationRepository, + dataStoreManager, + contentRatingsRepository, + getParentalControlStateUseCase, + playStoreRepository) + } + + @Test + fun `allow app installation when parental control is disabled`() { + runTest { + val appPendingInstallation = AppInstall() + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.Disabled) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `allow app installation when parental control is disabled and F-Droid app has no anti-features`() { + runTest { + val appPendingInstallation = + AppInstall().apply { + isFDroidApp = true + antiFeatures = emptyList() + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.Disabled) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `allow app installation when parental control is disabled and F-Droid app has anti-features other than NSFW`() { + runTest { + val appPendingInstallation = + AppInstall().apply { + isFDroidApp = true + antiFeatures = + listOf( + mapOf( + "NonFreeAssets" to + "Artwork, layouts and prerecorded voices are under a non-commercial license")) + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.Disabled) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `allow app installation when parental control is enabled and F-Droid app has anti-features other than NSFW`() { + runTest { + val appPendingInstallation = + AppInstall().apply { + isFDroidApp = true + antiFeatures = + listOf( + mapOf( + "NonFreeAssets" to + "Artwork, layouts and prerecorded voices are under a non-commercial license")) + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `deny app installation when parental control is enabled and F-Droid app has NSFW anti-features`() { + runTest { + val appPendingInstallation = + AppInstall().apply { + isFDroidApp = true + antiFeatures = listOf(mapOf("NSFW" to "Shows explicit content.")) + } + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Denied, installationPermissionState) + } + } + + @Test + fun `allow app installation when parental control is enabled and Google Play app's rating is equal to child's age group`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "Rated for 3+" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "THREE", + ageGroup = "0-3", + ratings = + listOf("rated for 3+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.OK)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Allowed, installationPermissionState) + } + } + + @Test + fun `deny app installation when parental control is enabled and Google Play app's rating exceeds child's age group`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "Rated for 7+" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "THREE", + ageGroup = "0-3", + ratings = + listOf("rated for 3+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.OK)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Denied, installationPermissionState) + } + } + + @Test + fun `deny app installation on data load error when parental control is enabled and Google Play app has no content rating`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "THREE", + ageGroup = "0-3", + ratings = + listOf("rated for 3+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.OK)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(DeniedOnDataLoadError, installationPermissionState) + } + } + + @Test + fun `deny app installation on data load error when parental control is enabled and Google Play app has no content rating and app details can't be loaded`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "THREE", + ageGroup = "0-3", + ratings = + listOf("rated for 3+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.UNKNOWN)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(DeniedOnDataLoadError, installationPermissionState) + } + } + + // Note: This case is unlikely to happen + @Test + fun `deny app installation when parental control is enabled and parental control state can't match with Google Play content ratings`() { + runTest { + val appPackage = "com.unit.test" + val contentRatingTitle = "Rated for 3+" + val contentRatingId = contentRatingTitle.lowercase() + + val googlePlayContentRatingGroup = + listOf( + GooglePlayContentRatingGroup( + id = "EIGHTEEN", + ageGroup = "18+", + ratings = + listOf("rated for 18+") // ratings will be parsed as lowercase in real + )) + + val email = "test@test.com" + val token = "token" + + val contentRating = ContentRating(title = contentRatingTitle) + val contentRatingWithId = + ContentRating(id = contentRatingId, title = contentRatingTitle) + + val appPendingInstallation: AppInstall = + AppInstall(packageName = appPackage).apply { + this.isFDroidApp = false + this.contentRating = contentRating + } + + val application = Application(isFDroidApp = false, contentRating = contentRating) + + val authData = AuthData(email, token) + + Mockito.`when`(getParentalControlStateUseCase.invoke()) + .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) + + Mockito.`when`(contentRatingsRepository.contentRatingGroups) + .thenReturn(googlePlayContentRatingGroup) + + Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) + + Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) + + Mockito.`when`( + applicationRepository.getApplicationDetails( + appPendingInstallation.id, + appPendingInstallation.packageName, + authData, + appPendingInstallation.origin)) + .thenReturn(Pair(application, ResultStatus.UNKNOWN)) + + Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) + .thenReturn(contentRatingWithId) + + val installationPermissionState = useCase.invoke(appPendingInstallation) + + assertEquals(Denied, installationPermissionState) + } + } +}