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'