diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 5feaed5a4475c315da2ba0873df24d9ffa344bae..3c941c851105b6a522a6f12748266eb03c1da7ea 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/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt
index 704d9f030542f2ff6c2307a5fada9551ae70dc4c..61c632596c3e36ff50420a95978d98aedf96a00a 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()
}
@@ -194,7 +195,11 @@ class MainActivity : AppCompatActivity() {
}
launch {
- observerErrorEvent()
+ observeErrorEvent()
+ }
+
+ launch {
+ observeErrorDialogEvent()
}
launch {
@@ -204,10 +209,26 @@ class MainActivity : AppCompatActivity() {
launch {
observeNoInternetEvent()
}
+
+ launch {
+ observeAgeLimitRestrictionEvent()
+ }
}
}
}
+ private suspend fun observeAgeLimitRestrictionEvent() {
+ EventBus.events.filter {
+ it is AppEvent.AgeLimitRestrictionEvent
+ }.collectLatest {
+ ApplicationDialogFragment(
+ 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)
+ }
+ }
+
private fun observePurchaseDeclined() {
viewModel.purchaseDeclined.observe(this) {
if (it.isNotEmpty()) {
@@ -355,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 {
@@ -363,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/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt
index dd32f1a5d7cebbef84d56f431118a9be4fefd2a4..98d8d56c9b2780c6718271ed10d6222b3260ec75 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
new file mode 100644
index 0000000000000000000000000000000000000000..cfb734abcef8f7e38af4c8bb26a82917e81f762a
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingGroup.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 ContentRatingGroup(
+ val id: String,
+ @SerializedName("age_group")
+ val ageGroup: String,
+ var ratings: List
+)
diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingParser.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cde87e63857918a0678281d366f0061905bc1954
--- /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
new file mode 100644
index 0000000000000000000000000000000000000000..44f17df51e5ed2035e548ab30f991f084701d00a
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright MURENA SAS 2024
+ * Apps Quickly and easily install Android apps onto your device!
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package foundation.e.apps.data.blockedApps
+
+import com.google.gson.Gson
+import com.google.gson.JsonSyntaxException
+import com.google.gson.reflect.TypeToken
+import foundation.e.apps.data.DownloadManager
+import foundation.e.apps.data.install.FileManager
+import timber.log.Timber
+import java.io.File
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Singleton
+class ContentRatingsRepository @Inject constructor(
+ private val downloadManager: DownloadManager,
+ private val contentRatingParser: ContentRatingParser
+) {
+
+ private var _contentRatingGroups = listOf()
+ val contentRatingGroups: List
+ get() = _contentRatingGroups
+
+ companion object {
+ private const val CONTENT_RATINGS_FILE_URL =
+ "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" +
+ "content_ratings.json?ref_type=heads&inline=false"
+ private const val CONTENT_RATINGS_FILE_NAME = "content_ratings.json"
+ }
+
+ fun fetchContentRatingData() {
+ downloadManager.downloadFileInCache(
+ CONTENT_RATINGS_FILE_URL,
+ fileName = CONTENT_RATINGS_FILE_NAME
+ ) { success, _ ->
+ if (success) {
+ contentRatingParser.parseContentRatingData()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6cab641e34873363211bef205395976b37c97fff
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/blockedApps/ParentalControlRepository.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright MURENA SAS 2024
+ * Apps Quickly and easily install Android apps onto your device!
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package foundation.e.apps.data.blockedApps
+
+import android.content.Context
+import android.net.Uri
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ParentalControlRepository @Inject constructor(
+ @ApplicationContext private val context: Context
+) {
+
+ companion object {
+ private const val URI_PARENTAL_CONTROL_PROVIDER =
+ "content://foundation.e.parentalcontrol.provider/age"
+ }
+
+ fun getSelectedAgeGroup(): Age {
+ val uri = Uri.parse(URI_PARENTAL_CONTROL_PROVIDER)
+ val cursor = context.contentResolver.query(uri, null, null, null, null)
+
+ cursor?.use {
+ if (it.moveToFirst()) {
+ val ageOrdinal = it.getInt(it.getColumnIndexOrThrow("age"))
+ return Age.values()[ageOrdinal]
+ }
+ }
+
+ return Age.PARENTAL_CONTROL_DISABLED
+ }
+}
+
+enum class Age {
+ THREE,
+ SIX,
+ ELEVEN,
+ FIFTEEN,
+ SEVENTEEN,
+ PARENTAL_CONTROL_DISABLED
+}
diff --git a/app/src/main/java/foundation/e/apps/data/install/models/AppInstall.kt b/app/src/main/java/foundation/e/apps/data/install/models/AppInstall.kt
index 2d654261881d1cdae4eb62f9ab16ed61acbab408..28f92b4d64e5be5481ff3b7a5a6a92cfc5442a44 100644
--- a/app/src/main/java/foundation/e/apps/data/install/models/AppInstall.kt
+++ b/app/src/main/java/foundation/e/apps/data/install/models/AppInstall.kt
@@ -3,6 +3,7 @@ package foundation.e.apps.data.install.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 AppInstall(
Status.INSTALLING
)
+ @Ignore
+ var contentRating: ContentRating = ContentRating()
+
fun isAppInstalling() = installingStatusList.contains(status)
fun isAwaiting() = status == Status.AWAITING
diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt
index 798eb4daa09362d8dfed036f1b60cab5a96ee0f2..0239ce1f3366e1819c763876af65e8ffd3269af0 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 d971814641eab2d081f901391e634fea012d5cff..da884772134504403fa3cc1bdf52ffec02d5fe0c 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/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c859fe5e1d5d40511f2d78bbae8cb9a4322f729d
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package foundation.e.apps.domain
+
+import com.aurora.gplayapi.data.models.AuthData
+import foundation.e.apps.data.ResultSupreme
+import foundation.e.apps.data.application.ApplicationRepository
+import foundation.e.apps.data.blockedApps.Age
+import foundation.e.apps.data.blockedApps.ContentRatingGroup
+import foundation.e.apps.data.blockedApps.ContentRatingsRepository
+import foundation.e.apps.data.blockedApps.ParentalControlRepository
+import foundation.e.apps.data.enums.ResultStatus
+import foundation.e.apps.data.install.models.AppInstall
+import foundation.e.apps.data.playstore.PlayStoreRepository
+import foundation.e.apps.data.preference.DataStoreManager
+import timber.log.Timber
+import javax.inject.Inject
+
+class ValidateAppAgeLimitUseCase @Inject constructor(
+ private val applicationRepository: ApplicationRepository,
+ private val dataStoreManager: DataStoreManager,
+ private val contentRatingRepository: ContentRatingsRepository,
+ private val parentalControlRepository: ParentalControlRepository,
+ private val playStoreRepository: PlayStoreRepository
+) {
+
+ suspend operator fun invoke(app: AppInstall): ResultSupreme {
+ val authData = dataStoreManager.getAuthData()
+ val ageGroup = parentalControlRepository.getSelectedAgeGroup()
+
+ return when {
+ isParentalControlDisabled(ageGroup) -> ResultSupreme.Success(data = true)
+ hasNoContentRating(app, authData) -> ResultSupreme.Error(data = false)
+ else -> validateAgeLimit(ageGroup, app)
+ }
+ }
+
+ private fun validateAgeLimit(
+ ageGroup: Age,
+ app: AppInstall
+ ): ResultSupreme.Success {
+ val allowedContentRating =
+ contentRatingRepository.contentRatingGroups.find { it.id == ageGroup.toString() }
+
+ Timber.d(
+ "Selected age group: $ageGroup \n" +
+ "Content rating: ${app.contentRating.id} \n" +
+ "Allowed content rating: $allowedContentRating"
+ )
+
+ return ResultSupreme.Success(isValidAppAgeRating(app, allowedContentRating))
+ }
+
+ private suspend fun hasNoContentRating(app: AppInstall, authData: AuthData) =
+ !verifyContentRatingExists(app, authData)
+
+ private fun isValidAppAgeRating(
+ app: AppInstall,
+ allowedContentRating: ContentRatingGroup?
+ ) = (app.contentRating.id.isNotEmpty()
+ && allowedContentRating?.ratings?.contains(app.contentRating.id) == true)
+
+ private fun isParentalControlDisabled(ageGroup: Age) = ageGroup == Age.PARENTAL_CONTROL_DISABLED
+
+ private suspend fun verifyContentRatingExists(
+ app: AppInstall,
+ authData: AuthData
+ ): Boolean {
+ if (app.contentRating.title.isEmpty()) {
+ applicationRepository
+ .getApplicationDetails(
+ app.id, app.packageName, authData, app.origin
+ ).let { (appDetails, resultStatus) ->
+ if (resultStatus == ResultStatus.OK) {
+ app.contentRating = appDetails.contentRating
+ } else {
+ return false
+ }
+ }
+ }
+
+ if (app.contentRating.id.isEmpty()) {
+ app.contentRating =
+ playStoreRepository.getContentRatingWithId(
+ app.packageName,
+ app.contentRating
+ )
+ }
+
+ return app.contentRating.title.isNotEmpty() &&
+ app.contentRating.id.isNotEmpty()
+ }
+}
diff --git a/app/src/main/java/foundation/e/apps/install/AppInstallComponents.kt b/app/src/main/java/foundation/e/apps/install/AppInstallComponents.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dfe3f93ac9ca6266b65c3ad51c82b0f2c78fe85f
--- /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 b71e6e6bc029e2371c7a465b1e0913bd67d521b8..4a269e55585730549c684f67ab5b6e732a691daa 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,11 +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.ValidateAppAgeLimitUseCase
+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
@@ -51,9 +51,9 @@ 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 validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase,
private val dataStoreManager: DataStoreManager,
private val storageNotificationManager: StorageNotificationManager,
) {
@@ -93,7 +93,9 @@ class AppInstallProcessor @Inject constructor(
application.offer_type,
application.isFree,
application.originalSize
- )
+ ).also {
+ it.contentRating = application.contentRating
+ }
if (appInstall.type == Type.PWA) {
appInstall.downloadURLList = mutableListOf(application.url)
@@ -115,6 +117,7 @@ class AppInstallProcessor @Inject constructor(
) {
try {
val authData = dataStoreManager.getAuthData()
+
if (!appInstall.isFree && authData.isAnonymous) {
EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message))
return
@@ -122,14 +125,27 @@ 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
}
+ 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(appInstall.name))
+ } else {
+ EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc))
+ }
+
+ appInstallComponents.appManagerWrapper.cancelDownload(appInstall)
+ return
+ }
+
if (!context.isNetworkAvailable()) {
- appManagerWrapper.installationIssue(appInstall)
+ appInstallComponents.appManagerWrapper.installationIssue(appInstall)
EventBus.invokeEvent(AppEvent.NoInternetEvent(false))
return
}
@@ -137,19 +153,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)
}
}
@@ -158,7 +174,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) {
@@ -212,7 +228,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 {
@@ -220,22 +236,27 @@ 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)
@@ -248,7 +269,7 @@ class AppInstallProcessor @Inject constructor(
e
)
appInstall?.let {
- appManagerWrapper.cancelDownload(appInstall)
+ appInstallComponents.appManagerWrapper.cancelDownload(appInstall)
}
}
@@ -266,7 +287,7 @@ class AppInstallProcessor @Inject constructor(
}
private fun areFilesDownloadedButNotInstalled(appInstall: AppInstall) =
- appInstall.areFilesDownloaded() && (!appManagerWrapper.isFusedDownloadInstalled(
+ appInstall.areFilesDownloaded() && (!appInstallComponents.appManagerWrapper.isFusedDownloadInstalled(
appInstall
) || appInstall.status == Status.INSTALLING)
@@ -276,7 +297,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)
@@ -291,11 +312,12 @@ class AppInstallProcessor @Inject constructor(
}
private suspend fun isUpdateCompleted(): Boolean {
- val downloadListWithoutAnyIssue = 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()
}
@@ -317,14 +339,15 @@ 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 {
- emit(it)
- isInstallRunning(it)
- }.collect { latestFusedDownload ->
+ appInstallComponents.appInstallRepository.getDownloadFlowById(appInstall.id)
+ .transformWhile {
+ emit(it)
+ isInstallRunning(it)
+ }.collect { latestFusedDownload ->
handleFusedDownload(latestFusedDownload, appInstall)
}
}
@@ -361,7 +384,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)
}
}
@@ -372,7 +395,10 @@ 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/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt
index d7604a66af9ff571185d944a03d64443df3d1e12..d3976c568f6960c5117eeb665456e63c04e696b6 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/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt
index e2011f01b23d36412450353097e7941a4d6e53a0..bd95259361d53ad4eb9f6b0a479b7ccc0d424c91 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 50869d8357fa9a20472af5c4d9ce4d2b4b168acd..22101b2c6a10eff0c42a9576276867344a6eeb94 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)
}
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 30494abb82edf3c55f52cc2bc792313eee0ad1fd..e3db13f9fc914158d5b1cc669e95d6659440e477 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,7 +30,9 @@ 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)
+ 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 344bb732c16df61f2ca0bfea3d3c7b5e7e40dcf6..d01d12f84f06766118ec092fdefe02f87b22c4c9 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
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 6c1a4197488e7367b49a462bfc78071447797b23..480eeadda84da678d53a30e92c3f8adc0215ad4e 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,18 @@ package foundation.e.apps.installProcessor
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.aurora.gplayapi.data.models.AuthData
+import com.aurora.gplayapi.data.models.ContentRating
+import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.fdroid.FdroidRepository
import foundation.e.apps.data.application.ApplicationRepository
+import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.install.AppInstallRepository
import foundation.e.apps.data.install.AppManager
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.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 +46,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 +81,9 @@ class AppInstallProcessorTest {
private lateinit var appInstallProcessor: AppInstallProcessor
+ @Mock
+ private lateinit var validateAppAgeRatingUseCase: ValidateAppAgeLimitUseCase
+
@Mock
private lateinit var storageNotificationManager: StorageNotificationManager
@@ -85,12 +94,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 +166,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 +190,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(ResultSupreme.create(ResultStatus.OK, true))
+
+ 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(ResultSupreme.create(ResultStatus.OK, false))
+
+ 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)