From 05d7a306e0ce9c5b0a91269080daa61e025ce6f7 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 28 May 2024 20:18:48 +0600 Subject: [PATCH 01/11] feat: initial work of blocking install --- .../java/foundation/e/apps/MainActivity.kt | 17 ++++ .../e/apps/data/ParentalControlRepository.kt | 77 +++++++++++++++++++ .../e/apps/data/blockedApps/ContentRating.kt | 29 +++++++ .../blockedApps/ContentRatingsRepository.kt | 77 +++++++++++++++++++ .../fusedDownload/models/FusedDownload.kt | 4 + .../foundation/e/apps/di/UseCaseModule.kt | 43 +++++++++++ .../workmanager/AppInstallProcessor.kt | 12 ++- .../e/apps/ui/MainActivityViewModel.kt | 8 ++ .../e/apps/utils/eventBus/AppEvent.kt | 2 + app/src/main/res/values/strings.xml | 1 + 10 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt create mode 100644 app/src/main/java/foundation/e/apps/data/blockedApps/ContentRating.kt create mode 100644 app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt create mode 100644 app/src/main/java/foundation/e/apps/di/UseCaseModule.kt diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index dfc6b776b..6f8972504 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -120,6 +120,7 @@ class MainActivity : AppCompatActivity() { } viewModel.updateAppWarningList() + viewModel.updateContentRatings() observeEvents() } @@ -204,6 +205,22 @@ class MainActivity : AppCompatActivity() { launch { observeNoInternetEvent() } + + launch { + EventBus.events.filter { + it is AppEvent.AgeRateLimit + }.collectLatest { + ApplicationDialogFragment( + getString(R.string.unknown_error), + getString(R.string.age_rate_limit_message), + positiveButtonText = getString(R.string.ok), + positiveButtonAction = { + findNavController(binding.fragment.id).popBackStack() + } + ).show(supportFragmentManager, TAG) + + } + } } } } diff --git a/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt b/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt new file mode 100644 index 000000000..c9c0ca937 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt @@ -0,0 +1,77 @@ +/* + * 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 + +class ParentalControlRepository { + + fun isParentalControlEnabled(): Boolean { + return true + } + + fun getAllowedContentRatings(): List { + return listOf( + "PEGI 3", + "EVERYONE", + "All ages", + "General", + "All ages", + "For all", + "PEGI 3", + "Parental Guidance Recommended", +// "EVERYONE", +// "All ages", +// "General", +// "All ages", +// "For all", +// "Rated for 3+", +// "PEGI 3", +// "PEGI 7", +// "Parental Guidance Recommended", +// "EVERYONE", +// "All ages", +// "USK: Ages 6 and above", +// "General", +// "All ages", +// "For all", +// "Rated for 3+", +// "Rated for 7+", +// "PEGI 3", +// "PEGI 7", +// "PEGI 12", +// "Parental Guidance Recommended", +// "EVERYONE", +// "EVERYONE 10+", +// "TEEN", +// "All ages", +// "USK: Ages 6 and above", +// "USK: Ages 12 and above", +// "General", +// "Parental Guidance", +// "All ages", +// "Rated 10+", +// "Rated 12+", +// "For all", +// "Rated 12+", +// "Rated for 3+", +// "Rated for 7+", +// "Rated for 12+" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRating.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRating.kt new file mode 100644 index 000000000..b2553f19a --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRating.kt @@ -0,0 +1,29 @@ +/* + * 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 ContentRating( + val id: String, + @SerializedName("age_group") + val ageGroup: String, + val 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 new file mode 100644 index 000000000..6f554b657 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -0,0 +1,77 @@ +/* + * 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.reflect.TypeToken +import foundation.e.apps.data.DownloadManager +import foundation.e.apps.data.fusedDownload.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 gson: Gson, + @Named("cacheDir") private val cacheDir: String +) { + + private var _contentRatings = listOf() + val contentRatingsList: List + get() = _contentRatings + + 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) { + parseContentRatingData() + } + } + } + + private fun parseContentRatingData() { + _contentRatings = try { + val outputPath = "$cacheDir/warning_list/" + FileManager.moveFile("$cacheDir/", + CONTENT_RATINGS_FILE_NAME, outputPath) + val downloadedFile = File(outputPath + CONTENT_RATINGS_FILE_NAME) + Timber.d("Blocked list file exists: ${downloadedFile.exists()}") + val blockedAppInfoJson = String(downloadedFile.inputStream().readBytes()) + Timber.d("Blocked list file contents: $blockedAppInfoJson") + + val contentRatingsListType = object : TypeToken>() {}.type + gson.fromJson(blockedAppInfoJson, contentRatingsListType) + } catch (exception: Exception) { + Timber.e(exception.localizedMessage ?: "", exception) + mutableListOf() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/data/fusedDownload/models/FusedDownload.kt b/app/src/main/java/foundation/e/apps/data/fusedDownload/models/FusedDownload.kt index 905d4182b..2068ee014 100644 --- a/app/src/main/java/foundation/e/apps/data/fusedDownload/models/FusedDownload.kt +++ b/app/src/main/java/foundation/e/apps/data/fusedDownload/models/FusedDownload.kt @@ -3,6 +3,7 @@ package foundation.e.apps.data.fusedDownload.models import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey +import com.aurora.gplayapi.data.models.ContentRating import com.aurora.gplayapi.data.models.File import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.enums.Origin @@ -36,6 +37,9 @@ data class FusedDownload( Status.INSTALLING ) + @Ignore + var contentRating: ContentRating? = null + fun isAppInstalling() = installingStatusList.contains(status) fun isAwaiting() = status == Status.AWAITING diff --git a/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt b/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt new file mode 100644 index 000000000..c37c84100 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright MURENA SAS 2024 + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.CheckAppAgeLimitUseCase +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object UseCaseModule { + +// @Singleton +// @Provides +// fun provideGetAppsUseCase( +// appsRepository: ApplicationRepository, +// dataStoreManager: DataStoreManager +// ): CheckAppAgeLimitUseCase { +// return CheckAppAgeLimitUseCase(appsRepository, dataStoreManager) +// } +} \ No newline at end of file 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 ea98da521..ca6bce3f2 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 @@ -22,7 +22,6 @@ import android.content.Context import com.aurora.gplayapi.exceptions.ApiException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R -import foundation.e.apps.data.DownloadManager import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Status @@ -35,6 +34,7 @@ import foundation.e.apps.data.fusedDownload.FusedManagerRepository import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.CheckAppAgeLimitUseCase import foundation.e.apps.install.download.DownloadManagerUtils import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.updates.UpdatesNotifier @@ -55,6 +55,7 @@ class AppInstallProcessor @Inject constructor( private val fusedDownloadRepository: FusedDownloadRepository, private val fusedManagerRepository: FusedManagerRepository, private val applicationRepository: ApplicationRepository, + private val checkAppAgeLimitUseCase: CheckAppAgeLimitUseCase, private val dataStoreManager: DataStoreManager, private val storageNotificationManager: StorageNotificationManager, ) { @@ -96,6 +97,8 @@ class AppInstallProcessor @Inject constructor( application.originalSize ) + fusedDownload.contentRating = application.contentRating + if (fusedDownload.type == Type.PWA) { fusedDownload.downloadURLList = mutableListOf(application.url) } @@ -116,6 +119,13 @@ class AppInstallProcessor @Inject constructor( ) { try { val authData = dataStoreManager.getAuthData() + + if (checkAppAgeLimitUseCase.invoke(fusedDownload)) { + Timber.i("Content rating is not allowed for: ${fusedDownload.name}") + EventBus.invokeEvent(AppEvent.AgeRateLimit(fusedDownload.name)) + return + } + if (!fusedDownload.isFree && authData.isAnonymous) { EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message)) return 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 9a54ffd78..e81aec4b2 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -35,6 +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.ecloud.EcloudRepository import foundation.e.apps.data.enums.User import foundation.e.apps.data.enums.isInitialized @@ -59,6 +60,7 @@ class MainActivityViewModel @Inject constructor( private val pwaManager: PWAManager, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, + private val contentRatingsRepository: ContentRatingsRepository, private val appInstallProcessor: AppInstallProcessor, ) : ViewModel() { @@ -229,6 +231,12 @@ class MainActivityViewModel @Inject constructor( } } + fun updateContentRatings() { + viewModelScope.launch { + contentRatingsRepository.fetchContentRatingData() + } + } + fun getAppNameByPackageName(packageName: String): String { return appLoungePackageManager.getAppNameFromPackageName(packageName) } diff --git a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt index f7fbf663a..9bc7e1a5a 100644 --- a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt @@ -33,4 +33,6 @@ sealed class AppEvent(val data: Any) { class AppPurchaseEvent(fusedDownload: FusedDownload) : AppEvent(fusedDownload) class NoInternetEvent(isInternetAvailable: Boolean) : AppEvent(isInternetAvailable) class TooManyRequests : AppEvent(Unit) + + class AgeRateLimit(title: String) : AppEvent(String) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 344bb732c..fe0ce01ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -223,4 +223,5 @@ Split Install channel Sign in Ignore + You are too young to be able to install this app. Please check with your parent your age group is correct or disable Parental Control to be able to install it. \ No newline at end of file -- GitLab From 0a8da0101ebecd392d759ae1168a5ed843bce7cd Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 29 May 2024 19:31:22 +0600 Subject: [PATCH 02/11] Added ContentRating related repositories --- .../java/foundation/e/apps/MainActivity.kt | 3 - .../e/apps/data/ParentalControlRepository.kt | 77 ------------------- .../blockedApps/ContentRatingsRepository.kt | 9 +-- .../blockedApps/ParentalControlRepository.kt | 59 ++++++++++++++ .../foundation/e/apps/di/UseCaseModule.kt | 43 ----------- .../e/apps/domain/CheckAppAgeLimitUseCase.kt | 73 ++++++++++++++++++ .../workmanager/AppInstallProcessor.kt | 13 ++-- 7 files changed, 143 insertions(+), 134 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt create mode 100644 app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt delete mode 100644 app/src/main/java/foundation/e/apps/di/UseCaseModule.kt create mode 100644 app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 6f8972504..5f8f079a8 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -214,9 +214,6 @@ class MainActivity : AppCompatActivity() { getString(R.string.unknown_error), getString(R.string.age_rate_limit_message), positiveButtonText = getString(R.string.ok), - positiveButtonAction = { - findNavController(binding.fragment.id).popBackStack() - } ).show(supportFragmentManager, TAG) } diff --git a/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt b/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt deleted file mode 100644 index c9c0ca937..000000000 --- a/app/src/main/java/foundation/e/apps/data/ParentalControlRepository.kt +++ /dev/null @@ -1,77 +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 - -class ParentalControlRepository { - - fun isParentalControlEnabled(): Boolean { - return true - } - - fun getAllowedContentRatings(): List { - return listOf( - "PEGI 3", - "EVERYONE", - "All ages", - "General", - "All ages", - "For all", - "PEGI 3", - "Parental Guidance Recommended", -// "EVERYONE", -// "All ages", -// "General", -// "All ages", -// "For all", -// "Rated for 3+", -// "PEGI 3", -// "PEGI 7", -// "Parental Guidance Recommended", -// "EVERYONE", -// "All ages", -// "USK: Ages 6 and above", -// "General", -// "All ages", -// "For all", -// "Rated for 3+", -// "Rated for 7+", -// "PEGI 3", -// "PEGI 7", -// "PEGI 12", -// "Parental Guidance Recommended", -// "EVERYONE", -// "EVERYONE 10+", -// "TEEN", -// "All ages", -// "USK: Ages 6 and above", -// "USK: Ages 12 and above", -// "General", -// "Parental Guidance", -// "All ages", -// "Rated 10+", -// "Rated 12+", -// "For all", -// "Rated 12+", -// "Rated for 3+", -// "Rated for 7+", -// "Rated for 12+" - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 6f554b657..480989de9 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -37,7 +37,7 @@ class ContentRatingsRepository @Inject constructor( ) { private var _contentRatings = listOf() - val contentRatingsList: List + val contentRatings: List get() = _contentRatings companion object { @@ -63,12 +63,11 @@ class ContentRatingsRepository @Inject constructor( FileManager.moveFile("$cacheDir/", CONTENT_RATINGS_FILE_NAME, outputPath) val downloadedFile = File(outputPath + CONTENT_RATINGS_FILE_NAME) - Timber.d("Blocked list file exists: ${downloadedFile.exists()}") - val blockedAppInfoJson = String(downloadedFile.inputStream().readBytes()) - Timber.d("Blocked list file contents: $blockedAppInfoJson") + val contentRatingJson = String(downloadedFile.inputStream().readBytes()) + Timber.d("ContentRatings file contents: $contentRatingJson") val contentRatingsListType = object : TypeToken>() {}.type - gson.fromJson(blockedAppInfoJson, contentRatingsListType) + gson.fromJson(contentRatingJson, contentRatingsListType) } catch (exception: Exception) { Timber.e(exception.localizedMessage ?: "", exception) mutableListOf() 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 new file mode 100644 index 000000000..9a4e56be9 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt @@ -0,0 +1,59 @@ +/* + * 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(): Ages? { + 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 Ages.values()[ageOrdinal] + } + } + + return null + } +} + +enum class Ages { + THREE, + SIX, + ELEVEN, + FIFTEEN, + SEVENTEEN, +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt b/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt deleted file mode 100644 index c37c84100..000000000 --- a/app/src/main/java/foundation/e/apps/di/UseCaseModule.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * Apps Quickly and easily install Android apps onto your device! - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package foundation.e.apps.di - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.preference.DataStoreManager -import foundation.e.apps.domain.CheckAppAgeLimitUseCase -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object UseCaseModule { - -// @Singleton -// @Provides -// fun provideGetAppsUseCase( -// appsRepository: ApplicationRepository, -// dataStoreManager: DataStoreManager -// ): CheckAppAgeLimitUseCase { -// return CheckAppAgeLimitUseCase(appsRepository, dataStoreManager) -// } -} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt new file mode 100644 index 000000000..1ca1849f0 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt @@ -0,0 +1,73 @@ +/* + * 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 + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.application.ApplicationRepository +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.fusedDownload.models.FusedDownload +import foundation.e.apps.data.preference.DataStoreManager +import timber.log.Timber +import javax.inject.Inject + +class CheckAppAgeLimitUseCase @Inject constructor( + private val applicationRepository: ApplicationRepository, + private val dataStoreManager: DataStoreManager, + private val contentRatingRepository: ContentRatingsRepository, + private val parentalControlRepository: ParentalControlRepository +) { + + suspend operator fun invoke(fusedDownload: FusedDownload): Boolean { + val authData = dataStoreManager.getAuthData() + if (fusedDownload.contentRating?.title?.isEmpty() == true) { + updateContentRating(fusedDownload, authData) + } + + val selectedAgeGroup = parentalControlRepository.getSelectedAgeGroup() + val allowedContentRating = contentRatingRepository.contentRatings.find { + it.id == selectedAgeGroup.toString() + } + + Timber.d("Selected age group: $selectedAgeGroup \n" + + "Content rating: ${fusedDownload.contentRating?.title} \n" + + "Allowed content rating: $allowedContentRating") + return selectedAgeGroup != null + && fusedDownload.contentRating?.title?.isNotEmpty() == true + && allowedContentRating?.ratings?.contains(fusedDownload.contentRating!!.title) == false + } + + private suspend fun updateContentRating( + fusedDownload: FusedDownload, + authData: AuthData + ) { + applicationRepository.getApplicationDetails( + fusedDownload.id, + fusedDownload.packageName, + authData, + fusedDownload.origin + ).let { (appDetails, resultStatus) -> + if (resultStatus == ResultStatus.OK) { + fusedDownload.contentRating = appDetails.contentRating + } + } + } +} \ No newline at end of file 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 ca6bce3f2..b655c2c52 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 @@ -120,12 +120,6 @@ class AppInstallProcessor @Inject constructor( try { val authData = dataStoreManager.getAuthData() - if (checkAppAgeLimitUseCase.invoke(fusedDownload)) { - Timber.i("Content rating is not allowed for: ${fusedDownload.name}") - EventBus.invokeEvent(AppEvent.AgeRateLimit(fusedDownload.name)) - return - } - if (!fusedDownload.isFree && authData.isAnonymous) { EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message)) return @@ -139,6 +133,13 @@ class AppInstallProcessor @Inject constructor( return } + if (checkAppAgeLimitUseCase.invoke(fusedDownload)) { + Timber.i("Content rating is not allowed for: ${fusedDownload.name}") + EventBus.invokeEvent(AppEvent.AgeRateLimit(fusedDownload.name)) + fusedManagerRepository.cancelDownload(fusedDownload) + return + } + if (!context.isNetworkAvailable()) { fusedManagerRepository.installationIssue(fusedDownload) EventBus.invokeEvent(AppEvent.NoInternetEvent(false)) -- GitLab From b6579f8108419f403c720a68af65aa20e2a027dc Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 29 May 2024 22:49:33 +0600 Subject: [PATCH 03/11] refactor: agelimitrestrictionevent --- .../java/foundation/e/apps/MainActivity.kt | 23 +++++++++++-------- .../workmanager/AppInstallProcessor.kt | 2 +- .../e/apps/utils/eventBus/AppEvent.kt | 3 +-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 37301748d..55978abc3 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -207,21 +207,24 @@ class MainActivity : AppCompatActivity() { } launch { - EventBus.events.filter { - it is AppEvent.AgeRateLimit - }.collectLatest { - ApplicationDialogFragment( - getString(R.string.unknown_error), - getString(R.string.age_rate_limit_message), - positiveButtonText = getString(R.string.ok), - ).show(supportFragmentManager, TAG) - - } + observeAgeLimitRestrictionEvent() } } } } + private suspend fun observeAgeLimitRestrictionEvent() { + EventBus.events.filter { + it is AppEvent.AgeLimitRestrictionEvent + }.collectLatest { + ApplicationDialogFragment( + getString(R.string.unknown_error), + getString(R.string.age_rate_limit_message), + positiveButtonText = getString(R.string.ok), + ).show(supportFragmentManager, TAG) + } + } + private fun observePurchaseDeclined() { viewModel.purchaseDeclined.observe(this) { if (it.isNotEmpty()) { 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 72a9ace78..9b52caf24 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 @@ -135,7 +135,7 @@ class AppInstallProcessor @Inject constructor( if (checkAppAgeLimitUseCase.invoke(appInstall)) { Timber.i("Content rating is not allowed for: ${appInstall.name}") - EventBus.invokeEvent(AppEvent.AgeRateLimit(appInstall.name)) + EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(appInstall.name)) appManagerWrapper.cancelDownload(appInstall) return } diff --git a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt index d7643d0a3..68e4c2832 100644 --- a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt @@ -33,6 +33,5 @@ sealed class AppEvent(val data: Any) { class AppPurchaseEvent(appInstall: AppInstall) : AppEvent(appInstall) class NoInternetEvent(isInternetAvailable: Boolean) : AppEvent(isInternetAvailable) class TooManyRequests : AppEvent(Unit) - - class AgeRateLimit(title: String) : AppEvent(String) + class AgeLimitRestrictionEvent(title: String) : AppEvent(String) } -- GitLab From def7bb1005ccac2185b55e70a692c8bbb329e60d Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 30 May 2024 09:26:13 +0600 Subject: [PATCH 04/11] refactor: fix detekt issuees --- app/detekt-baseline.xml | 2 +- ...ContentRating.kt => ContentRatingGroup.kt} | 2 +- .../blockedApps/ContentRatingsRepository.kt | 28 ++++++----- .../blockedApps/ParentalControlRepository.kt | 2 +- .../e/apps/domain/CheckAppAgeLimitUseCase.kt | 4 +- .../e/apps/install/AppInstallComponents.kt | 31 ++++++++++++ .../workmanager/AppInstallProcessor.kt | 48 +++++++++---------- .../e/apps/utils/eventBus/AppEvent.kt | 2 +- 8 files changed, 77 insertions(+), 42 deletions(-) rename app/src/main/java/foundation/e/apps/data/blockedApps/{ContentRating.kt => ContentRatingGroup.kt} (96%) create mode 100644 app/src/main/java/foundation/e/apps/install/AppInstallComponents.kt diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 5feaed5a4..3c941c851 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 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 contentRatingsRepository: ContentRatingsRepository, private val appInstallProcessor: AppInstallProcessor, ) LongParameterList:UpdatesManagerImpl.kt$UpdatesManagerImpl$( @ApplicationContext private val context: Context, private val appLoungePackageManager: AppLoungePackageManager, private val applicationRepository: ApplicationRepository, private val faultyAppRepository: FaultyAppRepository, private val appLoungePreference: AppLoungePreference, private val fdroidRepository: FdroidRepository, private val blockedAppRepository: BlockedAppRepository, ) LongParameterList:UpdatesWorker.kt$UpdatesWorker$( @Assisted private val context: Context, @Assisted private val params: WorkerParameters, private val updatesManagerRepository: UpdatesManagerRepository, private val dataStoreManager: DataStoreManager, private val authenticatorRepository: AuthenticatorRepository, private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, ) MagicNumber:AnonymousLoginManager.kt$AnonymousLoginManager$200 diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRating.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt similarity index 96% rename from app/src/main/java/foundation/e/apps/data/blockedApps/ContentRating.kt rename to app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt index b2553f19a..7d472591f 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRating.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt @@ -21,7 +21,7 @@ package foundation.e.apps.data.blockedApps import com.google.gson.annotations.SerializedName -data class ContentRating( +data class ContentRatingGroup( val id: String, @SerializedName("age_group") val ageGroup: String, diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index a21259a0c..43df8b11f 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -20,6 +20,7 @@ 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 @@ -36,13 +37,14 @@ class ContentRatingsRepository @Inject constructor( @Named("cacheDir") private val cacheDir: String ) { - private var _contentRatings = listOf() - val contentRatings: List - get() = _contentRatings + 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" + "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" } @@ -58,7 +60,7 @@ class ContentRatingsRepository @Inject constructor( } private fun parseContentRatingData() { - _contentRatings = try { + _contentRatingGroups = try { val outputPath = "$cacheDir/warning_list/" FileManager.moveFile("$cacheDir/", CONTENT_RATINGS_FILE_NAME, outputPath) @@ -66,11 +68,15 @@ class ContentRatingsRepository @Inject constructor( val contentRatingJson = String(downloadedFile.inputStream().readBytes()) Timber.d("ContentRatings file contents: $contentRatingJson") - val contentRatingsListType = object : TypeToken>() {}.type - gson.fromJson(contentRatingJson, contentRatingsListType) - } catch (exception: Exception) { - Timber.e(exception.localizedMessage ?: "", exception) - mutableListOf() + val contentRatingsListTypeGroup = object : TypeToken>() {}.type + gson.fromJson(contentRatingJson, contentRatingsListTypeGroup) + } catch (exception: JsonSyntaxException) { + handleException(exception) } } -} \ No newline at end of file + + private fun handleException(exception: Exception): MutableList { + Timber.e(exception.localizedMessage ?: "", exception) + return mutableListOf() + } +} 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 index 9a4e56be9..07824e6be 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt @@ -56,4 +56,4 @@ enum class Ages { ELEVEN, FIFTEEN, SEVENTEEN, -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt index 158d81e89..259c02910 100644 --- a/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt @@ -43,7 +43,7 @@ class CheckAppAgeLimitUseCase @Inject constructor( } val selectedAgeGroup = parentalControlRepository.getSelectedAgeGroup() - val allowedContentRating = contentRatingRepository.contentRatings.find { + val allowedContentRating = contentRatingRepository.contentRatingGroups.find { it.id == selectedAgeGroup.toString() } @@ -70,4 +70,4 @@ class CheckAppAgeLimitUseCase @Inject constructor( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/install/AppInstallComponents.kt b/app/src/main/java/foundation/e/apps/install/AppInstallComponents.kt new file mode 100644 index 000000000..dfe3f93ac --- /dev/null +++ b/app/src/main/java/foundation/e/apps/install/AppInstallComponents.kt @@ -0,0 +1,31 @@ +/* + * Copyright MURENA SAS 2024 + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.install + +import foundation.e.apps.data.install.AppInstallRepository +import foundation.e.apps.data.install.AppManagerWrapper +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AppInstallComponents @Inject constructor( + val appInstallRepository: AppInstallRepository, + val appManagerWrapper: AppManagerWrapper +) 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 9b52caf24..58b257258 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 @@ -29,12 +29,11 @@ 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.install.AppInstallRepository -import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.domain.CheckAppAgeLimitUseCase +import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.download.DownloadManagerUtils import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.updates.UpdatesNotifier @@ -52,8 +51,7 @@ import javax.inject.Inject class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, - private val appInstallRepository: AppInstallRepository, - private val appManagerWrapper: AppManagerWrapper, + private val appInstallComponents: AppInstallComponents, private val applicationRepository: ApplicationRepository, private val checkAppAgeLimitUseCase: CheckAppAgeLimitUseCase, private val dataStoreManager: DataStoreManager, @@ -127,7 +125,7 @@ class AppInstallProcessor @Inject constructor( if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) return - val downloadAdded = appManagerWrapper.addDownload(appInstall) + val downloadAdded = appInstallComponents.appManagerWrapper.addDownload(appInstall) if (!downloadAdded) { Timber.i("Update adding ABORTED! status: $downloadAdded") return @@ -135,13 +133,13 @@ class AppInstallProcessor @Inject constructor( if (checkAppAgeLimitUseCase.invoke(appInstall)) { Timber.i("Content rating is not allowed for: ${appInstall.name}") - EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(appInstall.name)) - appManagerWrapper.cancelDownload(appInstall) + EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent()) + appInstallComponents.appManagerWrapper.cancelDownload(appInstall) return } if (!context.isNetworkAvailable()) { - appManagerWrapper.installationIssue(appInstall) + appInstallComponents.appManagerWrapper.installationIssue(appInstall) EventBus.invokeEvent(AppEvent.NoInternetEvent(false)) return } @@ -149,19 +147,19 @@ class AppInstallProcessor @Inject constructor( if (StorageComputer.spaceMissing(appInstall) > 0) { Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}") storageNotificationManager.showNotEnoughSpaceNotification(appInstall) - appManagerWrapper.installationIssue(appInstall) + appInstallComponents.appManagerWrapper.installationIssue(appInstall) EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) return } - appManagerWrapper.updateAwaiting(appInstall) + appInstallComponents.appManagerWrapper.updateAwaiting(appInstall) InstallWorkManager.enqueueWork(appInstall, isAnUpdate) } catch (e: Exception) { Timber.e( "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}", e ) - appManagerWrapper.installationIssue(appInstall) + appInstallComponents.appManagerWrapper.installationIssue(appInstall) } } @@ -170,7 +168,7 @@ class AppInstallProcessor @Inject constructor( try { updateFusedDownloadWithAppDownloadLink(appInstall) } catch (e: ApiException.AppNotPurchased) { - appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) + appInstallComponents.appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) EventBus.invokeEvent(AppEvent.AppPurchaseEvent(appInstall)) return false } catch (e: GplayHttpRequestException) { @@ -224,7 +222,7 @@ class AppInstallProcessor @Inject constructor( try { Timber.d("Fused download name $fusedDownloadId") - appInstall = appInstallRepository.getDownloadById(fusedDownloadId) + appInstall = appInstallComponents.appInstallRepository.getDownloadById(fusedDownloadId) Timber.i(">>> dowork started for Fused download name " + appInstall?.name + " " + fusedDownloadId) appInstall?.let { @@ -232,22 +230,22 @@ class AppInstallProcessor @Inject constructor( checkDownloadingState(appInstall) this.isItUpdateWork = - isItUpdateWork && appManagerWrapper.isFusedDownloadInstalled(appInstall) + isItUpdateWork && appInstallComponents.appManagerWrapper.isFusedDownloadInstalled(appInstall) if (!appInstall.isAppInstalling()) { Timber.d("!!! returned") return@let } - if (!appManagerWrapper.validateFusedDownload(appInstall)) { - appManagerWrapper.installationIssue(it) + if (!appInstallComponents.appManagerWrapper.validateFusedDownload(appInstall)) { + appInstallComponents.appManagerWrapper.installationIssue(it) Timber.d("!!! installationIssue") return@let } if (areFilesDownloadedButNotInstalled(appInstall)) { Timber.i("===> Downloaded But not installed ${appInstall.name}") - appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) + appInstallComponents.appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) } runInForeground?.invoke(it.name) @@ -260,7 +258,7 @@ class AppInstallProcessor @Inject constructor( e ) appInstall?.let { - appManagerWrapper.cancelDownload(appInstall) + appInstallComponents.appManagerWrapper.cancelDownload(appInstall) } } @@ -278,7 +276,7 @@ class AppInstallProcessor @Inject constructor( } private fun areFilesDownloadedButNotInstalled(appInstall: AppInstall) = - appInstall.areFilesDownloaded() && (!appManagerWrapper.isFusedDownloadInstalled( + appInstall.areFilesDownloaded() && (!appInstallComponents.appManagerWrapper.isFusedDownloadInstalled( appInstall ) || appInstall.status == Status.INSTALLING) @@ -288,7 +286,7 @@ class AppInstallProcessor @Inject constructor( if (isItUpdateWork) { appInstall?.let { val packageStatus = - appManagerWrapper.getFusedDownloadPackageStatus(appInstall) + appInstallComponents.appManagerWrapper.getFusedDownloadPackageStatus(appInstall) if (packageStatus == Status.INSTALLED) { UpdatesDao.addSuccessfullyUpdatedApp(it) @@ -303,7 +301,7 @@ class AppInstallProcessor @Inject constructor( } private suspend fun isUpdateCompleted(): Boolean { - val downloadListWithoutAnyIssue = appInstallRepository.getDownloadList().filter { + val downloadListWithoutAnyIssue = appInstallComponents.appInstallRepository.getDownloadList().filter { !listOf( Status.INSTALLATION_ISSUE, Status.PURCHASE_NEEDED ).contains(it.status) @@ -329,11 +327,11 @@ class AppInstallProcessor @Inject constructor( private suspend fun startAppInstallationProcess(appInstall: AppInstall) { if (appInstall.isAwaiting()) { - appManagerWrapper.downloadApp(appInstall) + appInstallComponents.appManagerWrapper.downloadApp(appInstall) Timber.i("===> doWork: Download started ${appInstall.name} ${appInstall.status}") } - appInstallRepository.getDownloadFlowById(appInstall.id).transformWhile { + appInstallComponents.appInstallRepository.getDownloadFlowById(appInstall.id).transformWhile { emit(it) isInstallRunning(it) }.collect { latestFusedDownload -> @@ -373,7 +371,7 @@ class AppInstallProcessor @Inject constructor( val message = "Handling install status is failed for ${download.packageName} exception: ${e.localizedMessage}" Timber.e(message, e) - appManagerWrapper.installationIssue(download) + appInstallComponents.appManagerWrapper.installationIssue(download) finishInstallation(download) } } @@ -384,7 +382,7 @@ class AppInstallProcessor @Inject constructor( } Status.DOWNLOADED -> { - appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) + appInstallComponents.appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) } Status.INSTALLING -> { diff --git a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt index 68e4c2832..86fc27ed8 100644 --- a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt @@ -33,5 +33,5 @@ sealed class AppEvent(val data: Any) { class AppPurchaseEvent(appInstall: AppInstall) : AppEvent(appInstall) class NoInternetEvent(isInternetAvailable: Boolean) : AppEvent(isInternetAvailable) class TooManyRequests : AppEvent(Unit) - class AgeLimitRestrictionEvent(title: String) : AppEvent(String) + class AgeLimitRestrictionEvent : AppEvent(Unit) } -- GitLab From 1163d6831296f9d553ef24882a7d413103cd23f4 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 30 May 2024 14:06:31 +0600 Subject: [PATCH 05/11] updated logic of age restriction --- .../apps/data/application/data/Application.kt | 2 +- .../data/blockedApps/ContentRatingGroup.kt | 2 +- .../blockedApps/ContentRatingsRepository.kt | 6 ++- .../e/apps/data/install/models/AppInstall.kt | 2 +- .../data/playstore/PlayStoreRepository.kt | 2 +- .../data/playstore/PlayStoreRepositoryImpl.kt | 2 +- .../e/apps/domain/CheckAppAgeLimitUseCase.kt | 49 ++++++++++++------- .../workmanager/AppInstallProcessor.kt | 42 ++++++++++------ .../ui/application/ApplicationFragment.kt | 7 +-- .../ui/application/ApplicationViewModel.kt | 26 ++++++---- 10 files changed, 88 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index dd32f1a5d..98d8d56c9 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,7 @@ data class Application( var isGplayReplaced: Boolean = false, @SerializedName(value = "on_fdroid") val isFDroidApp: Boolean = false, - val contentRating: ContentRating = ContentRating() + var contentRating: ContentRating = ContentRating() ) { 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 index 7d472591f..cfb734abc 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.kt @@ -25,5 +25,5 @@ data class ContentRatingGroup( val id: String, @SerializedName("age_group") val ageGroup: String, - val ratings: List + var ratings: List ) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 43df8b11f..0a37e3a8f 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -69,7 +69,11 @@ class ContentRatingsRepository @Inject constructor( Timber.d("ContentRatings file contents: $contentRatingJson") val contentRatingsListTypeGroup = object : TypeToken>() {}.type - gson.fromJson(contentRatingJson, contentRatingsListTypeGroup) + val contentRatingGroups: List = gson.fromJson(contentRatingJson, contentRatingsListTypeGroup) + contentRatingGroups.map { + it.ratings = it.ratings.map { rating -> rating.lowercase()} + it + } } catch (exception: JsonSyntaxException) { handleException(exception) } 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 31ccd7446..28f92b4d6 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 @@ -38,7 +38,7 @@ data class AppInstall( ) @Ignore - var contentRating: ContentRating? = null + var contentRating: ContentRating = ContentRating() fun isAppInstalling() = installingStatusList.contains(status) diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt index 798eb4daa..0239ce1f3 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt @@ -45,7 +45,7 @@ interface PlayStoreRepository : StoreRepository { offerType: Int ): List - suspend fun updateContentRatingWithId( + suspend fun getContentRatingWithId( appPackage: String, contentRating: ContentRating ): ContentRating diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt index d97181464..da8847721 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepositoryImpl.kt @@ -214,7 +214,7 @@ class PlayStoreRepositoryImpl @Inject constructor( return downloadData } - override suspend fun updateContentRatingWithId( + override suspend fun getContentRatingWithId( appPackage: String, contentRating: ContentRating ): ContentRating { diff --git a/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt index 259c02910..34bb6fdb7 100644 --- a/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt @@ -25,6 +25,7 @@ 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 @@ -33,41 +34,53 @@ class CheckAppAgeLimitUseCase @Inject constructor( private val applicationRepository: ApplicationRepository, private val dataStoreManager: DataStoreManager, private val contentRatingRepository: ContentRatingsRepository, - private val parentalControlRepository: ParentalControlRepository + private val parentalControlRepository: ParentalControlRepository, + private val playStoreRepository: PlayStoreRepository ) { suspend operator fun invoke(appInstall: AppInstall): Boolean { val authData = dataStoreManager.getAuthData() - if (appInstall.contentRating?.title?.isEmpty() == true) { - updateContentRating(appInstall, authData) - } + + verifyContentRatingExists(appInstall, authData) val selectedAgeGroup = parentalControlRepository.getSelectedAgeGroup() val allowedContentRating = contentRatingRepository.contentRatingGroups.find { it.id == selectedAgeGroup.toString() } - Timber.d("Selected age group: $selectedAgeGroup \n" + - "Content rating: ${appInstall.contentRating?.title} \n" + - "Allowed content rating: $allowedContentRating") + Timber.d( + "Selected age group: $selectedAgeGroup \n" + + "Content rating: ${appInstall.contentRating.id} \n" + + "Allowed content rating: $allowedContentRating" + ) return selectedAgeGroup != null - && appInstall.contentRating?.title?.isNotEmpty() == true - && allowedContentRating?.ratings?.contains(appInstall.contentRating!!.title) == false + && appInstall.contentRating.id.isNotEmpty() + && allowedContentRating?.ratings?.contains(appInstall.contentRating.id) == false } - private suspend fun updateContentRating( + private suspend fun verifyContentRatingExists( appInstall: AppInstall, authData: AuthData ) { - applicationRepository.getApplicationDetails( - appInstall.id, - appInstall.packageName, - authData, - appInstall.origin - ).let { (appDetails, resultStatus) -> - if (resultStatus == ResultStatus.OK) { - appInstall.contentRating = appDetails.contentRating + if (appInstall.contentRating.title.isEmpty()) { + applicationRepository.getApplicationDetails( + appInstall.id, + appInstall.packageName, + authData, + appInstall.origin + ).let { (appDetails, resultStatus) -> + if (resultStatus == ResultStatus.OK) { + appInstall.contentRating = appDetails.contentRating + } + // todo: handle unhappy path and return from this method } } + + if (appInstall.contentRating.id.isEmpty()) { + appInstall.contentRating = playStoreRepository.getContentRatingWithId( + appInstall.packageName, + appInstall.contentRating + ) + } } } 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 58b257258..a551aa65d 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 @@ -51,7 +51,7 @@ import javax.inject.Inject class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, - private val appInstallComponents: AppInstallComponents, + private val appInstallComponents: AppInstallComponents, private val applicationRepository: ApplicationRepository, private val checkAppAgeLimitUseCase: CheckAppAgeLimitUseCase, private val dataStoreManager: DataStoreManager, @@ -93,9 +93,9 @@ class AppInstallProcessor @Inject constructor( application.offer_type, application.isFree, application.originalSize - ) - - appInstall.contentRating = application.contentRating + ).also { + it.contentRating = application.contentRating + } if (appInstall.type == Type.PWA) { appInstall.downloadURLList = mutableListOf(application.url) @@ -230,7 +230,9 @@ class AppInstallProcessor @Inject constructor( checkDownloadingState(appInstall) this.isItUpdateWork = - isItUpdateWork && appInstallComponents.appManagerWrapper.isFusedDownloadInstalled(appInstall) + isItUpdateWork && appInstallComponents.appManagerWrapper.isFusedDownloadInstalled( + appInstall + ) if (!appInstall.isAppInstalling()) { Timber.d("!!! returned") @@ -245,7 +247,10 @@ class AppInstallProcessor @Inject constructor( if (areFilesDownloadedButNotInstalled(appInstall)) { Timber.i("===> Downloaded But not installed ${appInstall.name}") - appInstallComponents.appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) + appInstallComponents.appManagerWrapper.updateDownloadStatus( + appInstall, + Status.INSTALLING + ) } runInForeground?.invoke(it.name) @@ -301,11 +306,12 @@ class AppInstallProcessor @Inject constructor( } private suspend fun isUpdateCompleted(): Boolean { - val downloadListWithoutAnyIssue = appInstallComponents.appInstallRepository.getDownloadList().filter { - !listOf( - Status.INSTALLATION_ISSUE, Status.PURCHASE_NEEDED - ).contains(it.status) - } + val downloadListWithoutAnyIssue = + appInstallComponents.appInstallRepository.getDownloadList().filter { + !listOf( + Status.INSTALLATION_ISSUE, Status.PURCHASE_NEEDED + ).contains(it.status) + } return UpdatesDao.successfulUpdatedApps.isNotEmpty() && downloadListWithoutAnyIssue.isEmpty() } @@ -331,10 +337,11 @@ class AppInstallProcessor @Inject constructor( Timber.i("===> doWork: Download started ${appInstall.name} ${appInstall.status}") } - appInstallComponents.appInstallRepository.getDownloadFlowById(appInstall.id).transformWhile { - emit(it) - isInstallRunning(it) - }.collect { latestFusedDownload -> + appInstallComponents.appInstallRepository.getDownloadFlowById(appInstall.id) + .transformWhile { + emit(it) + isInstallRunning(it) + }.collect { latestFusedDownload -> handleFusedDownload(latestFusedDownload, appInstall) } } @@ -382,7 +389,10 @@ class AppInstallProcessor @Inject constructor( } Status.DOWNLOADED -> { - appInstallComponents.appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) + appInstallComponents.appManagerWrapper.updateDownloadStatus( + appInstall, + Status.INSTALLING + ) } Status.INSTALLING -> { 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 e2011f01b..bd9525936 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 @@ -170,7 +170,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { binding.applicationLayout.visibility = View.INVISIBLE - applicationViewModel.application.observe(viewLifecycleOwner) { resultPair -> + applicationViewModel.applicationLiveData.observe(viewLifecycleOwner) { resultPair -> + Timber.d("ApplicationLiveData: ${resultPair.first.contentRating}") updateUi(resultPair) } @@ -473,7 +474,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } private fun openShareSheet() { - val application = applicationViewModel.application.value?.first ?: return + val application = applicationViewModel.applicationLiveData.value?.first ?: return val shareIntent = AppShareIntent.create(application.name, application.shareUri) startActivity(Intent.createChooser(shareIntent, null)) } @@ -935,7 +936,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { return EXODUS_URL } - val reportId = applicationViewModel.application.value!!.first.reportId + val reportId = applicationViewModel.applicationLiveData.value!!.first.reportId return "$EXODUS_REPORT_URL${Locale.getDefault().language}/reports/$reportId" } diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt index 50869d835..22101b2c6 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt @@ -57,7 +57,7 @@ class ApplicationViewModel @Inject constructor( private val appManagerWrapper: AppManagerWrapper, ) : LoadingViewModel() { - val application: MutableLiveData> = MutableLiveData() + val applicationLiveData: MutableLiveData> = MutableLiveData() val appStatus: MutableLiveData = MutableLiveData() val downloadProgress = downloadProgressLD private val _errorMessageLiveData: MutableLiveData = MutableLiveData() @@ -118,7 +118,7 @@ class ApplicationViewModel @Inject constructor( authData, origin ) - application.postValue(appData) + applicationLiveData.postValue(appData) updateShareVisibilityState(appData.first.shareUri.toString()) updateAppContentRatingState(packageName, appData.first.contentRating) @@ -155,10 +155,18 @@ class ApplicationViewModel @Inject constructor( // Initially update the state without ID to show the UI immediately _appContentRatingState.update { contentRating } - val ratingWithId = playStoreRepository.updateContentRatingWithId(packageName, contentRating) + val ratingWithId = playStoreRepository.getContentRatingWithId(packageName, contentRating) + // Later, update with a new rating; no visual change in the UI - _appContentRatingState.update { contentRating.copy(id = ratingWithId.id) } + val updatedContentRating = contentRating.copy(id = ratingWithId.id) + _appContentRatingState.update { updatedContentRating } + + applicationLiveData.value?.copy()?.let { + val application = it.first + application.contentRating = updatedContentRating + applicationLiveData.postValue(it) + } } private fun updateShareVisibilityState(shareUri: String) { @@ -177,7 +185,7 @@ class ApplicationViewModel @Inject constructor( if (this.first.package_name.isBlank()) { _errorMessageLiveData.postValue(R.string.app_not_found) } else { - application.postValue(this) + applicationLiveData.postValue(this) updateShareVisibilityState(first.shareUri.toString()) } } @@ -189,7 +197,7 @@ class ApplicationViewModel @Inject constructor( fun transformPermsToString(): String { var permissionString = "" - application.value?.first?.let { + applicationLiveData.value?.first?.let { // Filter list to only keep platform permissions val filteredList = it.perms.filter { it.startsWith("android.permission.") @@ -205,7 +213,7 @@ class ApplicationViewModel @Inject constructor( } fun getFusedApp(): Application? { - return application.value?.first + return applicationLiveData.value?.first } fun handleRatingFormat(rating: Double): String { @@ -214,13 +222,13 @@ class ApplicationViewModel @Inject constructor( suspend fun calculateProgress(progress: DownloadProgress): Pair { return appManagerWrapper.getCalculateProgressWithTotalSize( - application.value?.first, + applicationLiveData.value?.first, progress ) } fun updateApplicationStatus(downloadList: List) { - application.value?.first?.let { app -> + applicationLiveData.value?.first?.let { app -> appStatus.value = appManagerWrapper.getDownloadingItemStatus(app, downloadList) ?: applicationRepository.getFusedAppInstallationStatus(app) } -- GitLab From e603f412b569f4773898ce9d53705223d312c72c Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 30 May 2024 17:25:51 +0600 Subject: [PATCH 06/11] handled error for agerating validation --- .../blockedApps/ContentRatingsRepository.kt | 26 +++++--- ...eCase.kt => ValidateAppAgeLimitUseCase.kt} | 59 ++++++++++++------- .../workmanager/AppInstallProcessor.kt | 15 +++-- 3 files changed, 66 insertions(+), 34 deletions(-) rename app/src/main/java/foundation/e/apps/domain/{CheckAppAgeLimitUseCase.kt => ValidateAppAgeLimitUseCase.kt} (58%) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 0a37e3a8f..6f1bf1f76 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -62,23 +62,33 @@ class ContentRatingsRepository @Inject constructor( private fun parseContentRatingData() { _contentRatingGroups = try { val outputPath = "$cacheDir/warning_list/" - FileManager.moveFile("$cacheDir/", - CONTENT_RATINGS_FILE_NAME, outputPath) + FileManager.moveFile( + "$cacheDir/", + CONTENT_RATINGS_FILE_NAME, outputPath + ) val downloadedFile = File(outputPath + CONTENT_RATINGS_FILE_NAME) val contentRatingJson = String(downloadedFile.inputStream().readBytes()) Timber.d("ContentRatings file contents: $contentRatingJson") - val contentRatingsListTypeGroup = object : TypeToken>() {}.type - val contentRatingGroups: List = gson.fromJson(contentRatingJson, contentRatingsListTypeGroup) - contentRatingGroups.map { - it.ratings = it.ratings.map { rating -> rating.lowercase()} - it - } + parseJsonOfContentRatingGroup(contentRatingJson) } catch (exception: JsonSyntaxException) { handleException(exception) } } + private fun parseJsonOfContentRatingGroup(contentRatingJson: String): List { + val contentRatingsListTypeGroup = object : TypeToken>() {}.type + val contentRatingGroups: List = + gson.fromJson(contentRatingJson, contentRatingsListTypeGroup) + + return contentRatingGroups.map { + it.ratings = it.ratings.map { rating -> + rating.lowercase() + } + it + } + } + private fun handleException(exception: Exception): MutableList { Timber.e(exception.localizedMessage ?: "", exception) return mutableListOf() diff --git a/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt similarity index 58% rename from app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt rename to app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index 34bb6fdb7..535402df2 100644 --- a/app/src/main/java/foundation/e/apps/domain/CheckAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -21,6 +21,8 @@ package foundation.e.apps.domain import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.blockedApps.Ages +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 @@ -30,7 +32,7 @@ import foundation.e.apps.data.preference.DataStoreManager import timber.log.Timber import javax.inject.Inject -class CheckAppAgeLimitUseCase @Inject constructor( +class ValidateAppAgeLimitUseCase @Inject constructor( private val applicationRepository: ApplicationRepository, private val dataStoreManager: DataStoreManager, private val contentRatingRepository: ContentRatingsRepository, @@ -38,10 +40,12 @@ class CheckAppAgeLimitUseCase @Inject constructor( private val playStoreRepository: PlayStoreRepository ) { - suspend operator fun invoke(appInstall: AppInstall): Boolean { + suspend operator fun invoke(appInstall: AppInstall): Pair { val authData = dataStoreManager.getAuthData() - verifyContentRatingExists(appInstall, authData) + if (!verifyContentRatingExists(appInstall, authData)) { + return Pair(false, ResultStatus.UNKNOWN) + } val selectedAgeGroup = parentalControlRepository.getSelectedAgeGroup() val allowedContentRating = contentRatingRepository.contentRatingGroups.find { @@ -53,34 +57,47 @@ class CheckAppAgeLimitUseCase @Inject constructor( "Content rating: ${appInstall.contentRating.id} \n" + "Allowed content rating: $allowedContentRating" ) - return selectedAgeGroup != null - && appInstall.contentRating.id.isNotEmpty() - && allowedContentRating?.ratings?.contains(appInstall.contentRating.id) == false + + val isAppAgeLimitedValidated = isParentalControlDisabled(selectedAgeGroup) + || isAppAgeRatingValid(appInstall, allowedContentRating) + return Pair(isAppAgeLimitedValidated, ResultStatus.OK) } + private fun isAppAgeRatingValid( + appInstall: AppInstall, + allowedContentRating: ContentRatingGroup? + ) = (appInstall.contentRating.id.isNotEmpty() + && allowedContentRating?.ratings?.contains(appInstall.contentRating.id) == true) + + private fun isParentalControlDisabled(selectedAgeGroup: Ages?) = + selectedAgeGroup == null + private suspend fun verifyContentRatingExists( appInstall: AppInstall, authData: AuthData - ) { + ): Boolean { if (appInstall.contentRating.title.isEmpty()) { - applicationRepository.getApplicationDetails( - appInstall.id, - appInstall.packageName, - authData, - appInstall.origin - ).let { (appDetails, resultStatus) -> - if (resultStatus == ResultStatus.OK) { - appInstall.contentRating = appDetails.contentRating + applicationRepository + .getApplicationDetails( + appInstall.id, appInstall.packageName, authData, appInstall.origin + ).let { (appDetails, resultStatus) -> + if (resultStatus == ResultStatus.OK) { + appInstall.contentRating = appDetails.contentRating + } else { + return false + } } - // todo: handle unhappy path and return from this method - } } if (appInstall.contentRating.id.isEmpty()) { - appInstall.contentRating = playStoreRepository.getContentRatingWithId( - appInstall.packageName, - appInstall.contentRating - ) + appInstall.contentRating = + playStoreRepository.getContentRatingWithId( + appInstall.packageName, + appInstall.contentRating + ) } + + return appInstall.contentRating.title.isNotEmpty() && + appInstall.contentRating.id.isNotEmpty() } } 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 a551aa65d..07b30f5bc 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 @@ -32,7 +32,7 @@ import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.DataStoreManager -import foundation.e.apps.domain.CheckAppAgeLimitUseCase +import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.download.DownloadManagerUtils import foundation.e.apps.install.notification.StorageNotificationManager @@ -53,7 +53,7 @@ class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, private val appInstallComponents: AppInstallComponents, private val applicationRepository: ApplicationRepository, - private val checkAppAgeLimitUseCase: CheckAppAgeLimitUseCase, + private val validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase, private val dataStoreManager: DataStoreManager, private val storageNotificationManager: StorageNotificationManager, ) { @@ -131,9 +131,14 @@ class AppInstallProcessor @Inject constructor( return } - if (checkAppAgeLimitUseCase.invoke(appInstall)) { - Timber.i("Content rating is not allowed for: ${appInstall.name}") - EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent()) + val (isAgeLimitValidated, resultStatus) = validateAppAgeLimitUseCase.invoke(appInstall) + if (!isAgeLimitValidated) { + if (resultStatus == ResultStatus.OK) { + Timber.i("Content rating is not allowed for: ${appInstall.name}") + EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent()) + } else { + //TODO trigger an error event + } appInstallComponents.appManagerWrapper.cancelDownload(appInstall) return } -- GitLab From 69fc05c7038f6f93442affb750209aa2e48c228b Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 30 May 2024 19:17:43 +0600 Subject: [PATCH 07/11] fixed and added unit test --- .../AppInstallProcessorTest.kt | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) 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 6c1a41974..7a2bae0e6 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -21,13 +21,17 @@ 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.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.preference.DataStoreManager +import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.workmanager.AppInstallProcessor import foundation.e.apps.util.MainCoroutineRule @@ -41,6 +45,7 @@ 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 { @@ -75,6 +80,9 @@ class AppInstallProcessorTest { private lateinit var appInstallProcessor: AppInstallProcessor + @Mock + private lateinit var validateAppAgeRatingUseCase: ValidateAppAgeLimitUseCase + @Mock private lateinit var storageNotificationManager: StorageNotificationManager @@ -85,12 +93,14 @@ class AppInstallProcessorTest { appInstallRepository = AppInstallRepository(fakeFusedDownloadDAO) fakeFusedManagerRepository = FakeAppManagerWrapper(fakeFusedDownloadDAO, fakeFusedManager, fakeFdroidRepository) + val appInstallComponents = + AppInstallComponents(appInstallRepository, fakeFusedManagerRepository) appInstallProcessor = AppInstallProcessor( context, - appInstallRepository, - fakeFusedManagerRepository, + appInstallComponents, applicationRepository, + validateAppAgeRatingUseCase, dataStoreManager, storageNotificationManager ) @@ -155,7 +165,10 @@ class AppInstallProcessorTest { fakeFusedManagerRepository.forceCrash = true val finalFusedDownload = runProcessInstall(fusedDownload) - assertTrue("processInstall", finalFusedDownload == null || fusedDownload.status == Status.INSTALLATION_ISSUE) + assertTrue( + "processInstall", + finalFusedDownload == null || fusedDownload.status == Status.INSTALLATION_ISSUE + ) } @Test @@ -176,6 +189,26 @@ class AppInstallProcessorTest { assertEquals("processInstall", Status.INSTALLATION_ISSUE, finalFusedDownload?.status) } + @Test + fun `processInstallTest when age limit is satisfied`() = runTest { + val fusedDownload = initTest() + Mockito.`when`(validateAppAgeRatingUseCase.invoke(fusedDownload)) + .thenReturn(Pair(false, ResultStatus.OK)) + + val finalFusedDownload = runProcessInstall(fusedDownload) + assertEquals("processInstall", finalFusedDownload, null) + } + + @Test + fun `processInstallTest when age limit is not satisfied`() = runTest { + val fusedDownload = initTest() + Mockito.`when`(validateAppAgeRatingUseCase.invoke(fusedDownload)) + .thenReturn(Pair(true, ResultStatus.OK)) + + val finalFusedDownload = runProcessInstall(fusedDownload) + assertEquals("processInstall", finalFusedDownload, null) + } + private suspend fun runProcessInstall(appInstall: AppInstall): AppInstall? { appInstallProcessor.processInstall(appInstall.id, false) return fakeFusedDownloadDAO.getDownloadById(appInstall.id) -- GitLab From e2dfd09edbb9b9f5beb62bbfa43e2258a2cca6a0 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 31 May 2024 10:55:27 +0600 Subject: [PATCH 08/11] error dialog is added for error event --- .../java/foundation/e/apps/MainActivity.kt | 19 +++++++++++++++++-- .../workmanager/AppInstallProcessor.kt | 3 ++- .../e/apps/utils/eventBus/AppEvent.kt | 1 + 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 55978abc3..4384a88fb 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -195,7 +195,11 @@ class MainActivity : AppCompatActivity() { } launch { - observerErrorEvent() + observeErrorEvent() + } + + launch { + observeErrorDialogEvent() } launch { @@ -372,7 +376,7 @@ class MainActivity : AppCompatActivity() { findNavController(R.id.fragment).navigate(action) } - private suspend fun observerErrorEvent() { + private suspend fun observeErrorEvent() { EventBus.events.filter { appEvent -> appEvent is AppEvent.ErrorMessageEvent }.collectLatest { @@ -380,6 +384,17 @@ class MainActivity : AppCompatActivity() { } } + private suspend fun observeErrorDialogEvent() { + EventBus.events.filter { appEvent -> + appEvent is AppEvent.ErrorMessageDialogEvent + }.collectLatest { + ApplicationDialogFragment( + title = getString(R.string.unknown_error), + message = getString(it.data as Int) + ).show(supportFragmentManager, TAG) + } + } + private suspend fun observeSignatureMissMatchError() { EventBus.events.filter { appEvent -> appEvent is AppEvent.SignatureMissMatchError 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 07b30f5bc..fe6b33507 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 @@ -137,8 +137,9 @@ class AppInstallProcessor @Inject constructor( Timber.i("Content rating is not allowed for: ${appInstall.name}") EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent()) } else { - //TODO trigger an error event + EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) } + appInstallComponents.appManagerWrapper.cancelDownload(appInstall) return } diff --git a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt index 86fc27ed8..935f434a6 100644 --- a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt @@ -30,6 +30,7 @@ sealed class AppEvent(val data: Any) { class InvalidAuthEvent(authName: String) : AppEvent(authName) class ErrorMessageEvent(stringResourceId: Int) : AppEvent(stringResourceId) + class ErrorMessageDialogEvent(stringResourceId: Int) : AppEvent(stringResourceId) class AppPurchaseEvent(appInstall: AppInstall) : AppEvent(appInstall) class NoInternetEvent(isInternetAvailable: Boolean) : AppEvent(isInternetAvailable) class TooManyRequests : AppEvent(Unit) -- GitLab From d9174c97dfd7ca44079289d65d246347a53def4c Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Mon, 3 Jun 2024 17:53:38 +0600 Subject: [PATCH 09/11] refactor: validation age limit --- .../java/foundation/e/apps/MainActivity.kt | 4 +- .../data/blockedApps/ContentRatingParser.kt | 89 +++++++++++++++++++ .../blockedApps/ContentRatingsRepository.kt | 40 +-------- .../blockedApps/ParentalControlRepository.kt | 9 +- .../apps/domain/ValidateAppAgeLimitUseCase.kt | 21 +++-- .../workmanager/AppInstallProcessor.kt | 8 +- .../e/apps/utils/eventBus/AppEvent.kt | 2 +- app/src/main/res/values/strings.xml | 4 +- .../AppInstallProcessorTest.kt | 5 +- 9 files changed, 121 insertions(+), 61 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 4384a88fb..61c632596 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -222,8 +222,8 @@ class MainActivity : AppCompatActivity() { it is AppEvent.AgeLimitRestrictionEvent }.collectLatest { ApplicationDialogFragment( - getString(R.string.unknown_error), - getString(R.string.age_rate_limit_message), + getString(R.string.restricted_app, it.data as String), + getString(R.string.age_rate_limit_message, it.data as String), positiveButtonText = getString(R.string.ok), ).show(supportFragmentManager, TAG) } diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt new file mode 100644 index 000000000..cde87e638 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt @@ -0,0 +1,89 @@ +/* + * 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.install.FileManager +import timber.log.Timber +import java.io.File +import java.io.IOException +import javax.inject.Inject +import javax.inject.Named + +class ContentRatingParser @Inject constructor( + private val gson: Gson, + @Named("cacheDir") private val cacheDir: String +) { + + companion object { + private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json" + } + + fun parseContentRatingData(): List { + return try { + val outputPath = moveFile() + val contentRatingJson = readJsonFromFile(outputPath) + Timber.d("ContentRatings file contents: $contentRatingJson") + parseJsonOfContentRatingGroup(contentRatingJson) + } catch (exception: IOException) { + handleException(exception) + } catch (exception: JsonSyntaxException) { + handleException(exception) + } + } + + private fun readJsonFromFile(outputPath: String): String { + val downloadedFile = + File(outputPath + CONTENT_RATINGS_FILE_NAME) + val contentRatingJson = String(downloadedFile.inputStream().readBytes()) + + return contentRatingJson + } + + private fun moveFile(): String { + val outputPath = "$cacheDir/content_ratings/" + FileManager.moveFile( + "$cacheDir/", + CONTENT_RATINGS_FILE_NAME, outputPath + ) + + return outputPath + } + + private fun parseJsonOfContentRatingGroup(contentRatingJson: String): List { + val contentRatingsListTypeGroup = object : TypeToken>() {}.type + val contentRatingGroups: List = + gson.fromJson(contentRatingJson, contentRatingsListTypeGroup) + + return contentRatingGroups.map { + it.ratings = it.ratings.map { rating -> + rating.lowercase() + } + it + } + } + + private fun handleException(exception: Exception): List { + Timber.e(exception.localizedMessage ?: "", exception) + return listOf() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 6f1bf1f76..44f17df51 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt @@ -33,8 +33,7 @@ import javax.inject.Singleton @Singleton class ContentRatingsRepository @Inject constructor( private val downloadManager: DownloadManager, - private val gson: Gson, - @Named("cacheDir") private val cacheDir: String + private val contentRatingParser: ContentRatingParser ) { private var _contentRatingGroups = listOf() @@ -54,43 +53,8 @@ class ContentRatingsRepository @Inject constructor( fileName = CONTENT_RATINGS_FILE_NAME ) { success, _ -> if (success) { - parseContentRatingData() + contentRatingParser.parseContentRatingData() } } } - - private fun parseContentRatingData() { - _contentRatingGroups = try { - val outputPath = "$cacheDir/warning_list/" - FileManager.moveFile( - "$cacheDir/", - CONTENT_RATINGS_FILE_NAME, outputPath - ) - val downloadedFile = File(outputPath + CONTENT_RATINGS_FILE_NAME) - val contentRatingJson = String(downloadedFile.inputStream().readBytes()) - Timber.d("ContentRatings file contents: $contentRatingJson") - - parseJsonOfContentRatingGroup(contentRatingJson) - } catch (exception: JsonSyntaxException) { - handleException(exception) - } - } - - private fun parseJsonOfContentRatingGroup(contentRatingJson: String): List { - val contentRatingsListTypeGroup = object : TypeToken>() {}.type - val contentRatingGroups: List = - gson.fromJson(contentRatingJson, contentRatingsListTypeGroup) - - return contentRatingGroups.map { - it.ratings = it.ratings.map { rating -> - rating.lowercase() - } - it - } - } - - private fun handleException(exception: Exception): MutableList { - Timber.e(exception.localizedMessage ?: "", exception) - return mutableListOf() - } } 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 index 07824e6be..6cab641e3 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt @@ -35,25 +35,26 @@ class ParentalControlRepository @Inject constructor( "content://foundation.e.parentalcontrol.provider/age" } - fun getSelectedAgeGroup(): Ages? { + 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 Ages.values()[ageOrdinal] + return Age.values()[ageOrdinal] } } - return null + return Age.PARENTAL_CONTROL_DISABLED } } -enum class Ages { +enum class Age { THREE, SIX, ELEVEN, FIFTEEN, SEVENTEEN, + PARENTAL_CONTROL_DISABLED } 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 535402df2..2d782ef6d 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -20,8 +20,9 @@ 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.Ages +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 @@ -40,14 +41,17 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private val playStoreRepository: PlayStoreRepository ) { - suspend operator fun invoke(appInstall: AppInstall): Pair { + suspend operator fun invoke(appInstall: AppInstall): ResultSupreme { val authData = dataStoreManager.getAuthData() + val selectedAgeGroup = parentalControlRepository.getSelectedAgeGroup() + if (isParentalControlDisabled(selectedAgeGroup)) { + return ResultSupreme.Success( true) + } if (!verifyContentRatingExists(appInstall, authData)) { - return Pair(false, ResultStatus.UNKNOWN) + return ResultSupreme.Error(false) } - val selectedAgeGroup = parentalControlRepository.getSelectedAgeGroup() val allowedContentRating = contentRatingRepository.contentRatingGroups.find { it.id == selectedAgeGroup.toString() } @@ -58,9 +62,8 @@ class ValidateAppAgeLimitUseCase @Inject constructor( "Allowed content rating: $allowedContentRating" ) - val isAppAgeLimitedValidated = isParentalControlDisabled(selectedAgeGroup) - || isAppAgeRatingValid(appInstall, allowedContentRating) - return Pair(isAppAgeLimitedValidated, ResultStatus.OK) + val isAppAgeLimitValid = isAppAgeRatingValid(appInstall, allowedContentRating) + return ResultSupreme.Success(isAppAgeLimitValid) } private fun isAppAgeRatingValid( @@ -69,8 +72,8 @@ class ValidateAppAgeLimitUseCase @Inject constructor( ) = (appInstall.contentRating.id.isNotEmpty() && allowedContentRating?.ratings?.contains(appInstall.contentRating.id) == true) - private fun isParentalControlDisabled(selectedAgeGroup: Ages?) = - selectedAgeGroup == null + private fun isParentalControlDisabled(selectedAgeGroup: Age?) = + selectedAgeGroup == Age.PARENTAL_CONTROL_DISABLED private suspend fun verifyContentRatingExists( appInstall: AppInstall, 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 fe6b33507..4a269e555 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 @@ -131,11 +131,11 @@ class AppInstallProcessor @Inject constructor( return } - val (isAgeLimitValidated, resultStatus) = validateAppAgeLimitUseCase.invoke(appInstall) - if (!isAgeLimitValidated) { - if (resultStatus == ResultStatus.OK) { + val ageLimitValidationResult = validateAppAgeLimitUseCase.invoke(appInstall) + if (ageLimitValidationResult.data == false) { + if (ageLimitValidationResult.isSuccess()) { Timber.i("Content rating is not allowed for: ${appInstall.name}") - EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent()) + EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(appInstall.name)) } else { EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) } diff --git a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt index 935f434a6..e3db13f9f 100644 --- a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt @@ -34,5 +34,5 @@ sealed class AppEvent(val data: Any) { class AppPurchaseEvent(appInstall: AppInstall) : AppEvent(appInstall) class NoInternetEvent(isInternetAvailable: Boolean) : AppEvent(isInternetAvailable) class TooManyRequests : AppEvent(Unit) - class AgeLimitRestrictionEvent : AppEvent(Unit) + class AgeLimitRestrictionEvent(type: String) : AppEvent(type) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fe0ce01ad..d01d12f84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -124,6 +124,9 @@ Having troubles? https://doc.e.foundation/support-topics/app_lounge_troubleshooting Share + [%1$s] Restricted App + You are too young to be able to install %1$s. Please check with your parent your age group is correct or disable Parental Control to be able to install it. + Update All @@ -223,5 +226,4 @@ Split Install channel Sign in Ignore - You are too young to be able to install this app. Please check with your parent your age group is correct or disable Parental Control to be able to install it. \ No newline at end of file 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 7a2bae0e6..480eeadda 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -22,6 +22,7 @@ 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 @@ -193,7 +194,7 @@ class AppInstallProcessorTest { fun `processInstallTest when age limit is satisfied`() = runTest { val fusedDownload = initTest() Mockito.`when`(validateAppAgeRatingUseCase.invoke(fusedDownload)) - .thenReturn(Pair(false, ResultStatus.OK)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, true)) val finalFusedDownload = runProcessInstall(fusedDownload) assertEquals("processInstall", finalFusedDownload, null) @@ -203,7 +204,7 @@ class AppInstallProcessorTest { fun `processInstallTest when age limit is not satisfied`() = runTest { val fusedDownload = initTest() Mockito.`when`(validateAppAgeRatingUseCase.invoke(fusedDownload)) - .thenReturn(Pair(true, ResultStatus.OK)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, false)) val finalFusedDownload = runProcessInstall(fusedDownload) assertEquals("processInstall", finalFusedDownload, null) -- GitLab From c5a15160d36e5010ada5385b2621437599b1527b Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 4 Jun 2024 21:59:33 +0600 Subject: [PATCH 10/11] refactor: modify ValidateAppAgeLimitUseCase code organization --- .../apps/domain/ValidateAppAgeLimitUseCase.kt | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index 2d782ef6d..37d5b8d82 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -1,19 +1,18 @@ /* - * Copyright MURENA SAS 2024 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2024 MURENA SAS * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * */ @@ -41,39 +40,43 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private val playStoreRepository: PlayStoreRepository ) { - suspend operator fun invoke(appInstall: AppInstall): ResultSupreme { + suspend operator fun invoke(app: AppInstall): ResultSupreme { val authData = dataStoreManager.getAuthData() - val selectedAgeGroup = parentalControlRepository.getSelectedAgeGroup() - if (isParentalControlDisabled(selectedAgeGroup)) { - return ResultSupreme.Success( true) - } + val ageGroup = parentalControlRepository.getSelectedAgeGroup() - if (!verifyContentRatingExists(appInstall, authData)) { - return ResultSupreme.Error(false) + return when { + isParentalControlDisabled(ageGroup) -> ResultSupreme.Success(data = true) + hasNoContentRating(app, authData) -> ResultSupreme.Error(data = false) + else -> validateAgeLimit(ageGroup, app) } + } - val allowedContentRating = contentRatingRepository.contentRatingGroups.find { - it.id == selectedAgeGroup.toString() - } + private fun validateAgeLimit( + selectedAgeGroup: Age, + app: AppInstall + ): ResultSupreme.Success { + val allowedContentRating = + contentRatingRepository.contentRatingGroups.find { it.id == selectedAgeGroup.toString() } Timber.d( "Selected age group: $selectedAgeGroup \n" + - "Content rating: ${appInstall.contentRating.id} \n" + + "Content rating: ${app.contentRating.id} \n" + "Allowed content rating: $allowedContentRating" ) - val isAppAgeLimitValid = isAppAgeRatingValid(appInstall, allowedContentRating) - return ResultSupreme.Success(isAppAgeLimitValid) + return ResultSupreme.Success(isValidAppAgeRating(app, allowedContentRating)) } - private fun isAppAgeRatingValid( - appInstall: AppInstall, + private suspend fun hasNoContentRating(app: AppInstall, authData: AuthData) = + !verifyContentRatingExists(app, authData) + + private fun isValidAppAgeRating( + app: AppInstall, allowedContentRating: ContentRatingGroup? - ) = (appInstall.contentRating.id.isNotEmpty() - && allowedContentRating?.ratings?.contains(appInstall.contentRating.id) == true) + ) = (app.contentRating.id.isNotEmpty() + && allowedContentRating?.ratings?.contains(app.contentRating.id) == true) - private fun isParentalControlDisabled(selectedAgeGroup: Age?) = - selectedAgeGroup == Age.PARENTAL_CONTROL_DISABLED + private fun isParentalControlDisabled(ageGroup: Age) = ageGroup == Age.PARENTAL_CONTROL_DISABLED private suspend fun verifyContentRatingExists( appInstall: AppInstall, -- GitLab From a8d9bcf789148dfffdcba8dc7d6027ed3996aebf Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 4 Jun 2024 22:03:31 +0600 Subject: [PATCH 11/11] refactor: rename parameters --- .../apps/domain/ValidateAppAgeLimitUseCase.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index 37d5b8d82..c859fe5e1 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -52,14 +52,14 @@ class ValidateAppAgeLimitUseCase @Inject constructor( } private fun validateAgeLimit( - selectedAgeGroup: Age, + ageGroup: Age, app: AppInstall ): ResultSupreme.Success { val allowedContentRating = - contentRatingRepository.contentRatingGroups.find { it.id == selectedAgeGroup.toString() } + contentRatingRepository.contentRatingGroups.find { it.id == ageGroup.toString() } Timber.d( - "Selected age group: $selectedAgeGroup \n" + + "Selected age group: $ageGroup \n" + "Content rating: ${app.contentRating.id} \n" + "Allowed content rating: $allowedContentRating" ) @@ -79,31 +79,31 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private fun isParentalControlDisabled(ageGroup: Age) = ageGroup == Age.PARENTAL_CONTROL_DISABLED private suspend fun verifyContentRatingExists( - appInstall: AppInstall, + app: AppInstall, authData: AuthData ): Boolean { - if (appInstall.contentRating.title.isEmpty()) { + if (app.contentRating.title.isEmpty()) { applicationRepository .getApplicationDetails( - appInstall.id, appInstall.packageName, authData, appInstall.origin + app.id, app.packageName, authData, app.origin ).let { (appDetails, resultStatus) -> if (resultStatus == ResultStatus.OK) { - appInstall.contentRating = appDetails.contentRating + app.contentRating = appDetails.contentRating } else { return false } } } - if (appInstall.contentRating.id.isEmpty()) { - appInstall.contentRating = + if (app.contentRating.id.isEmpty()) { + app.contentRating = playStoreRepository.getContentRatingWithId( - appInstall.packageName, - appInstall.contentRating + app.packageName, + app.contentRating ) } - return appInstall.contentRating.title.isNotEmpty() && - appInstall.contentRating.id.isNotEmpty() + return app.contentRating.title.isNotEmpty() && + app.contentRating.id.isNotEmpty() } } -- GitLab