diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index c8f15598e727c2dd5142c078551d4c744e35ba8c..e8a4412f9fba64d10aabbd03b4bb8ba54c35b959 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -19,6 +19,7 @@ + CyclomaticComplexMethod:AppInstallProcessor.kt$AppInstallProcessor$suspend fun enqueueFusedDownload( appInstall: AppInstall, isAnUpdate: Boolean = false, isSystemApp: Boolean = false ) EmptyCatchBlock:NativeDeviceInfoProviderModule.kt$NativeDeviceInfoProviderModule${ } EmptyFunctionBlock:CleanApkAuthenticator.kt$CleanApkAuthenticator${} InstanceOfCheckForException:GPlayHttpClient.kt$GPlayHttpClient$e is SocketTimeoutException @@ -95,7 +96,8 @@ MaxLineLength:UpdatesWorker.kt$UpdatesWorker$applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED MayBeConst:EcloudApiInterface.kt$EcloudApiInterface.Companion$val BASE_URL = "https://eu.gtoken.ecloud.global/" MemberNameEqualsClassName:Stores.kt$Stores$private val stores = defaultStores.toMutableMap() - NewLineAtEndOfFile:ContentRatingValidity.kt$foundation.e.apps.domain.model.ContentRatingValidity.kt + NestedBlockDepth:AppInstallProcessor.kt$AppInstallProcessor$suspend fun enqueueFusedDownload( appInstall: AppInstall, isAnUpdate: Boolean = false, isSystemApp: Boolean = false ) + NewLineAtEndOfFile:ContentRatingValidity.kt$foundation.e.apps.domain.model.ContentRatingValidity.kt NewLineAtEndOfFile:HomeConverter.kt$foundation.e.apps.data.cleanapk.repositories.HomeConverter.kt NewLineAtEndOfFile:Stores.kt$foundation.e.apps.data.Stores.kt NewLineAtEndOfFile:SystemAppsUpdatesRepository.kt$foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository.kt diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/ParentalControlRepository.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/ParentalControlRepository.kt index d5279252a83cb99ffe0e2f07cec19e0f2cdc793a..04ad94c5f709efbf2cb056220bf6fa4c809b0d7e 100644 --- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/ParentalControlRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/ParentalControlRepository.kt @@ -19,10 +19,10 @@ package foundation.e.apps.data.parentalcontrol import android.content.Context -import android.net.Uri import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton +import androidx.core.net.toUri @Singleton class ParentalControlRepository @Inject constructor( @@ -32,21 +32,38 @@ class ParentalControlRepository @Inject constructor( companion object { private const val URI_PARENTAL_CONTROL_PROVIDER = "content://foundation.e.parentalcontrol.provider/age" + private const val URI_PARENTAL_CONTROL_INSTALL_TYPE_APP_MANAGEMENT = + "content://foundation.e.parentalcontrol.provider/typeappmanagement" + const val KEY_PARENTAL_GUIDANCE = "parental guidance" } fun getSelectedAgeGroup(): Age { - val uri = Uri.parse(URI_PARENTAL_CONTROL_PROVIDER) + val uri = URI_PARENTAL_CONTROL_PROVIDER.toUri() 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.entries[ageOrdinal] } } return Age.PARENTAL_CONTROL_DISABLED } + + fun getSelectedTypeAppManagement(): ProtectionMode { + val uri = URI_PARENTAL_CONTROL_INSTALL_TYPE_APP_MANAGEMENT.toUri() + val cursor = context.contentResolver.query(uri, null, null, null, null) + + cursor?.use { + if (it.moveToFirst()) { + val typeAppManagement = it.getInt(it.getColumnIndexOrThrow("typeappmanagement")) + return ProtectionMode.entries[typeAppManagement] + } + } + + return ProtectionMode.PARENTAL_CONTROL_DISABLED + } } enum class Age { @@ -57,3 +74,9 @@ enum class Age { SEVENTEEN, PARENTAL_CONTROL_DISABLED } + +enum class ProtectionMode { + REGULAR_MODE, + REQUEST_PIN_MODE, + 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 00b9f17765103ba7ba1e4b1317af7963b29403da..89c49302a075da2f5deb671ae0e9530b5e6e6dc3 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -28,6 +28,8 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.parentalcontrol.Age import foundation.e.apps.data.parentalcontrol.ContentRatingDao import foundation.e.apps.data.parentalcontrol.ParentalControlRepository +import foundation.e.apps.data.parentalcontrol.ParentalControlRepository.Companion.KEY_PARENTAL_GUIDANCE +import foundation.e.apps.data.parentalcontrol.ProtectionMode import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingGroup import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository @@ -51,6 +53,9 @@ class ValidateAppAgeLimitUseCase @Inject constructor( private val ageGroup: Age get() = parentalControlRepository.getSelectedAgeGroup() + private val protectionMode: ProtectionMode + get() = parentalControlRepository.getSelectedTypeAppManagement() + suspend operator fun invoke(app: AppInstall): ResultSupreme { return when { @@ -75,6 +80,11 @@ class ValidateAppAgeLimitUseCase @Inject constructor( ) hasNoContentRatingOnGPlay(app) -> ResultSupreme.Error() + + isParentalGuidance(app) -> ResultSupreme.Success( + data = ContentRatingValidity(false, requestPin = true) + ) + else -> validateAgeLimit(ageGroup, app) } } @@ -113,22 +123,21 @@ class ValidateAppAgeLimitUseCase @Inject constructor( ageGroup: Age, app: AppInstall ): ResultSupreme { - val allowedContentRating = - gPlayContentRatingRepository.contentRatingGroups.find { it.id == ageGroup.toString() } + if (protectionMode == ProtectionMode.REGULAR_MODE) { + val allowedContentRating = + gPlayContentRatingRepository.contentRatingGroups.find { it.id == ageGroup.toString() } - Timber.d( - "${app.packageName} - Content rating: ${app.contentRating.id} \n" + - "Selected age group: $ageGroup \n" + - "Allowed content rating: $allowedContentRating" - ) + Timber.d( + "${app.packageName} - Content rating: ${app.contentRating.id} \n" + + "Selected age group: $ageGroup \nAllowed content rating: $allowedContentRating") - return ResultSupreme.Success( - ContentRatingValidity( - isValidAppAgeRating( - app, - allowedContentRating - ), app.contentRating + val isValid = isValidAppAgeRating(app, allowedContentRating) + return ResultSupreme.Success( + ContentRatingValidity(isValid, app.contentRating, requestPin = false) ) + } + return ResultSupreme.Success( + data = ContentRatingValidity(false, requestPin = true) ) } @@ -170,4 +179,13 @@ class ValidateAppAgeLimitUseCase @Inject constructor( return app.contentRating.title.isNotEmpty() && app.contentRating.id.isNotEmpty() } + + private suspend fun isParentalGuidance(app: AppInstall): Boolean { + if (protectionMode == ProtectionMode.REGULAR_MODE) { + val fetchedContentRating = + gPlayContentRatingRepository.getEnglishContentRating(app.packageName) + return fetchedContentRating?.id == KEY_PARENTAL_GUIDANCE + } + return false + } } diff --git a/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt b/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt index 57ff5b5c5879e521f9e3407408dc7a5f77beb6bd..46b3d53b1ea1ef7a9799154f73b798147e8f90cd 100644 --- a/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt +++ b/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt @@ -21,4 +21,5 @@ package foundation.e.apps.domain.model import com.aurora.gplayapi.data.models.ContentRating -data class ContentRatingValidity(val isValid: Boolean, val contentRating: ContentRating? = null) \ No newline at end of file +data class ContentRatingValidity(val isValid: Boolean, val contentRating: ContentRating? = null, + val requestPin: Boolean = false) \ 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 58cc4ca8196a592b58b406374a3504d1ec6e89fb..120a3a0a6edb5011377c45d07bffb341a1747239 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 @@ -36,15 +36,18 @@ import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.domain.model.ContentRatingValidity 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 +import foundation.e.apps.utils.ParentalControlAuthenticator import foundation.e.apps.utils.StorageComputer import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.getFormattedString import foundation.e.apps.utils.isNetworkAvailable +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.transformWhile import timber.log.Timber @@ -139,14 +142,21 @@ class AppInstallProcessor @Inject constructor( val ageLimitValidationResult = validateAppAgeLimitUseCase.invoke(appInstall) if (ageLimitValidationResult.data?.isValid != true) { if (ageLimitValidationResult.isSuccess()) { - Timber.i("Content rating is not allowed for: ${appInstall.name}") - EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(appInstall.name)) + awaitInvokeAgeLimitEvent(appInstall.name) + if (ageLimitValidationResult.data?.requestPin == true){ + val isAuthenticated = ParentalControlAuthenticator.awaitAuthentication() + if (isAuthenticated) { + ageLimitValidationResult.setData(ContentRatingValidity(true)) + } + } } else { EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) } - appInstallComponents.appManagerWrapper.cancelDownload(appInstall) - return + if (ageLimitValidationResult.data?.isValid != true) { + appInstallComponents.appManagerWrapper.cancelDownload(appInstall) + return + } } if (!context.isNetworkAvailable()) { @@ -167,13 +177,19 @@ class AppInstallProcessor @Inject constructor( InstallWorkManager.enqueueWork(appInstall, isAnUpdate) } catch (e: Exception) { Timber.e( - "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}", - e + e, + "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}" ) appInstallComponents.appManagerWrapper.installationIssue(appInstall) } } + suspend fun awaitInvokeAgeLimitEvent(type: String) { + val deferred = CompletableDeferred() + EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(type, deferred)) + deferred.await() // await closing dialog box + } + // returns TRUE if updating urls is successful, otherwise false. private suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean { try { diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt index 8d914dd59487d691f9da4f4514eed18f4dbe234a..6e1e3596429c8f89c974e34e68d6232dff2def25 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -26,6 +26,7 @@ import android.view.View import android.widget.Toast import android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider @@ -58,6 +59,7 @@ import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections import foundation.e.apps.ui.settings.SettingsFragment import foundation.e.apps.ui.setup.signin.SignInViewModel +import foundation.e.apps.utils.ParentalControlAuthenticator import foundation.e.apps.utils.SystemInfoProvider import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus @@ -65,8 +67,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume @AndroidEntryPoint class MainActivity : AppCompatActivity() { @@ -80,9 +86,22 @@ class MainActivity : AppCompatActivity() { private const val SESSION_DIALOG_TAG = "session_dialog" } + private val parentalControlAuthenticatorLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val authenticationResult = result.data?.getBooleanExtra( + ParentalControlAuthenticator.KEY_PACO_RESULT, false) == true + ParentalControlAuthenticator.setResult(authenticationResult) + } else { + ParentalControlAuthenticator.setResult(false) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + ParentalControlAuthenticator.initLauncher(parentalControlAuthenticatorLauncher) + // Add an OnBackPressedCallback to handle the back press onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { @@ -244,15 +263,28 @@ class MainActivity : AppCompatActivity() { } 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) - } + EventBus.events.filterIsInstance() + .collectLatest { event -> + suspendCancellableCoroutine { continuation -> + val dialog = ApplicationDialogFragment( + getString(R.string.restricted_app, event.data as String), + getString(R.string.age_rate_limit_message, event.data), + positiveButtonText = getString(R.string.ok), + ) + + dialog.setOnDialogClosedListener { + event.onClose?.complete(Unit) + continuation.resume(Unit) + } + + dialog.show(supportFragmentManager, TAG) + + continuation.invokeOnCancellation { + dialog.dismissAllowingStateLoss() + event.onClose?.completeExceptionally(CancellationException("Popup dismissed")) + } + } + } } private fun observePurchaseDeclined() { diff --git a/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt index 597c437e5394e9234139a9fd73f6bfbe77760610..92b0b4a382eab86082504684771a326efafd7548 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt @@ -73,6 +73,10 @@ class ApplicationDialogFragment() : DialogFragment() { isCancelable = cancelable } + fun setOnDialogClosedListener(listener: () -> Unit) { + onDismissListener = listener + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val positiveButtonText = positiveButtonText?.ifEmpty { getString(R.string.ok) } diff --git a/app/src/main/java/foundation/e/apps/utils/ParentalControlAuthenticator.kt b/app/src/main/java/foundation/e/apps/utils/ParentalControlAuthenticator.kt new file mode 100644 index 0000000000000000000000000000000000000000..cac3b1037f829b2cbc77abf522c6a2cabc72879c --- /dev/null +++ b/app/src/main/java/foundation/e/apps/utils/ParentalControlAuthenticator.kt @@ -0,0 +1,41 @@ +package foundation.e.apps.utils + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import kotlinx.coroutines.CompletableDeferred +import timber.log.Timber + +/* +This class is used to to deal with PaCO, and get accurate password result +This class is a singleton + */ +object ParentalControlAuthenticator { + + private const val KEY_PACO_AUTHENTICATION = "foundation.e.parentalcontrol.START_AUTHENTICATE" //PaCo intent + const val KEY_PACO_RESULT = "authentication_result" //PaCo Key + + private var launcher: ActivityResultLauncher? = null + private var authResultDeferred: CompletableDeferred? = null + + fun initLauncher(launcher: ActivityResultLauncher) { + this.launcher = launcher + } + + suspend fun awaitAuthentication(): Boolean { + authResultDeferred = CompletableDeferred() + launcher?.let { + val intent = Intent(KEY_PACO_AUTHENTICATION) + it.launch(intent) + } ?: run { + Timber.e("Err: the launcher is NOT started") + authResultDeferred?.complete(false) + } + + // Await the Authenticate from PaCo + return authResultDeferred?.await() == true + } + + fun setResult(result: Boolean) { + authResultDeferred?.complete(result) + } +} 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 9c3cd7e5f8c823a2e44ebbf26cd762afd58a039a..701312475ef8e974fd7d1c1398d1fc7ec9bcf92d 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 @@ -24,6 +24,7 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.install.models.AppInstall +import kotlinx.coroutines.CompletableDeferred sealed class AppEvent(val data: Any) { class SignatureMissMatchError(packageName: String) : AppEvent(packageName) @@ -35,6 +36,9 @@ sealed class AppEvent(val data: Any) { class AppPurchaseEvent(appInstall: AppInstall) : AppEvent(appInstall) class NoInternetEvent(isInternetAvailable: Boolean) : AppEvent(isInternetAvailable) class TooManyRequests : AppEvent(Unit) - class AgeLimitRestrictionEvent(type: String) : AppEvent(type) + class AgeLimitRestrictionEvent( + type: String, + val onClose: CompletableDeferred? = null + ) : AppEvent(type) class SuccessfulLogin(user: User): AppEvent(user) }