diff --git a/app/src/main/java/foundation/e/apps/data/database/AppDatabase.kt b/app/src/main/java/foundation/e/apps/data/database/AppDatabase.kt index 69c179b19378071c80eea13226560792d6892351..76e2cba0c5d18bbbad150bdb382c7cf24944a619 100644 --- a/app/src/main/java/foundation/e/apps/data/database/AppDatabase.kt +++ b/app/src/main/java/foundation/e/apps/data/database/AppDatabase.kt @@ -4,24 +4,39 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.TypeConverters import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import foundation.e.apps.data.database.install.AppInstallConverter import foundation.e.apps.data.exodus.Tracker import foundation.e.apps.data.exodus.TrackerDao import foundation.e.apps.data.faultyApps.FaultyApp import foundation.e.apps.data.faultyApps.FaultyAppDao import foundation.e.apps.data.fdroid.FdroidDao import foundation.e.apps.data.fdroid.models.FdroidEntity +import foundation.e.apps.data.parentalcontrol.ContentRatingEntity +import foundation.e.apps.data.parentalcontrol.ContentRatingDao +import foundation.e.apps.data.parentalcontrol.FDroidNsfwApp +import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingGroup @Database( - entities = [Tracker::class, FdroidEntity::class, FaultyApp::class], - version = 4, + entities = [ + Tracker::class, + FdroidEntity::class, + FaultyApp::class, + ContentRatingEntity::class, + FDroidNsfwApp::class, + GPlayContentRatingGroup::class, + ], + version = 5, exportSchema = false ) +@TypeConverters(AppInstallConverter::class) abstract class AppDatabase : RoomDatabase() { abstract fun trackerDao(): TrackerDao abstract fun fdroidDao(): FdroidDao abstract fun faultyAppsDao(): FaultyAppDao + abstract fun contentRatingDao(): ContentRatingDao companion object { private lateinit var INSTANCE: AppDatabase diff --git a/app/src/main/java/foundation/e/apps/data/database/install/AppInstallConverter.kt b/app/src/main/java/foundation/e/apps/data/database/install/AppInstallConverter.kt index c2bca4df307290f191a110b34ceeefea37ec5d94..ab0ec0f29832866d2f2ba84a86bf29aa9df4d250 100644 --- a/app/src/main/java/foundation/e/apps/data/database/install/AppInstallConverter.kt +++ b/app/src/main/java/foundation/e/apps/data/database/install/AppInstallConverter.kt @@ -1,30 +1,43 @@ package foundation.e.apps.data.database.install import androidx.room.TypeConverter +import com.aurora.gplayapi.data.models.ContentRating import com.aurora.gplayapi.data.models.File import com.google.gson.Gson import com.google.gson.reflect.TypeToken class AppInstallConverter { + private val gson = Gson() + @TypeConverter - fun listToJsonString(value: List): String = Gson().toJson(value) + fun listToJsonString(value: List): String = gson.toJson(value) @TypeConverter fun jsonStringToList(value: String) = - Gson().fromJson(value, Array::class.java).toMutableList() + gson.fromJson(value, Array::class.java).toMutableList() @TypeConverter - fun listToJsonLong(value: MutableMap): String = Gson().toJson(value) + fun listToJsonLong(value: MutableMap): String = gson.toJson(value) @TypeConverter fun jsonLongToList(value: String): MutableMap = - Gson().fromJson(value, object : TypeToken>() {}.type) + gson.fromJson(value, object : TypeToken>() {}.type) @TypeConverter - fun filesToJsonString(value: List): String = Gson().toJson(value) + fun filesToJsonString(value: List): String = gson.toJson(value) @TypeConverter fun jsonStringToFiles(value: String) = - Gson().fromJson(value, Array::class.java).toMutableList() + gson.fromJson(value, Array::class.java).toMutableList() + + @TypeConverter + fun fromContentRating(contentRating: ContentRating): String { + return gson.toJson(contentRating) + } + + @TypeConverter + fun toContentRating(name: String): ContentRating { + return gson.fromJson(name, ContentRating::class.java) + } } diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManager.kt b/app/src/main/java/foundation/e/apps/data/install/AppManager.kt index f479335a420a52f70d17fd4625b1f76a08633658..a37de7778ad1a7a05306eb6c69d88e194f06c519 100644 --- a/app/src/main/java/foundation/e/apps/data/install/AppManager.kt +++ b/app/src/main/java/foundation/e/apps/data/install/AppManager.kt @@ -39,7 +39,7 @@ interface AppManager { suspend fun installApp(appInstall: AppInstall) - suspend fun cancelDownload(appInstall: AppInstall) + suspend fun cancelDownload(appInstall: AppInstall, packageName: String = "") suspend fun getFusedDownload(downloadId: Long = 0, packageName: String = ""): AppInstall fun flushOldDownload(packageName: String) diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt b/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt index c857f5c3f9ec490f4070d32d367a9a009850957f..1757e7d2f62504a80900211bc3d29febc6b56d42 100644 --- a/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt @@ -32,6 +32,8 @@ import foundation.e.apps.R 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.ContentRatingDao +import foundation.e.apps.data.parentalcontrol.ContentRatingEntity import foundation.e.apps.install.download.data.DownloadProgressLD import foundation.e.apps.install.pkg.PWAManager import foundation.e.apps.install.pkg.AppLoungePackageManager @@ -58,6 +60,9 @@ class AppManagerImpl @Inject constructor( @ApplicationContext private val context: Context ) : AppManager { + @Inject + lateinit var contentRatingDao: ContentRatingDao + private val mutex = Mutex() @RequiresApi(Build.VERSION_CODES.O) @@ -89,6 +94,7 @@ class AppManagerImpl @Inject constructor( override suspend fun updateDownloadStatus(appInstall: AppInstall, status: Status) { if (status == Status.INSTALLED) { appInstall.status = status + insertContentRating(appInstall) flushOldDownload(appInstall.packageName) appInstallRepository.deleteDownload(appInstall) } else if (status == Status.INSTALLING) { @@ -99,6 +105,17 @@ class AppManagerImpl @Inject constructor( } } + private suspend fun insertContentRating(appInstall: AppInstall) { + contentRatingDao.insertContentRating( + ContentRatingEntity( + appInstall.packageName, + appInstall.contentRating.id, + appInstall.contentRating.title + ) + ) + Timber.d("inserted age rating: ${appInstall.contentRating.title}") + } + override suspend fun downloadApp(appInstall: AppInstall) { mutex.withLock { when (appInstall.type) { @@ -137,13 +154,14 @@ class AppManagerImpl @Inject constructor( } @OptIn(DelicateCoroutinesApi::class) - override suspend fun cancelDownload(appInstall: AppInstall) { + override suspend fun cancelDownload(appInstall: AppInstall, packageName: String) { mutex.withLock { if (appInstall.id.isNotBlank()) { removeFusedDownload(appInstall) } else { Timber.d("Unable to cancel download!") } + contentRatingDao.deleteContentRating(packageName) } } diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt b/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt index 7714a8062e31b71b34cab82fd3ba3d113056cda0..83cc3f3f9c3c4e36f4180eb5809e87ecf03a7c29 100644 --- a/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt +++ b/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt @@ -84,8 +84,8 @@ class AppManagerWrapper @Inject constructor( return appManager.updateDownloadStatus(appInstall, status) } - suspend fun cancelDownload(appInstall: AppInstall) { - return appManager.cancelDownload(appInstall) + suspend fun cancelDownload(appInstall: AppInstall, packageName: String = "") { + return appManager.cancelDownload(appInstall, packageName) } suspend fun installationIssue(appInstall: AppInstall) { 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..13477a2f42bedf5e0661220c786758a3c4025a8b 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 @@ -3,8 +3,10 @@ package foundation.e.apps.data.install.models import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey +import androidx.room.TypeConverter import com.aurora.gplayapi.data.models.ContentRating import com.aurora.gplayapi.data.models.File +import com.google.gson.Gson import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.enums.Status @@ -27,7 +29,8 @@ data class AppInstall( val isFree: Boolean = true, var appSize: Long = 0, var files: List = mutableListOf(), - var signature: String = String() + var signature: String = String(), + var contentRating: ContentRating = ContentRating() ) { @Ignore private val installingStatusList = listOf( @@ -37,9 +40,6 @@ data class AppInstall( Status.INSTALLING ) - @Ignore - var contentRating: ContentRating = ContentRating() - fun isAppInstalling() = installingStatusList.contains(status) fun isAwaiting() = status == Status.AWAITING diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/ContentRatingDao.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/ContentRatingDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..9568f1e96573bb7cef1585d16281ebe601cc3f30 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/ContentRatingDao.kt @@ -0,0 +1,68 @@ +/* + * 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 androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingGroup + +@Entity +data class ContentRatingEntity( + @PrimaryKey val packageName: String, + val ratingId: String, + val ratingTitle: String, +) + +@Entity +data class FDroidNsfwApp( + @PrimaryKey val packageName: String +) + +@Dao +interface ContentRatingDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertContentRatingGroups(ageGroups: List) + + @Query("SELECT * FROM GPlayContentRatingGroup") + suspend fun getAllContentRatingGroups(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertFDroidNsfwApp(nsfwApps: List) + + @Query("SELECT * FROM FDroidNsfwApp") + suspend fun getAllFDroidNsfwApp(): List + + @Query("DELETE FROM FDroidNsfwApp") + suspend fun clearFDroidNsfwApps() + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertContentRating(contentRatingEntity: ContentRatingEntity) + + @Query("DELETE FROM ContentRatingEntity WHERE packageName = :packageName") + suspend fun deleteContentRating(packageName: String) + + @Query("SELECT * FROM ContentRatingEntity WHERE packageName = :packageName LIMIT 1") + suspend fun getContentRating(packageName: String): ContentRatingEntity? + +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidAntiFeatureRepository.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidAntiFeatureRepository.kt index 5043d8acce197e2ff72c7c85ebacf18f4ab1123e..a8bbce78c1494af69329b0a42a5fc9984bacf967 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidAntiFeatureRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidAntiFeatureRepository.kt @@ -18,6 +18,8 @@ package foundation.e.apps.data.parentalcontrol.fdroid +import foundation.e.apps.data.parentalcontrol.ContentRatingDao +import foundation.e.apps.data.parentalcontrol.FDroidNsfwApp import javax.inject.Inject import javax.inject.Singleton @@ -26,11 +28,32 @@ class FDroidAntiFeatureRepository @Inject constructor( private val fDroidMonitorApi: FDroidMonitorApi, + private val contentRatingDao: ContentRatingDao, ) { var fDroidNsfwApps = listOf() private set suspend fun fetchNsfwApps() { - fDroidNsfwApps = fDroidMonitorApi.getMonitorData().body()?.getNsfwApps() ?: emptyList() + val fetchedFDroidNsfwApps = getFromApi() + + fDroidNsfwApps = if (fetchedFDroidNsfwApps.isEmpty()) { + getFromDb() + } else { + contentRatingDao.clearFDroidNsfwApps() + fetchedFDroidNsfwApps.also { pkgNames -> + contentRatingDao.insertFDroidNsfwApp(pkgNames.map { FDroidNsfwApp(it) }) + } + } + } + + private suspend fun getFromDb(): List { + return contentRatingDao.getAllFDroidNsfwApp().map { it.packageName } } + + private suspend fun getFromApi(): List { + return runCatching { + fDroidMonitorApi.getMonitorData().body()?.getNsfwApps() ?: emptyList() + }.getOrElse { emptyList() } + } + } diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingGroup.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingGroup.kt index 708e1f2780f97a125461f5550c7feec1579a424d..a7e7bec49c6bfc2302b354fae350b606060b260c 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingGroup.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingGroup.kt @@ -18,11 +18,14 @@ package foundation.e.apps.data.parentalcontrol.googleplay +import androidx.room.Entity +import androidx.room.PrimaryKey import com.squareup.moshi.Json +@Entity data class GPlayContentRatingGroup( - val id: String, + @PrimaryKey val id: String, @Json(name = "age_group") val ageGroup: String, - var ratings: List + var ratings: List = listOf() ) diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingRepository.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingRepository.kt index 28d03c09d3c8670d071b767bad18811f62a54670..a2421d6264c3321fa467293e217f5648ac3ad307 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingRepository.kt @@ -20,6 +20,7 @@ package foundation.e.apps.data.parentalcontrol.googleplay import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.handleNetworkResult +import foundation.e.apps.data.parentalcontrol.ContentRatingDao import foundation.e.apps.data.playstore.PlayStoreRepository import javax.inject.Inject import javax.inject.Singleton @@ -28,6 +29,7 @@ import javax.inject.Singleton class GPlayContentRatingRepository @Inject constructor( private val ageGroupApi: AgeGroupApi, private val playStoreRepository: PlayStoreRepository, + private val contentRatingDao: ContentRatingDao, ) { private var _contentRatingGroups = listOf() @@ -35,12 +37,27 @@ class GPlayContentRatingRepository @Inject constructor( get() = _contentRatingGroups suspend fun fetchContentRatingData() { - val response = ageGroupApi.getDefinedAgeGroups() - if (response.isSuccessful) { - _contentRatingGroups = response.body() ?: emptyList() + val fetchedContentRatingGroups = getFromApi() + + _contentRatingGroups = if (fetchedContentRatingGroups.isEmpty()) { + getFromDb() + } else { + fetchedContentRatingGroups.also { + contentRatingDao.insertContentRatingGroups(it) + } } } + private suspend fun getFromDb(): List { + return contentRatingDao.getAllContentRatingGroups() + } + + private suspend fun getFromApi(): List { + return runCatching { + ageGroupApi.getDefinedAgeGroups().body() ?: emptyList() + }.getOrElse { emptyList() } + } + suspend fun getEnglishContentRating(packageName: String): ContentRating? { return handleNetworkResult { playStoreRepository.getEnglishContentRating(packageName) diff --git a/app/src/main/java/foundation/e/apps/di/DaoModule.kt b/app/src/main/java/foundation/e/apps/di/DaoModule.kt index 738869c22030db7bce5ef8fd6c705f9dfdd8e64d..c58bbde22d4d7c233ad6c69a1fa96fe9e8a4facc 100644 --- a/app/src/main/java/foundation/e/apps/di/DaoModule.kt +++ b/app/src/main/java/foundation/e/apps/di/DaoModule.kt @@ -10,6 +10,7 @@ import foundation.e.apps.data.database.AppDatabase import foundation.e.apps.data.exodus.TrackerDao import foundation.e.apps.data.faultyApps.FaultyAppDao import foundation.e.apps.data.fdroid.FdroidDao +import foundation.e.apps.data.parentalcontrol.ContentRatingDao @InstallIn(SingletonComponent::class) @Module @@ -28,4 +29,9 @@ object DaoModule { fun getFaultyAppsDao(@ApplicationContext context: Context): FaultyAppDao { return AppDatabase.getInstance(context).faultyAppsDao() } + + @Provides + fun getContentRatingDao(@ApplicationContext context: Context): ContentRatingDao { + return AppDatabase.getInstance(context).contentRatingDao() + } } diff --git a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index 05cbe5c93da3ebcf26020b62e410ef0f8907156a..89c0c51e91b80d9e6c40290c424c74baadadb00f 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -18,6 +18,7 @@ package foundation.e.apps.domain +import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.apps.AppsApi import foundation.e.apps.data.parentalcontrol.Age @@ -26,8 +27,10 @@ import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository +import foundation.e.apps.data.parentalcontrol.ContentRatingDao import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingGroup import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository +import foundation.e.apps.domain.model.ContentRatingValidity import timber.log.Timber import javax.inject.Inject @@ -36,21 +39,30 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private val fDroidAntiFeatureRepository: FDroidAntiFeatureRepository, private val parentalControlRepository: ParentalControlRepository, private val appsApi: AppsApi, + private val contentRatingDao: ContentRatingDao, ) { companion object { const val KEY_ANTI_FEATURES_NSFW = "NSFW" } - suspend operator fun invoke(app: AppInstall): ResultSupreme { + suspend operator fun invoke(app: AppInstall): ResultSupreme { val ageGroup = parentalControlRepository.getSelectedAgeGroup() return when { - isParentalControlDisabled(ageGroup) -> ResultSupreme.Success(data = true) - isKnownNsfwApp(app) -> ResultSupreme.Success(data = false) - isCleanApkApp(app) -> ResultSupreme.Success(!isNsfwAppByCleanApkApi(app)) - isWhiteListedCleanApkApp(app) -> ResultSupreme.Success(data = true) - // Check for GPlay apps now + isParentalControlDisabled(ageGroup) -> ResultSupreme.Success( + data = ContentRatingValidity(true,) + ) + + isKnownNsfwApp(app) -> ResultSupreme.Success(data = ContentRatingValidity(false)) + isCleanApkApp(app) -> ResultSupreme.Success( + ContentRatingValidity(!isNsfwAppByCleanApkApi(app)) + ) + + isWhiteListedCleanApkApp(app) -> ResultSupreme.Success( + data = ContentRatingValidity(true) + ) + hasNoContentRatingOnGPlay(app) -> ResultSupreme.Error() else -> validateAgeLimit(ageGroup, app) } @@ -81,21 +93,29 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private fun validateAgeLimit( ageGroup: Age, app: AppInstall - ): ResultSupreme.Success { + ): ResultSupreme { val allowedContentRating = gPlayContentRatingRepository.contentRatingGroups.find { it.id == ageGroup.toString() } Timber.d( - "Selected age group: $ageGroup \n" + - "Content rating: ${app.contentRating.id} \n" + + "${app.packageName} - Content rating: ${app.contentRating.id} \n" + + "Selected age group: $ageGroup \n" + "Allowed content rating: $allowedContentRating" ) - return ResultSupreme.Success(isValidAppAgeRating(app, allowedContentRating)) + return ResultSupreme.Success( + ContentRatingValidity( + isValidAppAgeRating( + app, + allowedContentRating + ), app.contentRating + ) + ) } - private suspend fun hasNoContentRatingOnGPlay(app: AppInstall) = - !verifyContentRatingExists(app) + private suspend fun hasNoContentRatingOnGPlay(app: AppInstall): Boolean { + return app.origin == Origin.GPLAY && !verifyContentRatingExists(app) + } private fun isValidAppAgeRating( app: AppInstall, @@ -110,10 +130,20 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private suspend fun verifyContentRatingExists(app: AppInstall): Boolean { if (app.contentRating.id.isEmpty()) { - gPlayContentRatingRepository.getEnglishContentRating(app.packageName)?.run { - Timber.d("Updating content rating for package: ${app.packageName}") - app.contentRating = this - } + val fetchedContentRating = + gPlayContentRatingRepository.getEnglishContentRating(app.packageName) + + Timber.d("Fetched content rating - ${app.packageName} - ${fetchedContentRating?.id}") + + app.contentRating = if (fetchedContentRating == null) { + val contentRatingDb = contentRatingDao.getContentRating(app.packageName) + Timber.d("Content rating from DB - ${app.packageName} - ${contentRatingDb?.ratingId}") + ContentRating( + id = contentRatingDb?.ratingId ?: "", + title = contentRatingDb?.ratingTitle ?: "", + ) + } else fetchedContentRating + } return app.contentRating.title.isNotEmpty() && diff --git a/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt b/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt new file mode 100644 index 0000000000000000000000000000000000000000..57ff5b5c5879e521f9e3407408dc7a5f77beb6bd --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt @@ -0,0 +1,24 @@ +/* + * 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.domain.model + +import com.aurora.gplayapi.data.models.ContentRating + +data class ContentRatingValidity(val isValid: Boolean, val contentRating: ContentRating? = null) \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/install/pkg/PkgManagerBR.kt b/app/src/main/java/foundation/e/apps/install/pkg/PkgManagerBR.kt index 515b2703661176b78477dcce6b4a3827a69da3ca..77748b560491811bcb8762c13ccbc6f753927e21 100644 --- a/app/src/main/java/foundation/e/apps/install/pkg/PkgManagerBR.kt +++ b/app/src/main/java/foundation/e/apps/install/pkg/PkgManagerBR.kt @@ -54,42 +54,38 @@ open class PkgManagerBR : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { val action = intent?.action if (context != null && action != null) { - val packageUid = intent.getIntExtra(Intent.EXTRA_UID, 0) val isUpdating = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) - val packages = context.packageManager.getPackagesForUid(packageUid) val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -69) - val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) + val packageName = intent.data?.schemeSpecificPart Timber.d("onReceive: $packageName $action $extra $status") - packages?.let { pkgList -> - handlePackageList(pkgList, action, isUpdating, extra) + packageName?.let { + handlePackageList(it, action, isUpdating, extra) } } } private fun handlePackageList( - pkgList: Array, + packageName: String, action: String, isUpdating: Boolean, extra: String? ) { - pkgList.forEach { pkgName -> - when (action) { - Intent.ACTION_PACKAGE_ADDED -> { - updateDownloadStatus(pkgName) - removeFaultyAppByPackageName(pkgName) - } - - Intent.ACTION_PACKAGE_REMOVED -> { - if (!isUpdating) deleteDownload(pkgName) - removeFaultyAppByPackageName(pkgName) - } - - AppLoungePackageManager.ERROR_PACKAGE_INSTALL -> { - Timber.e("Installation failed due to error: $extra") - updateInstallationIssue(pkgName) - } + when (action) { + Intent.ACTION_PACKAGE_ADDED -> { + updateDownloadStatus(packageName) + removeFaultyAppByPackageName(packageName) + } + + Intent.ACTION_PACKAGE_REMOVED -> { + if (!isUpdating) deleteDownload(packageName) + removeFaultyAppByPackageName(packageName) + } + + AppLoungePackageManager.ERROR_PACKAGE_INSTALL -> { + Timber.e("Installation failed due to error: $extra") + updateInstallationIssue(packageName) } } } @@ -103,7 +99,7 @@ open class PkgManagerBR : BroadcastReceiver() { private fun deleteDownload(pkgName: String) { GlobalScope.launch { val fusedDownload = appManagerWrapper.getFusedDownload(packageName = pkgName) - appManagerWrapper.cancelDownload(fusedDownload) + appManagerWrapper.cancelDownload(fusedDownload, pkgName) } } 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 77fbf8b44d88a5a5ff9c0d5e1a5b21086c1049b4..83d9d7e940f1f48b3dc58ecf854d6929fb3f8691 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 @@ -132,7 +132,7 @@ class AppInstallProcessor @Inject constructor( } val ageLimitValidationResult = validateAppAgeLimitUseCase.invoke(appInstall) - if (ageLimitValidationResult.data != true) { + if (ageLimitValidationResult.data?.isValid != true) { if (ageLimitValidationResult.isSuccess()) { Timber.i("Content rating is not allowed for: ${appInstall.name}") EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(appInstall.name)) diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 86fdf8c15065f71e9482aa7c07ee02087fab5db1..f7cda6a029410cdd4cb795333369b4692c413ad3 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -39,13 +39,17 @@ 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.ResultSupreme 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.ContentRatingDao +import foundation.e.apps.data.parentalcontrol.ContentRatingEntity import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.domain.model.ContentRatingValidity import foundation.e.apps.install.pkg.AppLoungePackageManager import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async @@ -66,6 +70,7 @@ class AgeRatingProvider : ContentProvider() { fun provideValidateAppAgeLimitUseCase(): ValidateAppAgeLimitUseCase fun provideDataStoreManager(): DataStoreManager fun provideNotificationManager(): NotificationManager + fun provideContentRatingDao(): ContentRatingDao } companion object { @@ -80,6 +85,7 @@ class AgeRatingProvider : ContentProvider() { private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase private lateinit var dataStoreManager: DataStoreManager private lateinit var notificationManager: NotificationManager + private lateinit var contentRatingDao: ContentRatingDao private enum class UriCode(val code: Int) { LoginType(1), @@ -204,7 +210,22 @@ class AgeRatingProvider : ContentProvider() { origin = Origin.GPLAY ) val validateResult = validateAppAgeLimitUseCase.invoke(fakeAppInstall) - return validateResult.data + saveContentRatingIfInvalid(validateResult, packageName) + + return validateResult.data?.isValid + } + + private suspend fun saveContentRatingIfInvalid( + validateResult: ResultSupreme, + packageName: String + ) { + if (validateResult.data?.isValid == false) { + val ratingId = validateResult.data?.contentRating?.id ?: "" + val ratingTitle = validateResult.data?.contentRating?.title ?: "" + contentRatingDao.insertContentRating( + ContentRatingEntity(packageName, ratingId, ratingTitle) + ) + } } private suspend fun isAppValidRegardingNSWF(packageName: String): Boolean { @@ -213,7 +234,7 @@ class AgeRatingProvider : ContentProvider() { origin = Origin.CLEANAPK, ) val validateResult = validateAppAgeLimitUseCase.invoke(fakeAppInstall) - return validateResult.data ?: false + return validateResult.data?.isValid ?: false } private suspend fun shouldAllow(packageName: String): Boolean { @@ -236,6 +257,7 @@ class AgeRatingProvider : ContentProvider() { }.awaitAll() validityList.forEachIndexed { index: Int, isValid: Boolean? -> if (isValid != true) { + // Collect package names for blocklist cursor.addRow(arrayOf(packageNames[index])) } @@ -256,6 +278,7 @@ class AgeRatingProvider : ContentProvider() { validateAppAgeLimitUseCase = hiltEntryPoint.provideValidateAppAgeLimitUseCase() dataStoreManager = hiltEntryPoint.provideDataStoreManager() notificationManager = hiltEntryPoint.provideNotificationManager() + contentRatingDao = hiltEntryPoint.provideContentRatingDao() return true } diff --git a/app/src/test/java/foundation/e/apps/fusedManager/FakeAppManager.kt b/app/src/test/java/foundation/e/apps/fusedManager/FakeAppManager.kt index f8f1c97d97ff984fca242d7092c98643c3cf30d4..20abb9f03a489bcc4c0a07ae309a09cc2e5e5728 100644 --- a/app/src/test/java/foundation/e/apps/fusedManager/FakeAppManager.kt +++ b/app/src/test/java/foundation/e/apps/fusedManager/FakeAppManager.kt @@ -59,7 +59,7 @@ class FakeAppManager(private val appInstallDAO: AppInstallDAO) : AppManager { TODO("Not yet implemented") } - override suspend fun cancelDownload(appInstall: AppInstall) { + override suspend fun cancelDownload(appInstall: AppInstall, packageName: String) { TODO("Not yet implemented") } 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..5d893eb60d086c3632f67da56f35607f86cb6992 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -32,6 +32,7 @@ import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.domain.model.ContentRatingValidity import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.workmanager.AppInstallProcessor @@ -194,7 +195,7 @@ class AppInstallProcessorTest { fun `processInstallTest when age limit is satisfied`() = runTest { val fusedDownload = initTest() Mockito.`when`(validateAppAgeRatingUseCase.invoke(fusedDownload)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, true)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) val finalFusedDownload = runProcessInstall(fusedDownload) assertEquals("processInstall", finalFusedDownload, null) @@ -204,7 +205,7 @@ class AppInstallProcessorTest { fun `processInstallTest when age limit is not satisfied`() = runTest { val fusedDownload = initTest() Mockito.`when`(validateAppAgeRatingUseCase.invoke(fusedDownload)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, false)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(false))) val finalFusedDownload = runProcessInstall(fusedDownload) assertEquals("processInstall", finalFusedDownload, null) diff --git a/app/src/test/java/foundation/e/apps/installProcessor/FakeAppManagerWrapper.kt b/app/src/test/java/foundation/e/apps/installProcessor/FakeAppManagerWrapper.kt index b392cca60e417579a476de78bdca5b5014a3fffb..eb026c5bac6f4d212f7119367ee84c543b075a42 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/FakeAppManagerWrapper.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/FakeAppManagerWrapper.kt @@ -103,7 +103,7 @@ class FakeAppManagerWrapper( return installationStatus } - override suspend fun cancelDownload(appInstall: AppInstall) { + override suspend fun cancelDownload(appInstall: AppInstall, packageName: String) { fusedDownloadDAO.deleteDownload(appInstall) } }