diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3293d44a1e4b3c33610cf956aaf9acf8eb00f999..760a4491d7e9cfc545b2719359defa09711bc934 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -158,3 +158,15 @@ pushToPrebuilt: - git push # Sometimes a single push doesn't do all the job, so we have to push twice - git push + +publish-contracts: + stage: publish + needs: ["buildRelease"] + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: manual + - if: '$CI_COMMIT_TAG !~ "/^$/"' + when: always + script: + - ./gradlew :parental-control-data:build + - ./gradlew :parental-control-data:publish \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index c324b36834b93086c4ae9296d977f19915afdaa4..cd6bdee213a3fb3fe117feac5af11f9627e2acf6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,6 +54,11 @@ android { buildConfigField "String", "BUILD_ID", "\"${getGitHash() + "." + getDate()}\"" buildConfigField("String", "SENTRY_DSN", "\"${getSentryDsn()}\"") + def parentalControlPkgName = "foundation.e.parentalcontrol" + + manifestPlaceholders = [parentalControlPkgName: parentalControlPkgName] + buildConfigField "String", "PACKAGE_NAME_PARENTAL_CONTROL", "\"${parentalControlPkgName}\"" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -80,10 +85,15 @@ android { } sourceSets { + debug { + manifest.srcFile 'src/debug/AndroidManifest.xml' + } releaseDev { + manifest.srcFile 'src/release/AndroidManifest.xml' java.srcDirs = ['src/release/java'] } releaseStable { + manifest.srcFile 'src/release/AndroidManifest.xml' java.srcDirs = ['src/release/java'] } } @@ -149,11 +159,12 @@ allOpen { dependencies { - // TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 + implementation project(':parental-control-data') +// TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 api files('libs/splitinstall-lib.jar') implementation 'foundation.e.lib:telemetry:0.0.11-alpha' - implementation "foundation.e:gplayapi:3.2.10-3" + implementation "foundation.e:gplayapi:3.2.10-4" implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.5.6' diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..06576182c53944e646cdd220354e8323bd6ee37c --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e4492e32d18220564ee63b3bc8b087edd691be11..8042f9d891b9111886d9f9659dece38ed61623ae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,6 +49,12 @@ + + + + + + + + diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 61c632596c3e36ff50420a95978d98aedf96a00a..88496dfac5bede91c4e74d703e19753981edfcf9 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -18,6 +18,7 @@ package foundation.e.apps +import android.content.Intent import android.os.Build.VERSION import android.os.Build.VERSION_CODES import android.os.Bundle @@ -40,6 +41,9 @@ import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import foundation.e.apps.contract.ParentalControlContract.COLUMN_LOGIN_TYPE +import foundation.e.apps.data.Constants +import foundation.e.apps.data.enums.User import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.LoginViewModel @@ -123,6 +127,24 @@ class MainActivity : AppCompatActivity() { viewModel.updateContentRatings() observeEvents() + + checkGPlayLoginRequest(intent) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + checkGPlayLoginRequest(intent) + } + + private fun checkGPlayLoginRequest(intent: Intent?) { + viewModel.gPlayLoginRequested = + intent?.getBooleanExtra(Constants.REQUEST_GPLAY_LOGIN, false) ?: false + + if (!viewModel.gPlayLoginRequested) return + if (!viewModel.getTocStatus()) return + if (viewModel.getUser() !in listOf(User.GOOGLE, User.ANONYMOUS)) { + loginViewModel.logout() + } } private fun refreshSession() { @@ -315,12 +337,16 @@ class MainActivity : AppCompatActivity() { // Pop back stack to prevent showing TOSFragment on pressing back button. navController.popBackStack() navController.navigate(R.id.signInFragment) + if (viewModel.gPlayLoginRequested) viewModel.closeAfterLogin = true + return@observe } else -> {} } - it.find { it is AuthObject.GPlayAuth }?.result?.run { + val gPlayAuthObject = it.find { it is AuthObject.GPlayAuth } + + gPlayAuthObject?.result?.run { if (isSuccess()) { viewModel.gPlayAuthData = data as AuthData } else if (exception is GPlayValidationException) { @@ -333,7 +359,24 @@ class MainActivity : AppCompatActivity() { Timber.e(exception, "Login failed! message: ${exception?.localizedMessage}") } } + + // Broadcast if not gplay type login or successful gplay login + if (gPlayAuthObject == null || gPlayAuthObject.result.isSuccess()) { + broadcastGPlayLogin() + } + + if (viewModel.closeAfterLogin && it.isNotEmpty() && it.all { it.result.isSuccess() }) { + finishAndRemoveTask() + } + } + } + + private fun broadcastGPlayLogin() { + val intent = Intent(Constants.ACTION_PARENTAL_CONTROL_APP_LOUNGE_LOGIN).apply { + setPackage(BuildConfig.PACKAGE_NAME_PARENTAL_CONTROL) + putExtra(COLUMN_LOGIN_TYPE, viewModel.getUser().name) } + sendBroadcast(intent) } private fun setupViewModels() { diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index 09b79b97272bf97efa745c8aa25876d0097667f4..63fb6553e023004016c9d8689ca1e818c6e2eacc 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -18,6 +18,8 @@ package foundation.e.apps.data +import foundation.e.apps.BuildConfig + object Constants { const val timeoutDurationInMillis: Long = 10000 @@ -30,4 +32,9 @@ object Constants { const val ACTION_DUMP_APP_INSTALL_STATE = "foundation.e.apps.action.APP_INSTALL_STATE" const val TAG_APP_INSTALL_STATE = "APP_INSTALL_STATE" + + const val ACTION_PARENTAL_CONTROL_APP_LOUNGE_LOGIN = + "${BuildConfig.PACKAGE_NAME_PARENTAL_CONTROL}.action.APP_LOUNGE_LOGIN" + + const val REQUEST_GPLAY_LOGIN = "request_gplay_login" } diff --git a/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt b/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..9a3d04694d775fa277161fb51443ca9f1f70c927 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/ageRating/AgeGroupApi.kt @@ -0,0 +1,35 @@ +/* + * 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.ageRating + +import foundation.e.apps.data.blockedApps.ContentRatingGroup +import retrofit2.Response +import retrofit2.http.GET + +interface AgeGroupApi { + + companion object { + const val BASE_URL = "https://gitlab.e.foundation/e/os/app-lounge-content-ratings/-/raw/main/" + } + + @GET("content_ratings.json?ref_type=heads") + suspend fun getDefinedAgeGroups(): Response> + +} diff --git a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt index 1a5ebfa3675379bf67592d751151238581a56db5..72f5eb4d6badd69d5472a3d51f579c9f53450413 100644 --- a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt +++ b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApi.kt @@ -19,6 +19,7 @@ package foundation.e.apps.data.application.apps import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.FilterLevel import foundation.e.apps.data.enums.Origin diff --git a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt index 62876de4baf222ce94bed89ec59ab1871b6815ff..e4847beced933d98557325c576edf5aea4a854e1 100644 --- a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt @@ -21,6 +21,7 @@ package foundation.e.apps.data.application.apps import android.content.Context import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.ContentRating import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.AppSourcesContainer import foundation.e.apps.data.application.ApplicationDataManager 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 cfb734abcef8f7e38af4c8bb26a82917e81f762a..ec2be627ee096d09fc6ee33f6404be321f8f1dfe 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 @@ -19,11 +19,11 @@ package foundation.e.apps.data.blockedApps -import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json data class ContentRatingGroup( val id: String, - @SerializedName("age_group") + @Json(name = "age_group") val ageGroup: String, var ratings: List ) diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/ContentRatingsRepository.kt index 44f17df51e5ed2035e548ab30f991f084701d00a..595904bbe3a5bfaac5bf84efa45b4ecc5a934263 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 @@ -19,42 +19,37 @@ 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 com.aurora.gplayapi.data.models.ContentRating +import com.aurora.gplayapi.helpers.ContentRatingHelper +import foundation.e.apps.data.ageRating.AgeGroupApi +import foundation.e.apps.data.handleNetworkResult +import foundation.e.apps.data.login.AuthenticatorRepository 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 val ageGroupApi: AgeGroupApi, + private val authenticatorRepository: AuthenticatorRepository, ) { 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" + suspend fun fetchContentRatingData() { + val response = ageGroupApi.getDefinedAgeGroups() + if (response.isSuccessful) { + _contentRatingGroups = response.body() ?: emptyList() + } } - fun fetchContentRatingData() { - downloadManager.downloadFileInCache( - CONTENT_RATINGS_FILE_URL, - fileName = CONTENT_RATINGS_FILE_NAME - ) { success, _ -> - if (success) { - contentRatingParser.parseContentRatingData() - } - } + suspend fun getEnglishContentRating(packageName: String): ContentRating? { + val authData = authenticatorRepository.gplayAuth ?: return null + val contentRatingHelper = ContentRatingHelper(authData) + + return handleNetworkResult { + contentRatingHelper.getEnglishContentRating(packageName) + }.data } } diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt index 8744d01df3964066fe9d3557946072bec0a69f44..3a2d832d82af8673c0d4bf8206cfe30c1bfedb85 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt @@ -29,23 +29,18 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.ageRating.AgeGroupApi import foundation.e.apps.data.cleanapk.data.app.Application import foundation.e.apps.data.ecloud.EcloudApiInterface import foundation.e.apps.data.exodus.ExodusTrackerApi import foundation.e.apps.data.fdroid.FdroidApiInterface import okhttp3.Cache import okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient -import okhttp3.Protocol -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory -import timber.log.Timber -import java.net.ConnectException import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Named diff --git a/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt b/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt index 5d99c5f8f2af5d7b5b7925752a6abf1a9bff65ca..5b22a91a14605a4f29cbf7ae5db72f083856b3e8 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/AppLoungeDataStore.kt @@ -125,7 +125,7 @@ class AppLoungeDataStore @Inject constructor( } } - fun getUserType(): User { + fun getUserType(): User { // TODO: Rename this to getUser() return runBlocking { userType.first().run { val userStrings = User.values().map { it.name } @@ -149,7 +149,7 @@ class AppLoungeDataStore @Inject constructor( } } -fun Flow.getSync(): String { +fun Flow.getSync(): T { return runBlocking { this@getSync.first() } diff --git a/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt b/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..1cb26267812ce529f8a023ae8df15bf0b899ee64 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/di/AgeRatingModule.kt @@ -0,0 +1,47 @@ +/* + * 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 com.squareup.moshi.Moshi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.ageRating.AgeGroupApi +import javax.inject.Singleton +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + +@Module +@InstallIn(SingletonComponent::class) +object AgeRatingModule { + + @Singleton + @Provides + fun provideAgeGroupApi(okHttpClient: OkHttpClient, moshi: Moshi): AgeGroupApi { + return Retrofit.Builder() + .baseUrl(AgeGroupApi.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(AgeGroupApi::class.java) + } +} 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 c859fe5e1d5d40511f2d78bbae8cb9a4322f729d..c8797ecda0f17493c191b623311d3ea60d5a3dc2 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -18,35 +18,27 @@ 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.application.apps.AppsApi 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) + hasNoContentRating(app) -> ResultSupreme.Error(data = false) else -> validateAgeLimit(ageGroup, app) } } @@ -67,40 +59,26 @@ class ValidateAppAgeLimitUseCase @Inject constructor( return ResultSupreme.Success(isValidAppAgeRating(app, allowedContentRating)) } - private suspend fun hasNoContentRating(app: AppInstall, authData: AuthData) = - !verifyContentRatingExists(app, authData) + private suspend fun hasNoContentRating(app: AppInstall) = + !verifyContentRatingExists(app) private fun isValidAppAgeRating( app: AppInstall, allowedContentRating: ContentRatingGroup? - ) = (app.contentRating.id.isNotEmpty() - && allowedContentRating?.ratings?.contains(app.contentRating.id) == true) + ): Boolean { + val allowedAgeRatings = allowedContentRating?.ratings?.map { it.lowercase() } ?: emptyList() + return app.contentRating.id.isNotEmpty() && allowedAgeRatings.contains(app.contentRating.id) + } 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 - } - } - } + private suspend fun verifyContentRatingExists(app: AppInstall): Boolean { if (app.contentRating.id.isEmpty()) { - app.contentRating = - playStoreRepository.getContentRatingWithId( - app.packageName, - app.contentRating - ) + contentRatingRepository.getEnglishContentRating(app.packageName)?.run { + Timber.d("Updating content rating for package: ${app.packageName}") + app.contentRating = this + } } return app.contentRating.title.isNotEmpty() && diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..57164db099842b096ecee2aad1e04ff39dc75912 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -0,0 +1,214 @@ +/* + * 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.provider + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.UriMatcher +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.BuildConfig +import foundation.e.apps.contract.ParentalControlContract.COLUMN_LOGIN_TYPE +import foundation.e.apps.contract.ParentalControlContract.COLUMN_PACKAGE_NAME +import foundation.e.apps.contract.ParentalControlContract.PATH_BLOCKLIST +import foundation.e.apps.contract.ParentalControlContract.PATH_LOGIN_TYPE +import foundation.e.apps.contract.ParentalControlContract.getAppLoungeProviderAuthority +import foundation.e.apps.data.blockedApps.ContentRatingsRepository +import foundation.e.apps.data.enums.Origin +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.install.pkg.AppLoungePackageManager +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import timber.log.Timber + +class AgeRatingProvider : ContentProvider() { + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface ContentProviderEntryPoint { + fun provideAuthenticationRepository(): AuthenticatorRepository + fun providePackageManager(): AppLoungePackageManager + fun provideContentRatingsRepository(): ContentRatingsRepository + fun provideValidateAppAgeLimitUseCase(): ValidateAppAgeLimitUseCase + fun provideDataStoreManager(): DataStoreManager + } + + private lateinit var authenticatorRepository: AuthenticatorRepository + private lateinit var appLoungePackageManager: AppLoungePackageManager + private lateinit var contentRatingsRepository: ContentRatingsRepository + private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase + private lateinit var dataStoreManager: DataStoreManager + + private enum class UriCode(val code: Int) { + LoginType(1), + AgeRating(2), + ; + } + + private val authority = getAppLoungeProviderAuthority(BuildConfig.DEBUG) + + private val uriMatcher by lazy { + UriMatcher(UriMatcher.NO_MATCH).apply { + addURI(authority, PATH_LOGIN_TYPE, UriCode.LoginType.code) + addURI(authority, PATH_BLOCKLIST, UriCode.AgeRating.code) + } + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + val code = uriMatcher.match(uri) + return when (code) { + UriCode.LoginType.code -> getLoginType() + UriCode.AgeRating.code -> getAgeRatings() + else -> null + } + } + + private fun getLoginType(): Cursor { + val cursor = MatrixCursor(arrayOf(COLUMN_LOGIN_TYPE)) + cursor.addRow(arrayOf(dataStoreManager.getUserType())) + return cursor + } + + private fun getAgeRatings(): Cursor { + val cursor = MatrixCursor(arrayOf(COLUMN_PACKAGE_NAME)) + val packageNames = appLoungePackageManager.getAllUserApps().map { it.packageName } + runBlocking { + withContext(IO) { + try { + if (packageNames.isEmpty()) return@withContext cursor + + ensureAgeGroupDataExists() + if (!setupAuthDataIfExists()) return@withContext null + + compileAppBlockList(cursor, packageNames) + } catch (e: Exception) { + Timber.e("AgeRatingProvider", "Error fetching age ratings", e) + } + } + } + return cursor + } + + private suspend fun ensureAgeGroupDataExists() { + if (contentRatingsRepository.contentRatingGroups.isEmpty()) { + contentRatingsRepository.fetchContentRatingData() + } + } + + /** + * Return true if valid AuthData could be fetched from data store, false otherwise. + */ + private fun setupAuthDataIfExists(): Boolean { + val authData = dataStoreManager.getAuthData() + if (authData.email.isNotBlank() && authData.authToken.isNotBlank()) { + authenticatorRepository.gplayAuth = authData + return true + } + Timber.e("Blank AuthData, cannot fetch ratings from provider.") + return false + } + + private suspend fun getAppAgeValidity(packageName: String): Boolean { + val fakeAppInstall = AppInstall( + packageName = packageName, + origin = Origin.GPLAY + ) + val validateResult = validateAppAgeLimitUseCase(fakeAppInstall) + return validateResult.data ?: false + } + + private suspend fun compileAppBlockList( + cursor: MatrixCursor, + packageNames: List, + ) { + withContext(IO) { + val validityList = packageNames.map { packageName -> + async { + getAppAgeValidity(packageName) + } + }.awaitAll() + validityList.forEachIndexed { index: Int, isValid: Boolean? -> + if (isValid != true) { + // Collect package names for blocklist + cursor.addRow(arrayOf(packageNames[index])) + } + } + } + } + + override fun onCreate(): Boolean { + val appContext = context?.applicationContext ?: error("Null context in ${this::class.java.name}") + val hiltEntryPoint = + EntryPointAccessors.fromApplication(appContext, ContentProviderEntryPoint::class.java) + + authenticatorRepository = hiltEntryPoint.provideAuthenticationRepository() + appLoungePackageManager = hiltEntryPoint.providePackageManager() + contentRatingsRepository = hiltEntryPoint.provideContentRatingsRepository() + validateAppAgeLimitUseCase = hiltEntryPoint.provideValidateAppAgeLimitUseCase() + dataStoreManager = hiltEntryPoint.provideDataStoreManager() + + return true + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + throw UnsupportedOperationException("Update operation is not supported by AgeRatingProvider") + } + + override fun getType(uri: Uri): String { + return when (uriMatcher.match(uri)) { + UriCode.LoginType.code -> + "vnd.android.cursor.item/${authority}.${UriCode.LoginType.code}" + UriCode.AgeRating.code -> + "vnd.android.cursor.item/${authority}.${UriCode.AgeRating.code}" + else -> throw IllegalArgumentException("Unknown URI: $uri") + } + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + throw UnsupportedOperationException("Insert operation is not supported by AgeRatingProvider") + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + throw UnsupportedOperationException("Delete operation is not supported by AgeRatingProvider") + } + +} diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index d3976c568f6960c5117eeb665456e63c04e696b6..4787b8e912aa33c661e98315c2cf3e5603a648b0 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -82,10 +82,17 @@ class MainActivityViewModel @Inject constructor( private val _errorMessageStringResource = MutableLiveData() val errorMessageStringResource: LiveData = _errorMessageStringResource + var gPlayLoginRequested = false + var closeAfterLogin = false + lateinit var connectivityManager: ConnectivityManager var shouldIgnoreSessionError = false + fun getTocStatus(): Boolean { + return appLoungeDataStore.tocStatus.getSync() + } + fun getUser(): User { return appLoungeDataStore.getUserType() } diff --git a/app/src/release/AndroidManifest.xml b/app/src/release/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b50f75715607c68a7cfaf51ccfae5b10a7f7b87 --- /dev/null +++ b/app/src/release/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4069cac69c596f773c247f04c7e72ce72c11b93b..d1c2c788a66d9506701ba423214eb52bf022190f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ plugins { id "org.jetbrains.kotlin.plugin.allopen" version "1.8.0" id 'androidx.navigation.safeargs' version '2.5.3' apply false id 'io.gitlab.arturbosch.detekt' version '1.23.1' + id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false } allprojects { diff --git a/parental-control-data/.gitignore b/parental-control-data/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /dev/null +++ b/parental-control-data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/parental-control-data/build.gradle b/parental-control-data/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..b65ce550605cd81dd41f15f60fe153f491d08b5a --- /dev/null +++ b/parental-control-data/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' + id 'maven-publish' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +publishing { + publications { + jar(MavenPublication) { + groupId = 'foundation.e.apps' + artifactId = 'ParentalControlData' + version = '1.0.0' + + artifact("$buildDir/libs/${project.name}.jar") + + pom { + name = 'ParentalControlContract' + description = 'Constants to be used in App Lounge content provider' + + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } + + repositories { + maven { + name = "GitLab" + url = uri("https://gitlab.e.foundation/api/v4/projects/355/packages/maven") + credentials(HttpHeaderCredentials) { + name = "Job-Token" + value = System.getenv("CI_JOB_TOKEN") + } + authentication { + header(HttpHeaderAuthentication) + } + } + } +} diff --git a/parental-control-data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt b/parental-control-data/src/main/java/foundation/e/apps/contract/ParentalControlContract.kt new file mode 100644 index 0000000000000000000000000000000000000000..016c92bbf71649618ecb8a108a26a09b5ba9a538 --- /dev/null +++ b/parental-control-data/src/main/java/foundation/e/apps/contract/ParentalControlContract.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.contract + +object ParentalControlContract { + const val COLUMN_PACKAGE_NAME = "package_name" + const val COLUMN_LOGIN_TYPE = "login_type" + + const val PATH_LOGIN_TYPE = "login_type" + const val PATH_BLOCKLIST = "block_list" + + fun getAppLoungeProviderAuthority(isDebug: Boolean = false) = + "foundation.e.apps${if (isDebug) ".debug" else ""}.provider" +} diff --git a/settings.gradle b/settings.gradle index 4f3c63f186ad758687c7fae4fc47ab773fbd7f17..31d7da316f152118ee57fb4f44be9ec551b02f5a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,3 +62,4 @@ dependencyResolutionManagement { } rootProject.name = "App Lounge" include ':app' +include ':parental-control-data'