From dd3c463287bfd7309778f2361f16c5dfb8e62177 Mon Sep 17 00:00:00 2001 From: jacquarg Date: Thu, 4 Dec 2025 08:53:03 +0100 Subject: [PATCH 1/7] refac:3870: Simplify login in domain layers. --- .../data/login/AuthenticatorRepository.kt | 43 +--- .../apps/data/login/CleanApkAuthenticator.kt | 58 ----- .../apps/data/login/PlayStoreAuthenticator.kt | 53 +---- .../e/apps/data/login/StoreAuthenticator.kt | 27 --- .../data/login/api/AnonymousLoginManager.kt | 34 +-- .../data/playstore/PlayStoreRepository.kt | 4 +- .../apps/domain/ValidateAppAgeLimitUseCase.kt | 2 +- .../ContentRatingValidity.kt | 2 +- .../domain/procedures/GPlayAuthentication.kt | 213 ++++++++++++++++++ .../e/apps/install/updates/UpdatesWorker.kt | 4 +- .../workmanager/AppInstallProcessor.kt | 2 +- .../e/apps/provider/AgeRatingProvider.kt | 2 +- .../foundation/e/apps/ui/LoginViewModel.kt | 14 +- .../AppInstallProcessorTest.kt | 2 +- 14 files changed, 251 insertions(+), 209 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/login/CleanApkAuthenticator.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/login/StoreAuthenticator.kt rename app/src/main/java/foundation/e/apps/domain/{model => entities}/ContentRatingValidity.kt (95%) create mode 100644 app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt diff --git a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt index 6c98a3801..1b1126bca 100644 --- a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt @@ -29,7 +29,7 @@ import javax.inject.Singleton @Singleton class AuthenticatorRepository @Inject constructor( private val loginCommon: LoginCommon, - private val authenticators: List, + private val authenticators: List, private val appLoungeDataStore: AppLoungeDataStore ) { @@ -45,51 +45,10 @@ class AuthenticatorRepository @Inject constructor( appLoungeDataStore.saveAuthData(auth) } - suspend fun fetchAuthObjects(authTypes: List = listOf()): List { - - val authObjectsLocal = ArrayList() - - for (authenticator in authenticators) { - if (!authenticator.isStoreActive()) continue - if (authenticator::class.java.simpleName in authTypes) { - authenticator.logout() - } - - val authObject = authenticator.login() - authObjectsLocal.add(authObject) - - if (authObject is AuthObject.GPlayAuth) { - appLoungeDataStore.saveAuthData(authObject.result.data) - } - } - - return authObjectsLocal - } - - suspend fun saveUserType(user: User) { - loginCommon.saveUserType(user) - } - - suspend fun saveGoogleLogin(email: String, oauth: String) { - loginCommon.saveGoogleLogin(email, oauth) - } - - suspend fun setNoGoogleMode() { - loginCommon.setNoGoogleMode() - } - - suspend fun logout() { - loginCommon.logout() - } - suspend fun getValidatedAuthData(): ResultSupreme { val authDataValidator = (authenticators.find { it is AuthDataValidator } as AuthDataValidator) val validateAuthData = authDataValidator.validateAuthData() appLoungeDataStore.saveAuthData(validateAuthData.data) return validateAuthData } - - private fun getUserType(): User { - return loginCommon.getUserType() - } } diff --git a/app/src/main/java/foundation/e/apps/data/login/CleanApkAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/CleanApkAuthenticator.kt deleted file mode 100644 index 8dac6ac1a..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/CleanApkAuthenticator.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * 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.login - -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.enums.User -import foundation.e.apps.data.preference.AppLoungeDataStore -import foundation.e.apps.data.preference.AppLoungePreference -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Just a dummy class for CleanApk, as it requires no authentication. - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -@Singleton -class CleanApkAuthenticator @Inject constructor( - private val appLoungeDataStore: AppLoungeDataStore, - private val appLoungePreference: AppLoungePreference, -) : StoreAuthenticator { - - private val user: User - get() = appLoungeDataStore.getUser() - - override fun isStoreActive(): Boolean { - if (user == User.UNAVAILABLE) { - /* - * UNAVAILABLE user means first login is not completed. - */ - return false - } - return appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected() - } - - override suspend fun login(): AuthObject.CleanApk { - return AuthObject.CleanApk( - ResultSupreme.Success(Unit), - user, - ) - } - - override suspend fun logout() {} -} diff --git a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt index 8de91e299..acc267e21 100644 --- a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt +++ b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt @@ -49,8 +49,7 @@ class PlayStoreAuthenticator @Inject constructor( @ApplicationContext private val context: Context, private val json: Json, private val appLoungeDataStore: AppLoungeDataStore, - private val appLoungePreference: AppLoungePreference, -) : StoreAuthenticator, AuthDataValidator { +): AuthDataValidator { @Inject lateinit var loginManagerFactory: PlayStoreLoginManagerFactory @@ -67,52 +66,6 @@ class PlayStoreAuthenticator @Inject constructor( private val locale: Locale get() = context.resources.configuration.locales[0] - override fun isStoreActive(): Boolean { - if (user == User.UNAVAILABLE) { - /* - * UNAVAILABLE user means first login is not completed. - */ - return false - } - return appLoungePreference.isPlayStoreSelected() - } - - /** - * Main entry point to get GPlay auth data. - */ - override suspend fun login(): AuthObject.GPlayAuth { - val savedAuth = getSavedAuthData() - - val authData = savedAuth ?: run { - // if no saved data, then generate new auth data. - val result = retryWithBackoff { - generateAuthData() - } - - result?.let { - if (it.isSuccess()) it.data!! - else return AuthObject.GPlayAuth(it, user) - } - } - - val formattedAuthData = authData?.let { formatAuthData(it) } - formattedAuthData?.locale = locale - val result: ResultSupreme = ResultSupreme.create( - status = ResultStatus.OK, - data = formattedAuthData - ) - result.otherPayload = formattedAuthData?.email - - if (savedAuth == null && formattedAuthData != null) { - saveAuthData(formattedAuthData) - } - - return AuthObject.GPlayAuth(result, user) - } - - override suspend fun logout() { - appLoungeDataStore.saveAuthData(null) - } /** * Get authData stored as JSON and convert to AuthData class. @@ -153,14 +106,14 @@ class PlayStoreAuthenticator @Inject constructor( return json.decodeFromString(localAuthDataJson) } - private suspend fun getAuthDataAnonymously(): ResultSupreme { + suspend fun getAuthDataAnonymously(): ResultSupreme { return loginWrapper.login(locale).run { if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) else this } } - private suspend fun getAuthDataWithGoogleAccount(): ResultSupreme { + suspend fun getAuthDataWithGoogleAccount(): ResultSupreme { val email = appLoungeDataStore.emailData.getSync() val oauthToken = appLoungeDataStore.oauthToken.getSync() diff --git a/app/src/main/java/foundation/e/apps/data/login/StoreAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/StoreAuthenticator.kt deleted file mode 100644 index bc0d18106..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/StoreAuthenticator.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * 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.login - -/** - * Store (Google Play Store, Clean Apk) authenticator. - */ -interface StoreAuthenticator { - suspend fun login(): AuthObject - suspend fun logout() - fun isStoreActive(): Boolean -} diff --git a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt index 08f81649e..922f86777 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt @@ -74,21 +74,21 @@ class AnonymousLoginManager( return authData } - /** - * Check if an AuthData is valid. Returns a [PlayResponse]. - * Check [PlayResponse.isSuccessful] to see if the validation was successful. - */ - override suspend fun validate(authData: AuthData): PlayResponse { - var result = PlayResponse() - withContext(Dispatchers.IO) { - try { - val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) - result = authValidator.getValidityResponse() - } catch (e: Exception) { - e.printStackTrace() - throw e - } - } - return result - } +// /** +// * Check if an AuthData is valid. Returns a [PlayResponse]. +// * Check [PlayResponse.isSuccessful] to see if the validation was successful. +// */ +// override suspend fun validate(authData: AuthData): PlayResponse { +// var result = PlayResponse() +// withContext(Dispatchers.IO) { +// try { +// val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) +// result = authValidator.getValidityResponse() +// } catch (e: Exception) { +// e.printStackTrace() +// throw e +// } +// } +// return result +// } } 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 2775e3291..47765e0d7 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 @@ -47,6 +47,7 @@ import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.login.PlayStoreAuthenticator import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.data.playstore.utils.GplayHttpRequestException +import foundation.e.apps.domain.procedures.GPlayAuthentication import foundation.e.apps.utils.SystemInfoProvider import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -60,6 +61,7 @@ class PlayStoreRepository @Inject constructor( @ApplicationContext private val context: Context, private val gPlayHttpClient: GPlayHttpClient, private val authenticatorRepository: AuthenticatorRepository, + private val gPlayAuthentication: GPlayAuthentication, private val applicationDataManager: ApplicationDataManager, private val playStoreSearchHelper: PlayStoreSearchHelper ) : StoreRepository { @@ -208,7 +210,7 @@ class PlayStoreRepository @Inject constructor( private suspend fun refreshPlayStoreAuthentication() { Timber.i("Refreshing authentication.") - authenticatorRepository.fetchAuthObjects(listOf(PlayStoreAuthenticator::class.java.simpleName)) + gPlayAuthentication.refresh() } suspend fun getAppDetailsWeb(packageName: String): Application? { 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 ab94f3cd2..3b7e1dd11 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -32,7 +32,7 @@ import foundation.e.apps.data.parentalcontrol.ParentalControlRepository.Companio import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingGroup import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository -import foundation.e.apps.domain.model.ContentRatingValidity +import foundation.e.apps.domain.entities.ContentRatingValidity import org.e.parentalcontrol.data.model.TypeAppManagement import timber.log.Timber import javax.inject.Inject diff --git a/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt b/app/src/main/java/foundation/e/apps/domain/entities/ContentRatingValidity.kt similarity index 95% rename from app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt rename to app/src/main/java/foundation/e/apps/domain/entities/ContentRatingValidity.kt index 25f6ee484..a7b660266 100644 --- a/app/src/main/java/foundation/e/apps/domain/model/ContentRatingValidity.kt +++ b/app/src/main/java/foundation/e/apps/domain/entities/ContentRatingValidity.kt @@ -17,7 +17,7 @@ * */ -package foundation.e.apps.domain.model +package foundation.e.apps.domain.entities import com.aurora.gplayapi.data.models.ContentRating diff --git a/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt b/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt new file mode 100644 index 000000000..b866af9f2 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt @@ -0,0 +1,213 @@ +package foundation.e.apps.domain.procedures + +import android.content.Context +import coil.request.NullRequestDataException +import com.aurora.gplayapi.data.models.AuthData +import dagger.hilt.android.qualifiers.ApplicationContext +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.login.AuthObject +import foundation.e.apps.data.login.PlayStoreAuthenticator +import foundation.e.apps.data.preference.AppLoungeDataStore +import foundation.e.apps.data.preference.AppLoungePreference +import foundation.e.apps.data.preference.getSync +import foundation.e.apps.di.qualifiers.IoCoroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GPlayAuthentication @Inject constructor( + private val appLoungeDataStore: AppLoungeDataStore, + private val appLoungePreference: AppLoungePreference, + private val playStoreAuthenticator: PlayStoreAuthenticator, + private val json: Json, + @ApplicationContext private val context: Context, + @IoCoroutineScope private val ioCoroutineScope: CoroutineScope, + ) { + + private val locale: Locale + get() = context.resources.configuration.locales[0] + + + // Move to LoginUseCase ? or AuthenticationUseCase ? + + suspend fun setGoogleUser(email: String, oauth: String): List = withContext(ioCoroutineScope.coroutineContext) { + appLoungeDataStore.saveGoogleLogin(email, oauth) + appLoungeDataStore.saveUserType(User.GOOGLE) + + + val result = buildFreshGoogleLoginToken() + if (result.isSuccess) { + appLoungeDataStore.saveAuthData(result.getOrNull()) + + } + + buildAuthObjectList(result, User.GOOGLE) + } + + suspend fun setAnonymousUser(): List = withContext(ioCoroutineScope.coroutineContext) { + appLoungeDataStore.saveUserType(User.GOOGLE) + val result = buildFreshAnonymousLoginToken() + if (result.isSuccess) { + appLoungeDataStore.saveAuthData(result.getOrNull()) + } + + buildAuthObjectList(result, User.GOOGLE) + } + + + suspend fun setNoGoogleMode() = withContext(ioCoroutineScope.coroutineContext) { + appLoungePreference.run { + disablePlayStore() + enableOpenSource() + enablePwa() + } + appLoungeDataStore.saveUserType(User.NO_GOOGLE) + } + + suspend fun fetchAuthObjects(authTypes: List = listOf()): List { + val userType = appLoungeDataStore.getUserType() + + if (authTypes.isEmpty()) { + // use saved state. + val savedAuth = getSavedAuthData() + if (savedAuth != null) { + return buildAuthObjectList(Result.success(savedAuth), userType = userType) + } + } + + val result = updateToken() + + return if (result?.isSuccess == true) { + buildAuthObjectList(result, User.GOOGLE) + } else if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { + listOf(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) + } else { + emptyList() + } + } + + + private fun getSavedAuthData(): AuthData? { + val authJson = appLoungeDataStore.authData.getSync() + return if (authJson.isBlank()) null + else try { + json.decodeFromString(authJson) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + private fun buildAuthObjectList(gPlayloginResult: Result, userType: User): List { + val authObjectsLocal = ArrayList() + + // Google Login case: + val result: ResultSupreme + if (gPlayloginResult.isSuccess) { + result = ResultSupreme.create( + status = ResultStatus.OK, + data = gPlayloginResult.getOrNull() + ) + result.otherPayload = gPlayloginResult.getOrNull()?.email + } else { + result = ResultSupreme.create(status = ResultStatus.UNKNOWN, exception = gPlayloginResult.exceptionOrNull() as? Exception) + } + + authObjectsLocal.add(AuthObject.GPlayAuth(result, userType)) + + + // Clean apk + if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { + authObjectsLocal.add(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) + } + + return authObjectsLocal + } + + suspend fun logout() { + appLoungeDataStore.destroyCredentials() + appLoungeDataStore.saveUserType(null) + // reset app source preferences on logout. + appLoungePreference.run { + enableOpenSource() + enablePwa() + enablePlayStore() + } + } + + + /////////// END. + + suspend fun refresh() { + updateToken() + } + + private suspend fun updateToken(): Result? { + val userType = appLoungeDataStore.getUserType() + + val result = when (userType) { + User.GOOGLE -> buildFreshGoogleLoginToken() + User.ANONYMOUS -> buildFreshAnonymousLoginToken() + else -> null + } + + if (result?.isSuccess == true) { + appLoungeDataStore.saveAuthData(result.getOrNull()) + } + return result + } + + + // hould be a repository method. + private suspend fun buildFreshGoogleLoginToken(): Result { + +// val result = retryWithBackoff { + val result = playStoreAuthenticator.getAuthDataWithGoogleAccount() + +// if (result != null && !result.isSuccess()) { +// return AuthObject.GPlayAuth(result, user) +// } + val rawData = result.data + if (!result.isSuccess()) { + return Result.failure(result.exception ?: Exception("Result without exception")) + } else if (result.isSuccess() && rawData == null) { + return Result.failure(NullRequestDataException()) + } + + + /* + * Aurora OSS GPlay API complains of missing headers sometimes. + * Converting [authData] to Json and back to [AuthData] fixed it. + */ + val authData = formatAuthData(result.data!!) + + // Force locale to be the one configured on the device. But didn't we expect to have the one of the Google Account here ? + authData.locale = locale + return Result.success(authData) + + } + + private suspend fun buildFreshAnonymousLoginToken(): Result { + val result = playStoreAuthenticator.getAuthDataAnonymously() + return if (!result.isSuccess()) { + Result.failure(result.exception ?: Exception("Result without exception")) + } else { + Result.success(result.data!!) + } + } + + /** + * Aurora OSS GPlay API complains of missing headers sometimes. + * Converting [authData] to Json and back to [AuthData] fixed it. + */ + private fun formatAuthData(authData: AuthData): AuthData { + val localAuthDataJson = json.encodeToString(authData) + return json.decodeFromString(localAuthDataJson) + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt index f2907200f..4d9a7c1a3 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt @@ -38,7 +38,6 @@ class UpdatesWorker @AssistedInject constructor( @Assisted private val params: WorkerParameters, private val updatesManagerRepository: UpdatesManagerRepository, private val appLoungeDataStore: AppLoungeDataStore, - private val authenticatorRepository: AuthenticatorRepository, private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, @@ -109,7 +108,7 @@ class UpdatesWorker @AssistedInject constructor( val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val appsNeededToUpdate = mutableListOf() val user = getUser() - val authData = authenticatorRepository.getValidatedAuthData().data + val authData = appLoungeDataStore.getAuthData() val resultStatus: ResultStatus if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { @@ -141,6 +140,7 @@ class UpdatesWorker @AssistedInject constructor( } if (resultStatus != ResultStatus.OK) { + // TODO 20251204 : detect GPlay token error, and call GPlayAuthentication.refresh() in this case. manageRetry(resultStatus.toString()) } else { /* 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 2831499ed..07e66676f 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,7 +36,7 @@ import foundation.e.apps.data.install.models.AppInstall 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.domain.entities.ContentRatingValidity import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.download.DownloadManagerUtils import foundation.e.apps.install.notification.StorageNotificationManager diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index e6785d4a2..b6fabf4c9 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -54,7 +54,7 @@ import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.domain.ValidateAppAgeLimitUseCase -import foundation.e.apps.domain.model.ContentRatingValidity +import foundation.e.apps.domain.entities.ContentRatingValidity import foundation.e.apps.install.pkg.AppLoungePackageManager import foundation.e.apps.utils.isNetworkAvailable import kotlinx.coroutines.Dispatchers.IO diff --git a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt index 211e45bdf..99b9883d8 100644 --- a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt @@ -26,6 +26,7 @@ import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.domain.procedures.GPlayAuthentication import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.launch import okhttp3.Cache @@ -37,7 +38,7 @@ import javax.inject.Inject */ @HiltViewModel class LoginViewModel @Inject constructor( - private val authenticatorRepository: AuthenticatorRepository, + private val gPlayAuthentication: GPlayAuthentication, private val cache: Cache, private val stores: Stores ) : ViewModel() { @@ -59,7 +60,7 @@ class LoginViewModel @Inject constructor( */ fun startLoginFlow(clearList: List = listOf()) { viewModelScope.launch { - val authObjectsLocal = authenticatorRepository.fetchAuthObjects(clearList) + val authObjectsLocal = gPlayAuthentication.fetchAuthObjects(clearList) authObjects.postValue(authObjectsLocal) } } @@ -72,7 +73,7 @@ class LoginViewModel @Inject constructor( fun initialAnonymousLogin(onUserSaved: () -> Unit) { viewModelScope.launch { stores.enableStore(Source.PLAY_STORE) - authenticatorRepository.saveUserType(User.ANONYMOUS) + gPlayAuthentication.setAnonymousUser() onUserSaved() startLoginFlow() } @@ -87,8 +88,7 @@ class LoginViewModel @Inject constructor( fun initialGoogleLogin(email: String, oauthToken: String, onUserSaved: () -> Unit) { viewModelScope.launch { stores.enableStore(Source.PLAY_STORE) - authenticatorRepository.saveGoogleLogin(email, oauthToken) - authenticatorRepository.saveUserType(User.GOOGLE) + gPlayAuthentication.setGoogleUser(email, oauthToken) onUserSaved() startLoginFlow() } @@ -104,7 +104,7 @@ class LoginViewModel @Inject constructor( fun initialNoGoogleLogin(onUserSaved: () -> Unit) { viewModelScope.launch { stores.disableStore(Source.PLAY_STORE) - authenticatorRepository.setNoGoogleMode() + gPlayAuthentication.setNoGoogleMode() onUserSaved() startLoginFlow() } @@ -139,7 +139,7 @@ class LoginViewModel @Inject constructor( fun logout() { viewModelScope.launch { cache.evictAll() - authenticatorRepository.logout() + gPlayAuthentication.logout() authObjects.postValue(listOf()) } } 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 a11efa092..919530224 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -30,7 +30,7 @@ import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.domain.ValidateAppAgeLimitUseCase -import foundation.e.apps.domain.model.ContentRatingValidity +import foundation.e.apps.domain.entities.ContentRatingValidity import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.workmanager.AppInstallProcessor -- GitLab From 3fbccc5160eb713a585fa4d963d3e60c4098da9d Mon Sep 17 00:00:00 2001 From: jacquarg Date: Thu, 4 Dec 2025 08:55:57 +0100 Subject: [PATCH 2/7] refac:3870: Remove GPlay Auth validator feature. --- .../e/apps/data/login/AuthDataValidator.kt | 26 ----------- .../data/login/AuthenticatorRepository.kt | 9 ---- .../apps/data/login/PlayStoreAuthenticator.kt | 20 +------- .../data/login/api/AnonymousLoginManager.kt | 20 -------- .../apps/data/login/api/GoogleLoginManager.kt | 18 -------- .../data/login/api/PlayStoreLoginManager.kt | 1 - .../data/login/api/PlayStoreLoginWrapper.kt | 27 ----------- .../playstore/utils/CustomAuthValidator.kt | 46 ------------------- 8 files changed, 1 insertion(+), 166 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/login/AuthDataValidator.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/playstore/utils/CustomAuthValidator.kt diff --git a/app/src/main/java/foundation/e/apps/data/login/AuthDataValidator.kt b/app/src/main/java/foundation/e/apps/data/login/AuthDataValidator.kt deleted file mode 100644 index 908fe70b5..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/AuthDataValidator.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright MURENA SAS 2023 - * 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.login - -import com.aurora.gplayapi.data.models.AuthData -import foundation.e.apps.data.ResultSupreme - -interface AuthDataValidator { - suspend fun validateAuthData(): ResultSupreme -} diff --git a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt index 1b1126bca..fcd074a3b 100644 --- a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt @@ -28,8 +28,6 @@ import javax.inject.Singleton @JvmSuppressWildcards @Singleton class AuthenticatorRepository @Inject constructor( - private val loginCommon: LoginCommon, - private val authenticators: List, private val appLoungeDataStore: AppLoungeDataStore ) { @@ -44,11 +42,4 @@ class AuthenticatorRepository @Inject constructor( suspend fun setGPlayAuth(auth: AuthData) { appLoungeDataStore.saveAuthData(auth) } - - suspend fun getValidatedAuthData(): ResultSupreme { - val authDataValidator = (authenticators.find { it is AuthDataValidator } as AuthDataValidator) - val validateAuthData = authDataValidator.validateAuthData() - appLoungeDataStore.saveAuthData(validateAuthData.data) - return validateAuthData - } } diff --git a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt index acc267e21..e2160f5fe 100644 --- a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt +++ b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt @@ -49,7 +49,7 @@ class PlayStoreAuthenticator @Inject constructor( @ApplicationContext private val context: Context, private val json: Json, private val appLoungeDataStore: AppLoungeDataStore, -): AuthDataValidator { +) { @Inject lateinit var loginManagerFactory: PlayStoreLoginManagerFactory @@ -159,22 +159,4 @@ class PlayStoreAuthenticator @Inject constructor( else this } } - - override suspend fun validateAuthData(): ResultSupreme { - val savedAuth = getSavedAuthData() - if (!isAuthDataValid(savedAuth)) { - Timber.i("Validating AuthData...") - val authData = generateAuthData() - authData.data?.let { - saveAuthData(it) - return authData - } - return ResultSupreme.create(ResultStatus.UNKNOWN) - } - - return ResultSupreme.create(ResultStatus.OK, savedAuth) - } - - private suspend fun isAuthDataValid(savedAuth: AuthData?) = - savedAuth != null && loginWrapper.validate(savedAuth).exception == null } diff --git a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt index 922f86777..f6fa89835 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt @@ -19,10 +19,8 @@ package foundation.e.apps.data.login.api import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.helpers.AuthHelper import foundation.e.apps.data.login.Auth -import foundation.e.apps.data.playstore.utils.CustomAuthValidator import foundation.e.apps.data.playstore.utils.GPlayHttpClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -73,22 +71,4 @@ class AnonymousLoginManager( } return authData } - -// /** -// * Check if an AuthData is valid. Returns a [PlayResponse]. -// * Check [PlayResponse.isSuccessful] to see if the validation was successful. -// */ -// override suspend fun validate(authData: AuthData): PlayResponse { -// var result = PlayResponse() -// withContext(Dispatchers.IO) { -// try { -// val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) -// result = authValidator.getValidityResponse() -// } catch (e: Exception) { -// e.printStackTrace() -// throw e -// } -// } -// return result -// } } diff --git a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt index 757ea13a4..8b65df30c 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt @@ -68,22 +68,4 @@ class GoogleLoginManager( } return authData } - - /** - * Check if an AuthData is valid. Returns a [PlayResponse]. - * Check [PlayResponse.isSuccessful] to see if the validation was successful. - */ - override suspend fun validate(authData: AuthData): PlayResponse { - var result = PlayResponse() - withContext(Dispatchers.IO) { - try { - val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) - result = authValidator.getValidityResponse() - } catch (e: Exception) { - e.printStackTrace() - throw e - } - } - return result - } } diff --git a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt index 12fad6b6d..29499c255 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt @@ -22,5 +22,4 @@ import com.aurora.gplayapi.data.models.PlayResponse interface PlayStoreLoginManager { suspend fun login(): AuthData? - suspend fun validate(authData: AuthData): PlayResponse } diff --git a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt index bf8820e02..ac1e8401b 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt @@ -62,33 +62,6 @@ class PlayStoreLoginWrapper constructor( } } - /** - * Get AuthData validity of in the form of PlayResponse. - * Advantage of not using a simple boolean is we get error message and - * network code of the request inside PlayResponse object. - * - * Applicable for both Google and Anonymous login. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ - suspend fun validate(authData: AuthData): ResultSupreme { - var response = PlayResponse() - val result = handleNetworkResult { - response = loginManager.validate(authData) - if (response.code != 200) { - throw Exception("Validation network code: ${response.code}") - } - response - } - return ResultSupreme.replicate(result, response).apply { - this.exception = when (result) { - is ResultSupreme.Timeout -> GPlayLoginException(true, "GPlay API timeout", user) - is ResultSupreme.Error -> GPlayLoginException(false, result.message, user) - else -> null - } - } - } - /** * Gets email and oauthToken from Google login, finds the AASToken from AC2DM response * and returns it. This token is then used to fetch AuthData from [login]. diff --git a/app/src/main/java/foundation/e/apps/data/playstore/utils/CustomAuthValidator.kt b/app/src/main/java/foundation/e/apps/data/playstore/utils/CustomAuthValidator.kt deleted file mode 100644 index e029c196e..000000000 --- a/app/src/main/java/foundation/e/apps/data/playstore/utils/CustomAuthValidator.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * 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.playstore.utils - -import com.aurora.gplayapi.GooglePlayApi -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse -import com.aurora.gplayapi.data.providers.HeaderProvider -import com.aurora.gplayapi.helpers.NativeHelper -import com.aurora.gplayapi.network.IHttpClient - -/** - * Custom implementation of [AuthValidator]. - * This returns [PlayResponse] for [getValidityResponse], - * which is a replacement of [AuthValidator.isValid] which just returned a boolean. - * A PlayResponse object allows us to look into whether the request had any network error. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 - */ -class CustomAuthValidator(authData: AuthData) : NativeHelper(authData) { - - override fun using(httpClient: IHttpClient) = apply { - this.httpClient = httpClient - } - - fun getValidityResponse(): PlayResponse { - val endpoint: String = GooglePlayApi.URL_SYNC - val headers = HeaderProvider.getDefaultHeaders(authData) - return httpClient.post(endpoint, headers, hashMapOf()) - } -} -- GitLab From 509bf2279a87451f75b4212280b47363234f1a87 Mon Sep 17 00:00:00 2001 From: jacquarg Date: Thu, 4 Dec 2025 09:13:06 +0100 Subject: [PATCH 3/7] refac:3870: Get saved token directly from appLoungeDataStore. --- .../data/login/AuthenticatorRepository.kt | 9 --- .../data/playstore/PlayStoreRepository.kt | 81 +++++++++++-------- .../e/apps/provider/AgeRatingProvider.kt | 4 +- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt index fcd074a3b..2c244668f 100644 --- a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt @@ -30,15 +30,6 @@ import javax.inject.Singleton class AuthenticatorRepository @Inject constructor( private val appLoungeDataStore: AppLoungeDataStore ) { - - fun getGPlayAuthOrThrow(): AuthData { - return kotlin.runCatching { - appLoungeDataStore.getAuthData() - }.getOrElse { - throw GPlayLoginException(false, "AuthData is not available", appLoungeDataStore.getUser()) - } - } - suspend fun setGPlayAuth(auth: AuthData) { appLoungeDataStore.saveAuthData(auth) } 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 47765e0d7..559765756 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 @@ -19,6 +19,7 @@ package foundation.e.apps.data.playstore import android.content.Context +import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.ContentRating import com.aurora.gplayapi.data.models.PlayFile @@ -47,6 +48,7 @@ import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.login.PlayStoreAuthenticator import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.data.playstore.utils.GplayHttpRequestException +import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.domain.procedures.GPlayAuthentication import foundation.e.apps.utils.SystemInfoProvider import kotlinx.coroutines.CancellationException @@ -63,7 +65,8 @@ class PlayStoreRepository @Inject constructor( private val authenticatorRepository: AuthenticatorRepository, private val gPlayAuthentication: GPlayAuthentication, private val applicationDataManager: ApplicationDataManager, - private val playStoreSearchHelper: PlayStoreSearchHelper + private val playStoreSearchHelper: PlayStoreSearchHelper, + private val appLoungeDataStore: AppLoungeDataStore ) : StoreRepository { override suspend fun getHomeScreenData(list: MutableList): List { @@ -201,11 +204,12 @@ class PlayStoreRepository @Inject constructor( appDetails.toApplication(context) } - private fun getAppDetailsHelper(): AppDetailsHelper { - val authData = authenticatorRepository.getGPlayAuthOrThrow() - val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) + private suspend fun getAppDetailsHelper(): AppDetailsHelper { + return doAuthenticatedRequest { authData -> + val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) - return appDetailsHelper + appDetailsHelper + } } private suspend fun refreshPlayStoreAuthentication() { @@ -272,15 +276,18 @@ class PlayStoreRepository @Inject constructor( throw IllegalStateException("Could not get download details for $idOrPackageName") } - // Don't store auth data in a variable. Always get GPlay auth from repository, - // because auth might get refreshed while fetching app details. - val purchaseHelper = PurchaseHelper(authenticatorRepository.getGPlayAuthOrThrow()) - .using(gPlayHttpClient) + doAuthenticatedRequest { authData -> + + // Don't store auth data in a variable. Always get GPlay auth from repository, + // because auth might get refreshed while fetching app details. + val purchaseHelper = PurchaseHelper(authData) + .using(gPlayHttpClient) - buildList { addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) } + buildList { addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) } + } } - fun isAnonymousUser() = authenticatorRepository.getGPlayAuthOrThrow().isAnonymous + fun isAnonymousUser() = appLoungeDataStore.getAuthData().isAnonymous suspend fun getOnDemandModule( packageName: String, @@ -288,39 +295,49 @@ class PlayStoreRepository @Inject constructor( versionCode: Long, offerType: Int ): List { - val downloadData = mutableListOf() - val authData = authenticatorRepository.getGPlayAuthOrThrow() - - withContext(Dispatchers.IO) { - val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) - downloadData.addAll( - purchaseHelper.purchase(packageName, versionCode, offerType, moduleName) - ) + return doAuthenticatedRequest { authData -> + val downloadData = mutableListOf() + + withContext(Dispatchers.IO) { + val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) + downloadData.addAll( + purchaseHelper.purchase(packageName, versionCode, offerType, moduleName) + ) + } + downloadData } - return downloadData } suspend fun getContentRatingWithId( appPackage: String, contentRating: ContentRating ): ContentRating { - val authData = authenticatorRepository.getGPlayAuthOrThrow() - val contentRatingHelper = ContentRatingHelper(authData) - - return withContext(Dispatchers.IO) { - contentRatingHelper.updateContentRatingWithId( - appPackage, - contentRating - ) + return doAuthenticatedRequest { authData -> + val contentRatingHelper = ContentRatingHelper(authData) + + withContext(Dispatchers.IO) { + contentRatingHelper.updateContentRatingWithId( + appPackage, + contentRating + ) + } } } suspend fun getEnglishContentRating(packageName: String): ContentRating { - val authData = authenticatorRepository.getGPlayAuthOrThrow() - val contentRatingHelper = ContentRatingHelper(authData) + return doAuthenticatedRequest { authData -> + val contentRatingHelper = ContentRatingHelper(authData) - return withContext(Dispatchers.IO) { - contentRatingHelper.getEnglishContentRating(packageName) + withContext(Dispatchers.IO) { + contentRatingHelper.getEnglishContentRating(packageName) + } } } + + //inline fun runSuspendCatching(block: () -> T): Result { + + private suspend fun doAuthenticatedRequest(request: suspend (AuthData) -> T): T { + val authData = appLoungeDataStore.getAuthData() + return request(authData) + } } diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index b6fabf4c9..518ab79a6 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -301,9 +301,9 @@ class AgeRatingProvider : ContentProvider() { private fun hasAuthData(): Boolean { return try { - authenticatorRepository.getGPlayAuthOrThrow() + appLoungeDataStore.getAuthData() true - } catch (e: GPlayLoginException) { + } catch (e: NoSuchElementException) { Timber.e("No AuthData to check content rating") false } -- GitLab From dd53844bf0d0f3b6c0ea37889f0f4672eece8c20 Mon Sep 17 00:00:00 2001 From: jacquarg Date: Thu, 4 Dec 2025 09:15:44 +0100 Subject: [PATCH 4/7] refac:3870: remove AuthenticatorRepository. --- .../data/login/AuthenticatorRepository.kt | 36 ----------------- .../apps/data/login/api/GoogleLoginManager.kt | 3 -- .../login/api/PlayStoreLoginManagerFactory.kt | 2 +- .../data/playstore/PlayStoreRepository.kt | 27 ++++++++++--- .../java/foundation/e/apps/di/LoginModule.kt | 39 ------------------- .../e/apps/install/updates/UpdatesWorker.kt | 1 - .../e/apps/provider/AgeRatingProvider.kt | 8 +--- .../foundation/e/apps/ui/LoginViewModel.kt | 2 - 8 files changed, 25 insertions(+), 93 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt delete mode 100644 app/src/main/java/foundation/e/apps/di/LoginModule.kt diff --git a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt b/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt deleted file mode 100644 index 2c244668f..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * 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.login - -import com.aurora.gplayapi.data.models.AuthData -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.enums.User -import foundation.e.apps.data.login.exceptions.GPlayLoginException -import foundation.e.apps.data.preference.AppLoungeDataStore -import javax.inject.Inject -import javax.inject.Singleton - -@JvmSuppressWildcards -@Singleton -class AuthenticatorRepository @Inject constructor( - private val appLoungeDataStore: AppLoungeDataStore -) { - suspend fun setGPlayAuth(auth: AuthData) { - appLoungeDataStore.saveAuthData(auth) - } -} diff --git a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt index 8b65df30c..9ff9215fb 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt @@ -21,8 +21,6 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.helpers.AuthHelper import foundation.e.apps.data.playstore.utils.AC2DMTask -import foundation.e.apps.data.playstore.utils.CustomAuthValidator -import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync import kotlinx.coroutines.Dispatchers @@ -30,7 +28,6 @@ import kotlinx.coroutines.withContext import java.util.Properties class GoogleLoginManager( - private val gPlayHttpClient: GPlayHttpClient, private val nativeDeviceProperty: Properties, private val aC2DMTask: AC2DMTask, private val appLoungeDataStore: AppLoungeDataStore, diff --git a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt index 121b9d626..d6e09ccab 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt @@ -37,7 +37,7 @@ class PlayStoreLoginManagerFactory @Inject constructor( fun createLoginManager(user: User): PlayStoreLoginManager { return when (user) { - User.GOOGLE -> GoogleLoginManager(gPlayHttpClient, nativeDeviceProperty, aC2DMTask, appLoungeDataStore) + User.GOOGLE -> GoogleLoginManager(nativeDeviceProperty, aC2DMTask, appLoungeDataStore) else -> AnonymousLoginManager(gPlayHttpClient, nativeDeviceProperty, json) } } 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 559765756..d6c52dfde 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 @@ -19,11 +19,14 @@ package foundation.e.apps.data.playstore import android.content.Context +import com.aurora.gplayapi.GooglePlayApi import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.ContentRating import com.aurora.gplayapi.data.models.PlayFile +import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.StreamCluster +import com.aurora.gplayapi.data.providers.HeaderProvider import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.ContentRatingHelper import com.aurora.gplayapi.helpers.PurchaseHelper @@ -35,6 +38,7 @@ import com.aurora.gplayapi.helpers.web.WebCategoryStreamHelper import com.aurora.gplayapi.helpers.web.WebTopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R +import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.StoreRepository import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.data.Application @@ -44,8 +48,7 @@ import foundation.e.apps.data.application.utils.CategoryType import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.enums.Source import foundation.e.apps.data.handleNetworkResult -import foundation.e.apps.data.login.AuthenticatorRepository -import foundation.e.apps.data.login.PlayStoreAuthenticator +import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.AppLoungeDataStore @@ -62,7 +65,6 @@ import com.aurora.gplayapi.data.models.App as GplayApp class PlayStoreRepository @Inject constructor( @ApplicationContext private val context: Context, private val gPlayHttpClient: GPlayHttpClient, - private val authenticatorRepository: AuthenticatorRepository, private val gPlayAuthentication: GPlayAuthentication, private val applicationDataManager: ApplicationDataManager, private val playStoreSearchHelper: PlayStoreSearchHelper, @@ -334,10 +336,25 @@ class PlayStoreRepository @Inject constructor( } } - //inline fun runSuspendCatching(block: () -> T): Result { - private suspend fun doAuthenticatedRequest(request: suspend (AuthData) -> T): T { val authData = appLoungeDataStore.getAuthData() return request(authData) } + + // Helper function, to detect error not specific to the request + // (authentication stale, network unreachable, ...) + // This can be used to workaround Exception swallowing done by GPlayAPI library. + private suspend fun sendCommonErrorsProbe() { + withContext(Dispatchers.IO) { + val endpoint: String = GooglePlayApi.URL_SYNC + val headers = HeaderProvider.getDefaultHeaders(appLoungeDataStore.getAuthData()) + + // No expectation for the response result, just look for the exceptions: + // will send on EventBus error for 401, 429. + // will throw GplayHttpRequestException for all response code "not 200 or 429". + // will throw SocketTimeoutException as GplayHttpRequestException 408 + // will throw all other exceptions. + gPlayHttpClient.post(endpoint, headers, hashMapOf()) + } + } } diff --git a/app/src/main/java/foundation/e/apps/di/LoginModule.kt b/app/src/main/java/foundation/e/apps/di/LoginModule.kt deleted file mode 100644 index ac964187d..000000000 --- a/app/src/main/java/foundation/e/apps/di/LoginModule.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.di - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import foundation.e.apps.data.login.CleanApkAuthenticator -import foundation.e.apps.data.login.PlayStoreAuthenticator -import foundation.e.apps.data.login.StoreAuthenticator - -@InstallIn(SingletonComponent::class) -@Module -object LoginModule { - - @Provides - fun providesAuthenticators( - gPlay: PlayStoreAuthenticator, - cleanApk: CleanApkAuthenticator, - ): List { - return listOf(gPlay, cleanApk) - } -} diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt index 4d9a7c1a3..52882a93a 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt @@ -21,7 +21,6 @@ import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository -import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.install.workmanager.AppInstallProcessor diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index 518ab79a6..de7e7aaba 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -46,8 +46,6 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.enums.Source import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.login.AuthenticatorRepository -import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.parentalcontrol.ContentRatingDao import foundation.e.apps.data.parentalcontrol.ContentRatingEntity import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository @@ -69,7 +67,6 @@ class AgeRatingProvider : ContentProvider() { @EntryPoint @InstallIn(SingletonComponent::class) interface ContentProviderEntryPoint { - fun provideAuthenticationRepository(): AuthenticatorRepository fun providePackageManager(): AppLoungePackageManager fun provideGPlayContentRatingsRepository(): GPlayContentRatingRepository fun provideFDroidAntiFeatureRepository(): FDroidAntiFeatureRepository @@ -86,7 +83,6 @@ class AgeRatingProvider : ContentProvider() { private const val AUTHORITY = "foundation.e.apps.provider" } - private lateinit var authenticatorRepository: AuthenticatorRepository private lateinit var appLoungePackageManager: AppLoungePackageManager private lateinit var gPlayContentRatingRepository: GPlayContentRatingRepository private lateinit var fDroidAntiFeatureRepository: FDroidAntiFeatureRepository @@ -216,7 +212,8 @@ class AgeRatingProvider : ContentProvider() { private suspend fun initAuthData() { val authData = appLoungeDataStore.getAuthData() if (authData.email.isNotBlank() && authData.authToken.isNotBlank()) { - authenticatorRepository.setGPlayAuth(authData) + // TODO 20251204 : this method doesn't make any sense. + appLoungeDataStore.saveAuthData(authData) } } @@ -318,7 +315,6 @@ class AgeRatingProvider : ContentProvider() { val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ContentProviderEntryPoint::class.java) - authenticatorRepository = hiltEntryPoint.provideAuthenticationRepository() appLoungePackageManager = hiltEntryPoint.providePackageManager() gPlayContentRatingRepository = hiltEntryPoint.provideGPlayContentRatingsRepository() fDroidAntiFeatureRepository = hiltEntryPoint.provideFDroidAntiFeatureRepository() diff --git a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt index 99b9883d8..2f2582993 100644 --- a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt @@ -23,9 +23,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.Stores import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.domain.procedures.GPlayAuthentication import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.launch -- GitLab From 6c12f40f6dcaf2c5125203d15fb0ae1e91eca6a6 Mon Sep 17 00:00:00 2001 From: jacquarg Date: Fri, 5 Dec 2025 12:18:59 +0100 Subject: [PATCH 5/7] refac:3870: Create LoginUseCase to fed up LoginViewModel. --- .../domain/procedures/GPlayAuthentication.kt | 114 +----------------- .../e/apps/domain/usecases/LoginUseCase.kt | 107 ++++++++++++++++ .../foundation/e/apps/ui/LoginViewModel.kt | 14 +-- 3 files changed, 115 insertions(+), 120 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt diff --git a/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt b/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt index b866af9f2..8c588edfc 100644 --- a/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt +++ b/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt @@ -23,7 +23,6 @@ import javax.inject.Singleton @Singleton class GPlayAuthentication @Inject constructor( private val appLoungeDataStore: AppLoungeDataStore, - private val appLoungePreference: AppLoungePreference, private val playStoreAuthenticator: PlayStoreAuthenticator, private val json: Json, @ApplicationContext private val context: Context, @@ -33,122 +32,11 @@ class GPlayAuthentication @Inject constructor( private val locale: Locale get() = context.resources.configuration.locales[0] - - // Move to LoginUseCase ? or AuthenticationUseCase ? - - suspend fun setGoogleUser(email: String, oauth: String): List = withContext(ioCoroutineScope.coroutineContext) { - appLoungeDataStore.saveGoogleLogin(email, oauth) - appLoungeDataStore.saveUserType(User.GOOGLE) - - - val result = buildFreshGoogleLoginToken() - if (result.isSuccess) { - appLoungeDataStore.saveAuthData(result.getOrNull()) - - } - - buildAuthObjectList(result, User.GOOGLE) - } - - suspend fun setAnonymousUser(): List = withContext(ioCoroutineScope.coroutineContext) { - appLoungeDataStore.saveUserType(User.GOOGLE) - val result = buildFreshAnonymousLoginToken() - if (result.isSuccess) { - appLoungeDataStore.saveAuthData(result.getOrNull()) - } - - buildAuthObjectList(result, User.GOOGLE) - } - - - suspend fun setNoGoogleMode() = withContext(ioCoroutineScope.coroutineContext) { - appLoungePreference.run { - disablePlayStore() - enableOpenSource() - enablePwa() - } - appLoungeDataStore.saveUserType(User.NO_GOOGLE) - } - - suspend fun fetchAuthObjects(authTypes: List = listOf()): List { - val userType = appLoungeDataStore.getUserType() - - if (authTypes.isEmpty()) { - // use saved state. - val savedAuth = getSavedAuthData() - if (savedAuth != null) { - return buildAuthObjectList(Result.success(savedAuth), userType = userType) - } - } - - val result = updateToken() - - return if (result?.isSuccess == true) { - buildAuthObjectList(result, User.GOOGLE) - } else if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { - listOf(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) - } else { - emptyList() - } - } - - - private fun getSavedAuthData(): AuthData? { - val authJson = appLoungeDataStore.authData.getSync() - return if (authJson.isBlank()) null - else try { - json.decodeFromString(authJson) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - private fun buildAuthObjectList(gPlayloginResult: Result, userType: User): List { - val authObjectsLocal = ArrayList() - - // Google Login case: - val result: ResultSupreme - if (gPlayloginResult.isSuccess) { - result = ResultSupreme.create( - status = ResultStatus.OK, - data = gPlayloginResult.getOrNull() - ) - result.otherPayload = gPlayloginResult.getOrNull()?.email - } else { - result = ResultSupreme.create(status = ResultStatus.UNKNOWN, exception = gPlayloginResult.exceptionOrNull() as? Exception) - } - - authObjectsLocal.add(AuthObject.GPlayAuth(result, userType)) - - - // Clean apk - if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { - authObjectsLocal.add(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) - } - - return authObjectsLocal - } - - suspend fun logout() { - appLoungeDataStore.destroyCredentials() - appLoungeDataStore.saveUserType(null) - // reset app source preferences on logout. - appLoungePreference.run { - enableOpenSource() - enablePwa() - enablePlayStore() - } - } - - - /////////// END. - suspend fun refresh() { updateToken() } - private suspend fun updateToken(): Result? { + suspend fun updateToken(): Result? { val userType = appLoungeDataStore.getUserType() val result = when (userType) { diff --git a/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt b/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt new file mode 100644 index 000000000..79b64336f --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt @@ -0,0 +1,107 @@ +package foundation.e.apps.domain.usecases + +import com.aurora.gplayapi.data.models.AuthData +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.login.AuthObject +import foundation.e.apps.data.preference.AppLoungeDataStore +import foundation.e.apps.data.preference.AppLoungePreference +import foundation.e.apps.di.qualifiers.IoCoroutineScope +import foundation.e.apps.domain.procedures.GPlayAuthentication +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LoginUseCase @Inject constructor( + private val gplayAuthentication: GPlayAuthentication, + private val appLoungeDataStore: AppLoungeDataStore, + private val appLoungePreference: AppLoungePreference, + @IoCoroutineScope private val ioCoroutineScope: CoroutineScope, +) { + suspend fun setGoogleUser(email: String, oauth: String) { + withContext(ioCoroutineScope.coroutineContext) { + appLoungeDataStore.saveGoogleLogin(email, oauth) + appLoungeDataStore.saveUserType(User.GOOGLE) + } + } + + suspend fun setAnonymousUser() { + withContext(ioCoroutineScope.coroutineContext) { + appLoungeDataStore.saveUserType(User.GOOGLE) + } + } + + + suspend fun setNoGoogleMode() { + withContext(ioCoroutineScope.coroutineContext) { + appLoungePreference.run { + disablePlayStore() + enableOpenSource() + enablePwa() + } + appLoungeDataStore.saveUserType(User.NO_GOOGLE) + } + } + + suspend fun fetchAuthObjects(authTypes: List = listOf()): List { + val userType = appLoungeDataStore.getUserType() + + if (authTypes.isEmpty()) { + // use saved state. + val savedAuth = appLoungeDataStore.getAuthData() + if (savedAuth != AuthData("", "")) { + return buildAuthObjectList(Result.success(savedAuth), userType = userType) + } + } + + val result = gplayAuthentication.updateToken() + + return if (result?.isSuccess == true) { + buildAuthObjectList(result, User.GOOGLE) + } else if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { + listOf(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) + } else { + emptyList() + } + } + + private fun buildAuthObjectList(gPlayloginResult: Result, userType: User): List { + val authObjectsLocal = ArrayList() + + // Google Login case: + val result: ResultSupreme + if (gPlayloginResult.isSuccess) { + result = ResultSupreme.create( + status = ResultStatus.OK, + data = gPlayloginResult.getOrNull() + ) + result.otherPayload = gPlayloginResult.getOrNull()?.email + } else { + result = ResultSupreme.create(status = ResultStatus.UNKNOWN, exception = gPlayloginResult.exceptionOrNull() as? Exception) + } + + authObjectsLocal.add(AuthObject.GPlayAuth(result, userType)) + + + // Clean apk + if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { + authObjectsLocal.add(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) + } + + return authObjectsLocal + } + + suspend fun logout() { + appLoungeDataStore.destroyCredentials() + appLoungeDataStore.saveUserType(null) + // reset app source preferences on logout. + appLoungePreference.run { + enableOpenSource() + enablePwa() + enablePlayStore() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt index 2f2582993..ea421912f 100644 --- a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt @@ -24,7 +24,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.Stores import foundation.e.apps.data.enums.Source import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.domain.procedures.GPlayAuthentication +import foundation.e.apps.domain.usecases.LoginUseCase import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.launch import okhttp3.Cache @@ -36,7 +36,7 @@ import javax.inject.Inject */ @HiltViewModel class LoginViewModel @Inject constructor( - private val gPlayAuthentication: GPlayAuthentication, + private val loginUseCase: LoginUseCase, private val cache: Cache, private val stores: Stores ) : ViewModel() { @@ -58,7 +58,7 @@ class LoginViewModel @Inject constructor( */ fun startLoginFlow(clearList: List = listOf()) { viewModelScope.launch { - val authObjectsLocal = gPlayAuthentication.fetchAuthObjects(clearList) + val authObjectsLocal = loginUseCase.fetchAuthObjects(clearList) authObjects.postValue(authObjectsLocal) } } @@ -71,7 +71,7 @@ class LoginViewModel @Inject constructor( fun initialAnonymousLogin(onUserSaved: () -> Unit) { viewModelScope.launch { stores.enableStore(Source.PLAY_STORE) - gPlayAuthentication.setAnonymousUser() + loginUseCase.setAnonymousUser() onUserSaved() startLoginFlow() } @@ -86,7 +86,7 @@ class LoginViewModel @Inject constructor( fun initialGoogleLogin(email: String, oauthToken: String, onUserSaved: () -> Unit) { viewModelScope.launch { stores.enableStore(Source.PLAY_STORE) - gPlayAuthentication.setGoogleUser(email, oauthToken) + loginUseCase.setGoogleUser(email, oauthToken) onUserSaved() startLoginFlow() } @@ -102,7 +102,7 @@ class LoginViewModel @Inject constructor( fun initialNoGoogleLogin(onUserSaved: () -> Unit) { viewModelScope.launch { stores.disableStore(Source.PLAY_STORE) - gPlayAuthentication.setNoGoogleMode() + loginUseCase.setNoGoogleMode() onUserSaved() startLoginFlow() } @@ -137,7 +137,7 @@ class LoginViewModel @Inject constructor( fun logout() { viewModelScope.launch { cache.evictAll() - gPlayAuthentication.logout() + loginUseCase.logout() authObjects.postValue(listOf()) } } -- GitLab From 2bc6d0888bf52cbff65eaf2bb3390180c4d302a4 Mon Sep 17 00:00:00 2001 From: jacquarg Date: Mon, 8 Dec 2025 10:15:13 +0100 Subject: [PATCH 6/7] refac:3870: simplify fetch AuthData procedures. --- .../e/apps/data/login/LoginCommon.kt | 69 -------- .../apps/data/login/PlayStoreAuthenticator.kt | 162 ------------------ .../apps/data/login/api/GoogleLoginManager.kt | 68 -------- .../data/login/api/PlayStoreLoginManager.kt | 25 --- .../login/api/PlayStoreLoginManagerFactory.kt | 44 ----- .../data/login/api/PlayStoreLoginWrapper.kt | 111 ------------ .../AC2DMTask.kt => GoogleLoginDataSource.kt} | 61 ++++--- .../data/playstore/PlayStoreRepository.kt | 73 +++++++- .../TokenDispenserDataSource.kt} | 51 ++---- .../domain/procedures/GPlayAuthentication.kt | 101 ----------- .../e/apps/domain/usecases/LoginUseCase.kt | 12 +- .../e/apps/install/updates/UpdatesWorker.kt | 9 +- .../e/apps/provider/AgeRatingProvider.kt | 2 +- .../foundation/e/apps/ui/LoginViewModel.kt | 4 +- .../java/foundation/e/apps/ui/MainActivity.kt | 3 +- .../apps/ui/parentFragment/TimeoutFragment.kt | 3 +- 16 files changed, 133 insertions(+), 665 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt delete mode 100644 app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt rename app/src/main/java/foundation/e/apps/data/playstore/{utils/AC2DMTask.kt => GoogleLoginDataSource.kt} (51%) rename app/src/main/java/foundation/e/apps/data/{login/api/AnonymousLoginManager.kt => playstore/TokenDispenserDataSource.kt} (50%) delete mode 100644 app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt b/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt deleted file mode 100644 index f2ef1fe12..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2019-2025 e Foundation - * - * 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.login - -import foundation.e.apps.data.enums.User -import foundation.e.apps.data.preference.AppLoungeDataStore -import foundation.e.apps.data.preference.AppLoungePreference -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Contains common function for first login, logout, get which type of authentication / source - * to be used etc... - * - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -@Singleton -class LoginCommon @Inject constructor( - private val appLoungeDataStore: AppLoungeDataStore, - private val appLoungePreference: AppLoungePreference, -) { - suspend fun saveUserType(user: User) { - appLoungeDataStore.saveUserType(user) - } - - fun getUserType(): User { - return appLoungeDataStore.getUser() - } - - suspend fun saveGoogleLogin(email: String, oauth: String) { - appLoungeDataStore.saveGoogleLogin(email, oauth) - } - - suspend fun setNoGoogleMode() { - appLoungePreference.run { - disablePlayStore() - enableOpenSource() - enablePwa() - } - appLoungeDataStore.saveUserType(User.NO_GOOGLE) - } - - suspend fun logout() { - appLoungeDataStore.destroyCredentials() - appLoungeDataStore.saveUserType(null) - // reset app source preferences on logout. - appLoungePreference.run { - enableOpenSource() - enablePwa() - enablePlayStore() - } - } -} diff --git a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt deleted file mode 100644 index e2160f5fe..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (C) 2019-2025 e Foundation - * - * 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.login - -import android.content.Context -import com.aurora.gplayapi.data.models.AuthData -import dagger.hilt.android.qualifiers.ApplicationContext -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.login.api.GoogleLoginManager -import foundation.e.apps.data.login.api.PlayStoreLoginManager -import foundation.e.apps.data.login.api.PlayStoreLoginManagerFactory -import foundation.e.apps.data.login.api.PlayStoreLoginWrapper -import foundation.e.apps.data.preference.AppLoungeDataStore -import foundation.e.apps.data.preference.AppLoungePreference -import foundation.e.apps.data.preference.getSync -import foundation.e.apps.data.retryWithBackoff -import kotlinx.serialization.json.Json -import timber.log.Timber -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Class to get GPlay auth data. Call [login] to get an already saved auth data - * or to fetch a new one for first use. Handles auth validation internally. - * - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -@Singleton -class PlayStoreAuthenticator @Inject constructor( - @ApplicationContext private val context: Context, - private val json: Json, - private val appLoungeDataStore: AppLoungeDataStore, -) { - - @Inject - lateinit var loginManagerFactory: PlayStoreLoginManagerFactory - - private val user: User - get() = appLoungeDataStore.getUser() - - private val loginManager: PlayStoreLoginManager - get() = loginManagerFactory.createLoginManager(user) - - private val loginWrapper: PlayStoreLoginWrapper - get() = PlayStoreLoginWrapper(loginManager, user) - - private val locale: Locale - get() = context.resources.configuration.locales[0] - - - /** - * Get authData stored as JSON and convert to AuthData class. - * Returns null if nothing is saved. - */ - private fun getSavedAuthData(): AuthData? { - val authJson = appLoungeDataStore.authData.getSync() - return if (authJson.isBlank()) null - else try { - json.decodeFromString(authJson) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - private suspend fun saveAuthData(authData: AuthData) { - appLoungeDataStore.saveAuthData(authData) - } - - /** - * Generate new AuthData based on the user type. - */ - private suspend fun generateAuthData(): ResultSupreme { - return when (appLoungeDataStore.getUser()) { - User.ANONYMOUS -> getAuthDataAnonymously() - User.GOOGLE -> getAuthDataWithGoogleAccount() - else -> ResultSupreme.Error("User type not ANONYMOUS or GOOGLE") - } - } - - /** - * Aurora OSS GPlay API complains of missing headers sometimes. - * Converting [authData] to Json and back to [AuthData] fixed it. - */ - private fun formatAuthData(authData: AuthData): AuthData { - val localAuthDataJson = json.encodeToString(authData) - return json.decodeFromString(localAuthDataJson) - } - - suspend fun getAuthDataAnonymously(): ResultSupreme { - return loginWrapper.login(locale).run { - if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) - else this - } - } - - suspend fun getAuthDataWithGoogleAccount(): ResultSupreme { - - val email = appLoungeDataStore.emailData.getSync() - val oauthToken = appLoungeDataStore.oauthToken.getSync() - val aasToken = appLoungeDataStore.aasToken.getSync() - /* - * If aasToken is not blank, means it was stored successfully from a previous Google login. - * Use it to fetch auth data. - */ - if (aasToken.isNotBlank()) { - return loginWrapper.login(locale) - } - - /* - * If aasToken is not yet saved / made, fetch it from email and oauthToken. - */ - val aasTokenResponse = loginWrapper.getAasToken( - loginManager as GoogleLoginManager, - email, - oauthToken - ) - - /* - * If fetch was unsuccessful, return blank auth data. - * We replicate from the response, so that it will carry on any error message if present - * in the aasTokenResponse. - */ - if (!aasTokenResponse.isSuccess()) { - return ResultSupreme.replicate(aasTokenResponse, null) - } - - val aasTokenFetched = aasTokenResponse.data ?: "" - - if (aasTokenFetched.isBlank()) { - return ResultSupreme.Error("Fetched AAS Token is blank") - } - - /* - * Finally save the aasToken and create auth data. - */ - appLoungeDataStore.saveAasToken(aasTokenFetched) - return loginWrapper.login(locale).run { - if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) - else this - } - } -} diff --git a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt deleted file mode 100644 index 9ff9215fb..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * 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.login.api - -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse -import com.aurora.gplayapi.helpers.AuthHelper -import foundation.e.apps.data.playstore.utils.AC2DMTask -import foundation.e.apps.data.preference.AppLoungeDataStore -import foundation.e.apps.data.preference.getSync -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.util.Properties - -class GoogleLoginManager( - private val nativeDeviceProperty: Properties, - private val aC2DMTask: AC2DMTask, - private val appLoungeDataStore: AppLoungeDataStore, -) : PlayStoreLoginManager { - - /** - * Get PlayResponse for AC2DM Map. This allows us to get an error message too. - * - * An aasToken is extracted from this map. This is passed to [login] - * to generate AuthData. This token is very important as it cannot be regenerated, - * hence it must be saved for future use. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ - suspend fun getAC2DMResponse(email: String, oauthToken: String): PlayResponse { - var response: PlayResponse - withContext(Dispatchers.IO) { - response = aC2DMTask.getAC2DMResponse(email, oauthToken) - } - return response - } - - /** - * Login - * - * @return authData: authentication data - */ - override suspend fun login(): AuthData? { - val email = appLoungeDataStore.emailData.getSync() - val aasToken = appLoungeDataStore.aasToken.getSync() - - var authData: AuthData? - withContext(Dispatchers.IO) { - authData = AuthHelper.build(email, aasToken, tokenType = AuthHelper.Token.AAS, properties = nativeDeviceProperty) - } - return authData - } -} diff --git a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt deleted file mode 100644 index 29499c255..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * 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.login.api - -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse - -interface PlayStoreLoginManager { - suspend fun login(): AuthData? -} diff --git a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt deleted file mode 100644 index d6e09ccab..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * 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.login.api - -import foundation.e.apps.data.enums.User -import foundation.e.apps.data.playstore.utils.AC2DMTask -import foundation.e.apps.data.playstore.utils.GPlayHttpClient -import foundation.e.apps.data.preference.AppLoungeDataStore -import kotlinx.serialization.json.Json -import java.util.Properties -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PlayStoreLoginManagerFactory @Inject constructor( - private val gPlayHttpClient: GPlayHttpClient, - private val nativeDeviceProperty: Properties, - private val aC2DMTask: AC2DMTask, - private val json: Json, - private val appLoungeDataStore: AppLoungeDataStore, -) { - - fun createLoginManager(user: User): PlayStoreLoginManager { - return when (user) { - User.GOOGLE -> GoogleLoginManager(nativeDeviceProperty, aC2DMTask, appLoungeDataStore) - else -> AnonymousLoginManager(gPlayHttpClient, nativeDeviceProperty, json) - } - } -} diff --git a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt deleted file mode 100644 index ac1e8401b..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * 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.login.api - -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.enums.User -import foundation.e.apps.data.handleNetworkResult -import foundation.e.apps.data.login.exceptions.GPlayLoginException -import foundation.e.apps.data.playstore.utils.AC2DMUtil -import foundation.e.apps.utils.eventBus.AppEvent -import foundation.e.apps.utils.eventBus.EventBus -import java.util.Locale - -/** - * Call methods of [GoogleLoginManager] and [AnonymousLoginManager] from here. - * - * Dependency Injection via hilt is not possible, - * we need to manually check login type, create an instance of either [GoogleLoginManager] - * or [AnonymousLoginManager] and pass it to [loginManager]. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -class PlayStoreLoginWrapper constructor( - private val loginManager: PlayStoreLoginManager, - private val user: User, -) { - - /** - * Gets the auth data from instance of [PlayStoreLoginManager]. - */ - suspend fun login(locale: Locale): ResultSupreme { - val result = handleNetworkResult { - loginManager.login() - } - return result.apply { - this.data?.locale = locale - this.exception = when (result) { - is ResultSupreme.Timeout -> GPlayLoginException(true, "GPlay API timeout", user) - is ResultSupreme.Error -> GPlayLoginException(false, result.message, user) - else -> { - EventBus.invokeEvent(AppEvent.SuccessfulLogin(user)) - null - } - } - } - } - - /** - * Gets email and oauthToken from Google login, finds the AASToken from AC2DM response - * and returns it. This token is then used to fetch AuthData from [login]. - * - * Do note that for a given oauthToken, it has been observed that AASToken can - * only be generated once. So this token must be saved for future use. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - * - * @param googleAccountLoginManager An instance of [GoogleLoginManager] must be passed, this method - * cannot work on [loginManager] as it is a common interface for both Google and Anonymous - * login, but this method is only for Google login. - */ - suspend fun getAasToken( - googleAccountLoginManager: GoogleLoginManager, - email: String, - oauthToken: String - ): ResultSupreme { - val result = handleNetworkResult { - var aasToken = "" - val response = googleAccountLoginManager.getAC2DMResponse(email, oauthToken) - var error = response.errorString - if (response.isSuccessful) { - val responseMap = AC2DMUtil.parseResponse(String(response.responseBytes)) - aasToken = responseMap["Token"] ?: "" - if (aasToken.isBlank() && error.isBlank()) { - error = "AASToken not found in map." - } - } - /* - * Default value of PlayResponse.errorString is "No Error". - * https://gitlab.com/AuroraOSS/gplayapi/-/blob/master/src/main/java/com/aurora/gplayapi/data/models/PlayResponse.kt - */ - if (error != "No Error") { - throw Exception(error) - } - aasToken - } - return result.apply { - this.exception = when (result) { - is ResultSupreme.Timeout -> GPlayLoginException(true, "GPlay API timeout", User.GOOGLE) - is ResultSupreme.Error -> GPlayLoginException(false, result.message, User.GOOGLE) - else -> null - } - } - } -} diff --git a/app/src/main/java/foundation/e/apps/data/playstore/utils/AC2DMTask.kt b/app/src/main/java/foundation/e/apps/data/playstore/GoogleLoginDataSource.kt similarity index 51% rename from app/src/main/java/foundation/e/apps/data/playstore/utils/AC2DMTask.kt rename to app/src/main/java/foundation/e/apps/data/playstore/GoogleLoginDataSource.kt index f1e3eb3eb..d25715858 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/utils/AC2DMTask.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/GoogleLoginDataSource.kt @@ -1,30 +1,17 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021, Rahul Kumar Patel - * Copyright (C) 2021 E FOUNDATION - * - * 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.playstore -package foundation.e.apps.data.playstore.utils - -import com.aurora.gplayapi.data.models.PlayResponse +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.helpers.AuthHelper +import foundation.e.apps.data.playstore.utils.AC2DMUtil +import foundation.e.apps.data.playstore.utils.GPlayHttpClient import okhttp3.RequestBody.Companion.toRequestBody import java.util.Locale +import java.util.Properties import javax.inject.Inject +import javax.inject.Singleton -class AC2DMTask @Inject constructor( +@Singleton +class GoogleLoginDataSource @Inject constructor( private val gPlayHttpClient: GPlayHttpClient ) { companion object { @@ -33,10 +20,7 @@ class AC2DMTask @Inject constructor( private const val PLAY_SERVICES_VERSION_CODE = 19629032 } - fun getAC2DMResponse(email: String?, oAuthToken: String?): PlayResponse { - if (email == null || oAuthToken == null) - return PlayResponse() - + fun getAasToken(email: String, oAuthToken: String): String { val params: MutableMap = hashMapOf() params["lang"] = Locale.getDefault().toString().replace("_", "-") params["google_play_services_version"] = PLAY_SERVICES_VERSION_CODE @@ -62,6 +46,27 @@ class AC2DMTask @Inject constructor( * Returning PlayResponse instead of map so that we can get the network response code. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ - return gPlayHttpClient.post(TOKEN_AUTH_URL, header, body.toRequestBody()) + val response = gPlayHttpClient.post(TOKEN_AUTH_URL, header, body.toRequestBody()) + var aasToken = "" + var error = response.errorString + if (response.isSuccessful) { + val responseMap = AC2DMUtil.parseResponse(String(response.responseBytes)) + aasToken = responseMap["Token"] ?: "" + if (aasToken.isBlank() && error.isBlank()) { + error = "AASToken not found in map." + } + } + /* + * Default value of PlayResponse.errorString is "No Error". + * https://gitlab.com/AuroraOSS/gplayapi/-/blob/master/src/main/java/com/aurora/gplayapi/data/models/PlayResponse.kt + */ + if (error != "No Error") { + throw Exception(error) + } + return aasToken + } + + suspend fun googleLogin(email: String, aasToken: String, nativeDeviceProperty: Properties): AuthData { + return AuthHelper.build(email, aasToken, tokenType = AuthHelper.Token.AAS, properties = nativeDeviceProperty) } -} +} \ No newline at end of file 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 d6c52dfde..bf4d6de11 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 @@ -24,7 +24,6 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.ContentRating import com.aurora.gplayapi.data.models.PlayFile -import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.providers.HeaderProvider import com.aurora.gplayapi.helpers.AppDetailsHelper @@ -38,7 +37,6 @@ import com.aurora.gplayapi.helpers.web.WebCategoryStreamHelper import com.aurora.gplayapi.helpers.web.WebTopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R -import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.StoreRepository import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.data.Application @@ -47,17 +45,21 @@ import foundation.e.apps.data.application.search.SearchSuggestion import foundation.e.apps.data.application.utils.CategoryType import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.User import foundation.e.apps.data.handleNetworkResult -import foundation.e.apps.data.login.exceptions.GPlayLoginException +import foundation.e.apps.data.playstore.TokenDispenserDataSource import foundation.e.apps.data.playstore.utils.GPlayHttpClient +import foundation.e.apps.data.playstore.GoogleLoginDataSource import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.AppLoungeDataStore -import foundation.e.apps.domain.procedures.GPlayAuthentication import foundation.e.apps.utils.SystemInfoProvider import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import timber.log.Timber +import java.util.Properties import javax.inject.Inject import com.aurora.gplayapi.data.models.App as GplayApp @@ -65,12 +67,67 @@ import com.aurora.gplayapi.data.models.App as GplayApp class PlayStoreRepository @Inject constructor( @ApplicationContext private val context: Context, private val gPlayHttpClient: GPlayHttpClient, - private val gPlayAuthentication: GPlayAuthentication, private val applicationDataManager: ApplicationDataManager, private val playStoreSearchHelper: PlayStoreSearchHelper, - private val appLoungeDataStore: AppLoungeDataStore + private val appLoungeDataStore: AppLoungeDataStore, + private val tokenDispenserDataSource: TokenDispenserDataSource, + private val googleLoginDataSource: GoogleLoginDataSource, + private val nativeDeviceProperty: Properties, + private val json: Json, ) : StoreRepository { + + suspend fun updateToken(): AuthData { + val userType = appLoungeDataStore.getUserType() + + val rawAuthToken = when (userType) { + User.GOOGLE -> fetchGoogleLoginToken() + User.ANONYMOUS -> tokenDispenserDataSource.fetchGToken(nativeDeviceProperty) + else -> throw Exception("User type not ANONYMOUS or GOOGLE") // TODO 20251208 <- better handle this case ? + } + + val authData = formatAuthData(rawAuthToken) + + // Force locale to be the one configured on the device. But didn't we expect to have the one of the Google Account here ? + authData.locale = context.resources.configuration.locales[0] //nativeDeviceProperty.getProperty("Locales").split(",")[0] + + appLoungeDataStore.saveAuthData(authData) + + return authData + } + + private suspend fun fetchGoogleLoginToken(): AuthData = withContext(Dispatchers.IO) { + val email = appLoungeDataStore.emailData.first() + var aasToken: String = appLoungeDataStore.aasToken.first() + + /* + * If aasToken is not blank, means it was stored successfully from a previous Google login. + * We will use it to fetch auth data. Otherwie, fetch it from email and oauthToken. + */ + if (aasToken.isBlank()) { + aasToken = googleLoginDataSource.getAasToken(email, appLoungeDataStore.oauthToken.first()) + + if (aasToken.isBlank()) { + // TODO 20251208 : Unit Test this case, improve exception + throw Exception("Fetched AAS Token is blank") + } + + appLoungeDataStore.saveAasToken(aasToken) + } + + googleLoginDataSource.googleLogin(email, aasToken, nativeDeviceProperty) + } + + /** + * Aurora OSS GPlay API complains of missing headers sometimes. + * Converting [authData] to Json and back to [AuthData] fixed it. + */ + private fun formatAuthData(authData: AuthData): AuthData { + val localAuthDataJson = json.encodeToString(authData) + return json.decodeFromString(localAuthDataJson) + } + + override suspend fun getHomeScreenData(list: MutableList): List { val homeScreenData = mutableMapOf>() val homeElements = createTopChartElements() @@ -216,7 +273,7 @@ class PlayStoreRepository @Inject constructor( private suspend fun refreshPlayStoreAuthentication() { Timber.i("Refreshing authentication.") - gPlayAuthentication.refresh() + updateToken() } suspend fun getAppDetailsWeb(packageName: String): Application? { @@ -344,7 +401,7 @@ class PlayStoreRepository @Inject constructor( // Helper function, to detect error not specific to the request // (authentication stale, network unreachable, ...) // This can be used to workaround Exception swallowing done by GPlayAPI library. - private suspend fun sendCommonErrorsProbe() { + suspend fun sendCommonErrorsProbe() { withContext(Dispatchers.IO) { val endpoint: String = GooglePlayApi.URL_SYNC val headers = HeaderProvider.getDefaultHeaders(appLoungeDataStore.getAuthData()) diff --git a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt b/app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt similarity index 50% rename from app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt rename to app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt index f6fa89835..b44e77321 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt @@ -1,22 +1,4 @@ -/* - * Copyright (C) 2019-2025 e Foundation - * - * 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.login.api +package foundation.e.apps.data.playstore import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.helpers.AuthHelper @@ -27,12 +9,15 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.util.Locale import java.util.Properties +import javax.inject.Inject +import javax.inject.Singleton -class AnonymousLoginManager( +// TODO: 20251208: this DataSource should be merge with foundation.e.apps.data.ecloud.EcloudRepository +@Singleton +class TokenDispenserDataSource @Inject constructor ( private val gPlayHttpClient: GPlayHttpClient, - private val nativeDeviceProperty: Properties, private val json: Json, -) : PlayStoreLoginManager { +) { private val tokenUrl: String = "https://eu.gtoken.ecloud.global" @@ -41,25 +26,24 @@ class AnonymousLoginManager( * * @return authData: authentication data */ - override suspend fun login(): AuthData? { - var authData: AuthData? = null - withContext(Dispatchers.IO) { + suspend fun fetchGToken(nativeDeviceProperty: Properties): AuthData { + return withContext(Dispatchers.IO) { val response = gPlayHttpClient.postAuth( tokenUrl, json.encodeToString(nativeDeviceProperty).toByteArray() ) if (response.code != 200 || !response.isSuccessful) { throw Exception( "Error fetching Anonymous credentials\n" + - "Network code: ${response.code}\n" + - "Success: ${response.isSuccessful}" + - response.errorString.run { - if (isNotBlank()) "\nError message: $this" - else "" - } + "Network code: ${response.code}\n" + + "Success: ${response.isSuccessful}" + + response.errorString.run { + if (isNotBlank()) "\nError message: $this" + else "" + } ) } else { val auth = json.decodeFromString(String(response.responseBytes)) - authData = AuthHelper.build( + AuthHelper.build( email = auth.email, token = auth.auth, tokenType = AuthHelper.Token.AUTH, @@ -69,6 +53,5 @@ class AnonymousLoginManager( ) } } - return authData } -} +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt b/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt deleted file mode 100644 index 8c588edfc..000000000 --- a/app/src/main/java/foundation/e/apps/domain/procedures/GPlayAuthentication.kt +++ /dev/null @@ -1,101 +0,0 @@ -package foundation.e.apps.domain.procedures - -import android.content.Context -import coil.request.NullRequestDataException -import com.aurora.gplayapi.data.models.AuthData -import dagger.hilt.android.qualifiers.ApplicationContext -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.login.AuthObject -import foundation.e.apps.data.login.PlayStoreAuthenticator -import foundation.e.apps.data.preference.AppLoungeDataStore -import foundation.e.apps.data.preference.AppLoungePreference -import foundation.e.apps.data.preference.getSync -import foundation.e.apps.di.qualifiers.IoCoroutineScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class GPlayAuthentication @Inject constructor( - private val appLoungeDataStore: AppLoungeDataStore, - private val playStoreAuthenticator: PlayStoreAuthenticator, - private val json: Json, - @ApplicationContext private val context: Context, - @IoCoroutineScope private val ioCoroutineScope: CoroutineScope, - ) { - - private val locale: Locale - get() = context.resources.configuration.locales[0] - - suspend fun refresh() { - updateToken() - } - - suspend fun updateToken(): Result? { - val userType = appLoungeDataStore.getUserType() - - val result = when (userType) { - User.GOOGLE -> buildFreshGoogleLoginToken() - User.ANONYMOUS -> buildFreshAnonymousLoginToken() - else -> null - } - - if (result?.isSuccess == true) { - appLoungeDataStore.saveAuthData(result.getOrNull()) - } - return result - } - - - // hould be a repository method. - private suspend fun buildFreshGoogleLoginToken(): Result { - -// val result = retryWithBackoff { - val result = playStoreAuthenticator.getAuthDataWithGoogleAccount() - -// if (result != null && !result.isSuccess()) { -// return AuthObject.GPlayAuth(result, user) -// } - val rawData = result.data - if (!result.isSuccess()) { - return Result.failure(result.exception ?: Exception("Result without exception")) - } else if (result.isSuccess() && rawData == null) { - return Result.failure(NullRequestDataException()) - } - - - /* - * Aurora OSS GPlay API complains of missing headers sometimes. - * Converting [authData] to Json and back to [AuthData] fixed it. - */ - val authData = formatAuthData(result.data!!) - - // Force locale to be the one configured on the device. But didn't we expect to have the one of the Google Account here ? - authData.locale = locale - return Result.success(authData) - - } - - private suspend fun buildFreshAnonymousLoginToken(): Result { - val result = playStoreAuthenticator.getAuthDataAnonymously() - return if (!result.isSuccess()) { - Result.failure(result.exception ?: Exception("Result without exception")) - } else { - Result.success(result.data!!) - } - } - - /** - * Aurora OSS GPlay API complains of missing headers sometimes. - * Converting [authData] to Json and back to [AuthData] fixed it. - */ - private fun formatAuthData(authData: AuthData): AuthData { - val localAuthDataJson = json.encodeToString(authData) - return json.decodeFromString(localAuthDataJson) - } -} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt b/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt index 79b64336f..a7a16d618 100644 --- a/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt @@ -5,10 +5,10 @@ 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.login.AuthObject +import foundation.e.apps.data.playstore.PlayStoreRepository import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.di.qualifiers.IoCoroutineScope -import foundation.e.apps.domain.procedures.GPlayAuthentication import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import javax.inject.Inject @@ -16,7 +16,7 @@ import javax.inject.Singleton @Singleton class LoginUseCase @Inject constructor( - private val gplayAuthentication: GPlayAuthentication, + private val playStoreRepository: PlayStoreRepository, private val appLoungeDataStore: AppLoungeDataStore, private val appLoungePreference: AppLoungePreference, @IoCoroutineScope private val ioCoroutineScope: CoroutineScope, @@ -46,10 +46,10 @@ class LoginUseCase @Inject constructor( } } - suspend fun fetchAuthObjects(authTypes: List = listOf()): List { + suspend fun fetchAuthObjects(forceRefresh: Boolean): List { val userType = appLoungeDataStore.getUserType() - if (authTypes.isEmpty()) { + if (!forceRefresh) { // use saved state. val savedAuth = appLoungeDataStore.getAuthData() if (savedAuth != AuthData("", "")) { @@ -57,9 +57,9 @@ class LoginUseCase @Inject constructor( } } - val result = gplayAuthentication.updateToken() + val result = runCatching { playStoreRepository.updateToken() } - return if (result?.isSuccess == true) { + return if (result.isSuccess) { buildAuthObjectList(result, User.GOOGLE) } else if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { listOf(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt index 52882a93a..d7f9b5de2 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt @@ -21,6 +21,7 @@ import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository +import foundation.e.apps.data.playstore.PlayStoreRepository import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.install.workmanager.AppInstallProcessor @@ -40,6 +41,7 @@ class UpdatesWorker @AssistedInject constructor( private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, + private val playStoreRepository: PlayStoreRepository, ) : CoroutineWorker(context, params) { companion object { @@ -107,9 +109,13 @@ class UpdatesWorker @AssistedInject constructor( val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val appsNeededToUpdate = mutableListOf() val user = getUser() - val authData = appLoungeDataStore.getAuthData() + val authData = appLoungeDataStore.getAuthData().takeIf { + runCatching { playStoreRepository.sendCommonErrorsProbe() }.isSuccess + } val resultStatus: ResultStatus + + if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { /* * Signifies valid Google user and valid auth data to update @@ -139,7 +145,6 @@ class UpdatesWorker @AssistedInject constructor( } if (resultStatus != ResultStatus.OK) { - // TODO 20251204 : detect GPlay token error, and call GPlayAuthentication.refresh() in this case. manageRetry(resultStatus.toString()) } else { /* diff --git a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt index de7e7aaba..cbad8dce2 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -209,10 +209,10 @@ class AgeRatingProvider : ContentProvider() { * Setup AuthData for other APIs to access, * if user has logged in with Google or Anonymous mode. */ + // TODO 20251204 : this method doesn't make any sense. private suspend fun initAuthData() { val authData = appLoungeDataStore.getAuthData() if (authData.email.isNotBlank() && authData.authToken.isNotBlank()) { - // TODO 20251204 : this method doesn't make any sense. appLoungeDataStore.saveAuthData(authData) } } diff --git a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt index ea421912f..dc4605960 100644 --- a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt @@ -56,9 +56,9 @@ class LoginViewModel @Inject constructor( /** * Main point of starting of entire authentication process. */ - fun startLoginFlow(clearList: List = listOf()) { + fun startLoginFlow(forceRefresh: Boolean = false) { viewModelScope.launch { - val authObjectsLocal = loginUseCase.fetchAuthObjects(clearList) + val authObjectsLocal = loginUseCase.fetchAuthObjects(forceRefresh) authObjects.postValue(authObjectsLocal) } } 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 74e346889..f93417665 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -51,7 +51,6 @@ 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.PlayStoreAuthenticator import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.install.updates.UpdatesNotifier @@ -182,7 +181,7 @@ class MainActivity : AppCompatActivity() { } private fun refreshSession() { - loginViewModel.startLoginFlow(listOf(PlayStoreAuthenticator::class.java.simpleName)) + loginViewModel.startLoginFlow(forceRefresh = true) } fun isInitialScreen(): Boolean { diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt index 42c17f976..841815c1d 100644 --- a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt @@ -34,7 +34,6 @@ import androidx.lifecycle.ViewModelProvider import foundation.e.apps.R import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.PlayStoreAuthenticator import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.CleanApkIOException import foundation.e.apps.data.login.exceptions.GPlayException @@ -210,7 +209,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { * Clears saved GPlay AuthData and restarts login process to get */ fun clearAndRestartGPlayLogin() { - loginViewModel.startLoginFlow(listOf(PlayStoreAuthenticator::class.java.simpleName)) + loginViewModel.startLoginFlow(forceRefresh = true) } /** -- GitLab From 6904534867d9c81f0d71647d6fd923010ec9cd43 Mon Sep 17 00:00:00 2001 From: jacquarg Date: Tue, 9 Dec 2025 11:17:15 +0100 Subject: [PATCH 7/7] refac:3870: Add unit tests. --- .../data/playstore/GoogleLoginDataSource.kt | 5 +- .../data/playstore/PlayStoreRepository.kt | 28 +-- .../playstore/TokenDispenserDataSource.kt | 25 ++- .../data/playstore/utils/GPlayHttpClient.kt | 2 +- .../data/preference/AppLoungeDataStore.kt | 4 +- .../e/apps/domain/usecases/LoginUseCase.kt | 65 +++--- .../e/apps/install/updates/UpdatesWorker.kt | 3 +- .../e/apps/receivers/DumpAuthData.kt | 2 +- .../signin/LocaleChangedBroadcastReceiver.kt | 2 +- .../data/playstore/PlayStoreRepositoryTest.kt | 161 +++++++++++++++ .../apps/domain/usecases/LoginUseCaseTest.kt | 194 ++++++++++++++++++ .../e/apps/login/LoginViewModelTest.kt | 6 +- 12 files changed, 424 insertions(+), 73 deletions(-) create mode 100644 app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt create mode 100644 app/src/test/java/foundation/e/apps/domain/usecases/LoginUseCaseTest.kt diff --git a/app/src/main/java/foundation/e/apps/data/playstore/GoogleLoginDataSource.kt b/app/src/main/java/foundation/e/apps/data/playstore/GoogleLoginDataSource.kt index d25715858..8f21743a2 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/GoogleLoginDataSource.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/GoogleLoginDataSource.kt @@ -2,6 +2,7 @@ package foundation.e.apps.data.playstore import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.helpers.AuthHelper +import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.playstore.utils.AC2DMUtil import foundation.e.apps.data.playstore.utils.GPlayHttpClient import okhttp3.RequestBody.Companion.toRequestBody @@ -61,7 +62,7 @@ class GoogleLoginDataSource @Inject constructor( * https://gitlab.com/AuroraOSS/gplayapi/-/blob/master/src/main/java/com/aurora/gplayapi/data/models/PlayResponse.kt */ if (error != "No Error") { - throw Exception(error) + throw GPlayException(false, error) } return aasToken } @@ -69,4 +70,4 @@ class GoogleLoginDataSource @Inject constructor( suspend fun googleLogin(email: String, aasToken: String, nativeDeviceProperty: Properties): AuthData { return AuthHelper.build(email, aasToken, tokenType = AuthHelper.Token.AAS, properties = nativeDeviceProperty) } -} \ No newline at end of file +} 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 bf4d6de11..92093ad28 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 @@ -47,9 +47,7 @@ import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.User import foundation.e.apps.data.handleNetworkResult -import foundation.e.apps.data.playstore.TokenDispenserDataSource import foundation.e.apps.data.playstore.utils.GPlayHttpClient -import foundation.e.apps.data.playstore.GoogleLoginDataSource import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.utils.SystemInfoProvider @@ -63,7 +61,7 @@ import java.util.Properties import javax.inject.Inject import com.aurora.gplayapi.data.models.App as GplayApp -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LongParameterList") class PlayStoreRepository @Inject constructor( @ApplicationContext private val context: Context, private val gPlayHttpClient: GPlayHttpClient, @@ -75,21 +73,20 @@ class PlayStoreRepository @Inject constructor( private val nativeDeviceProperty: Properties, private val json: Json, ) : StoreRepository { - - suspend fun updateToken(): AuthData { - val userType = appLoungeDataStore.getUserType() + val userType = appLoungeDataStore.getUser() val rawAuthToken = when (userType) { User.GOOGLE -> fetchGoogleLoginToken() User.ANONYMOUS -> tokenDispenserDataSource.fetchGToken(nativeDeviceProperty) - else -> throw Exception("User type not ANONYMOUS or GOOGLE") // TODO 20251208 <- better handle this case ? + else -> error("User type not ANONYMOUS or GOOGLE") } val authData = formatAuthData(rawAuthToken) - // Force locale to be the one configured on the device. But didn't we expect to have the one of the Google Account here ? - authData.locale = context.resources.configuration.locales[0] //nativeDeviceProperty.getProperty("Locales").split(",")[0] + // Force locale to be the one configured on the device. + // But didn't we expect to have the one of the Google Account here ? + authData.locale = context.resources.configuration.locales[0] appLoungeDataStore.saveAuthData(authData) @@ -101,17 +98,11 @@ class PlayStoreRepository @Inject constructor( var aasToken: String = appLoungeDataStore.aasToken.first() /* - * If aasToken is not blank, means it was stored successfully from a previous Google login. - * We will use it to fetch auth data. Otherwie, fetch it from email and oauthToken. - */ + * If aasToken is not blank, means it was stored successfully from a previous Google login. + * We will use it to fetch auth data. Otherwie, fetch it from email and oauthToken. + */ if (aasToken.isBlank()) { aasToken = googleLoginDataSource.getAasToken(email, appLoungeDataStore.oauthToken.first()) - - if (aasToken.isBlank()) { - // TODO 20251208 : Unit Test this case, improve exception - throw Exception("Fetched AAS Token is blank") - } - appLoungeDataStore.saveAasToken(aasToken) } @@ -127,7 +118,6 @@ class PlayStoreRepository @Inject constructor( return json.decodeFromString(localAuthDataJson) } - override suspend fun getHomeScreenData(list: MutableList): List { val homeScreenData = mutableMapOf>() val homeElements = createTopChartElements() diff --git a/app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt b/app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt index b44e77321..9d127fde6 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt @@ -3,7 +3,9 @@ package foundation.e.apps.data.playstore import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.helpers.AuthHelper import foundation.e.apps.data.login.Auth +import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.playstore.utils.GPlayHttpClient +import foundation.e.apps.data.playstore.utils.GPlayHttpClient.Companion.STATUS_CODE_OK import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json @@ -14,7 +16,7 @@ import javax.inject.Singleton // TODO: 20251208: this DataSource should be merge with foundation.e.apps.data.ecloud.EcloudRepository @Singleton -class TokenDispenserDataSource @Inject constructor ( +class TokenDispenserDataSource @Inject constructor( private val gPlayHttpClient: GPlayHttpClient, private val json: Json, ) { @@ -29,17 +31,18 @@ class TokenDispenserDataSource @Inject constructor ( suspend fun fetchGToken(nativeDeviceProperty: Properties): AuthData { return withContext(Dispatchers.IO) { val response = gPlayHttpClient.postAuth( - tokenUrl, json.encodeToString(nativeDeviceProperty).toByteArray() + tokenUrl, + json.encodeToString(nativeDeviceProperty).toByteArray() ) - if (response.code != 200 || !response.isSuccessful) { - throw Exception( + if (response.code != STATUS_CODE_OK || !response.isSuccessful) { + throw GPlayException( + false, "Error fetching Anonymous credentials\n" + - "Network code: ${response.code}\n" + - "Success: ${response.isSuccessful}" + - response.errorString.run { - if (isNotBlank()) "\nError message: $this" - else "" - } + "Network code: ${response.code}\n" + + "Success: ${response.isSuccessful}" + + response.errorString.run { + if (isNotBlank()) "\nError message: $this" else "" + } ) } else { val auth = json.decodeFromString(String(response.responseBytes)) @@ -54,4 +57,4 @@ class TokenDispenserDataSource @Inject constructor ( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt index 235e9ab52..0f504158a 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt @@ -56,7 +56,7 @@ class GPlayHttpClient @Inject constructor( private const val HTTP_METHOD_POST = "POST" private const val HTTP_METHOD_GET = "GET" private const val SEARCH_SUGGEST = "searchSuggest" - private const val STATUS_CODE_OK = 200 + const val STATUS_CODE_OK = 200 const val STATUS_CODE_UNAUTHORIZED = 401 const val STATUS_CODE_TOO_MANY_REQUESTS = 429 private const val URL_SUBSTRING_PURCHASE = "purchase" 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 871f9fae3..8d8e9606f 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 @@ -71,7 +71,7 @@ class AppLoungeDataStore @Inject constructor( private val TOCSTATUS = booleanPreferencesKey("tocStatus") private val TOSVERSION = stringPreferencesKey("tosversion") - val authData = context.dataStore.data.map { it[AUTHDATA] ?: "" } + val rawAuthData = context.dataStore.data.map { it[AUTHDATA] ?: "" } val emailData = context.dataStore.data.map { it[EMAIL] ?: "" } val oauthToken = context.dataStore.data.map { it[OAUTHTOKEN] ?: "" } val aasToken = context.dataStore.data.map { it[AASTOKEN] ?: "" } @@ -90,7 +90,7 @@ class AppLoungeDataStore @Inject constructor( } fun getAuthData(): AuthData { - val authData = authData.getSync() + val authData = rawAuthData.getSync() return if (authData.isEmpty()) { AuthData("", "") } else { diff --git a/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt b/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt index a7a16d618..d75181445 100644 --- a/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt @@ -30,11 +30,10 @@ class LoginUseCase @Inject constructor( suspend fun setAnonymousUser() { withContext(ioCoroutineScope.coroutineContext) { - appLoungeDataStore.saveUserType(User.GOOGLE) + appLoungeDataStore.saveUserType(User.ANONYMOUS) } } - suspend fun setNoGoogleMode() { withContext(ioCoroutineScope.coroutineContext) { appLoungePreference.run { @@ -47,24 +46,20 @@ class LoginUseCase @Inject constructor( } suspend fun fetchAuthObjects(forceRefresh: Boolean): List { - val userType = appLoungeDataStore.getUserType() - - if (!forceRefresh) { - // use saved state. - val savedAuth = appLoungeDataStore.getAuthData() - if (savedAuth != AuthData("", "")) { - return buildAuthObjectList(Result.success(savedAuth), userType = userType) + return withContext(ioCoroutineScope.coroutineContext) { + val userType = appLoungeDataStore.getUser() + + if (!forceRefresh) { + // use saved state. + val savedAuth = appLoungeDataStore.getAuthData() + if (savedAuth != AuthData("", "")) { + return@withContext buildAuthObjectList(Result.success(savedAuth), userType = userType) + } } - } - val result = runCatching { playStoreRepository.updateToken() } + val result = runCatching { playStoreRepository.updateToken() } - return if (result.isSuccess) { - buildAuthObjectList(result, User.GOOGLE) - } else if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { - listOf(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) - } else { - emptyList() + buildAuthObjectList(result, userType) } } @@ -72,22 +67,30 @@ class LoginUseCase @Inject constructor( val authObjectsLocal = ArrayList() // Google Login case: - val result: ResultSupreme - if (gPlayloginResult.isSuccess) { - result = ResultSupreme.create( - status = ResultStatus.OK, - data = gPlayloginResult.getOrNull() - ) - result.otherPayload = gPlayloginResult.getOrNull()?.email - } else { - result = ResultSupreme.create(status = ResultStatus.UNKNOWN, exception = gPlayloginResult.exceptionOrNull() as? Exception) - } - - authObjectsLocal.add(AuthObject.GPlayAuth(result, userType)) + if (userType == User.GOOGLE || userType == User.ANONYMOUS) { + val result: ResultSupreme + if (gPlayloginResult.isSuccess) { + result = ResultSupreme.create( + status = ResultStatus.OK, + data = gPlayloginResult.getOrNull() + ) + result.otherPayload = gPlayloginResult.getOrNull()?.email + } else { + result = ResultSupreme.create( + status = ResultStatus.UNKNOWN, + exception = gPlayloginResult.exceptionOrNull() as? Exception + ) + } + authObjectsLocal.add(AuthObject.GPlayAuth(result, userType)) + } // Clean apk - if (userType != User.UNAVAILABLE && (appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected())) { + if ( + userType != User.UNAVAILABLE && ( + appLoungePreference.isOpenSourceSelected() || appLoungePreference.isPWASelected() + ) + ) { authObjectsLocal.add(AuthObject.CleanApk(ResultSupreme.Success(Unit), userType,)) } @@ -104,4 +107,4 @@ class LoginUseCase @Inject constructor( enablePlayStore() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt index d7f9b5de2..0015741c8 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import timber.log.Timber +@Suppress("LongParameterList") @HiltWorker class UpdatesWorker @AssistedInject constructor( @Assisted private val context: Context, @@ -114,8 +115,6 @@ class UpdatesWorker @AssistedInject constructor( } val resultStatus: ResultStatus - - if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { /* * Signifies valid Google user and valid auth data to update diff --git a/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt b/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt index b523b4330..37f413ca1 100644 --- a/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt +++ b/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt @@ -51,7 +51,7 @@ class DumpAuthData : BroadcastReceiver() { private fun getAuthDataDump(context: Context): String { val json = Json // TODO: replace with context.configuration - val authData = AppLoungeDataStore(context, json).authData.getSync().let { + val authData = AppLoungeDataStore(context, json).rawAuthData.getSync().let { json.decodeFromString(it) } val filteredData = JSONObject().apply { diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/LocaleChangedBroadcastReceiver.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/LocaleChangedBroadcastReceiver.kt index e8fd90ba5..385d61d53 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/LocaleChangedBroadcastReceiver.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/LocaleChangedBroadcastReceiver.kt @@ -54,7 +54,7 @@ class LocaleChangedBroadcastReceiver : BroadcastReceiver() { } coroutineScope.launch { try { - val authDataJson = appLoungeDataStore.authData.getSync() + val authDataJson = appLoungeDataStore.rawAuthData.getSync() val authData = json.decodeFromString(authDataJson) authData.locale = context.resources.configuration.locales[0] appLoungeDataStore.saveAuthData(authData) diff --git a/app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt b/app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt new file mode 100644 index 000000000..c57a360ab --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2025 MURENA SAS + * 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.playstore + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.os.LocaleList +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.application.ApplicationDataManager +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.preference.AppLoungeDataStore +import foundation.e.apps.data.playstore.utils.GPlayHttpClient +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.Before +import org.junit.Test +import java.util.Locale +import java.util.Properties +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class PlayStoreRepositoryTest { + + private val context: Context = mockk() + private val resources: Resources = mockk() + private val configuration: Configuration = mockk() + private val locales: LocaleList = mockk() + + private val gPlayHttpClient: GPlayHttpClient = mockk(relaxed = true) + private val applicationDataManager: ApplicationDataManager = mockk(relaxed = true) + private val playStoreSearchHelper: PlayStoreSearchHelper = mockk(relaxed = true) + private val appLoungeDataStore: AppLoungeDataStore = mockk(relaxed = true) + private val tokenDispenserDataSource: TokenDispenserDataSource = mockk(relaxed = true) + private val googleLoginDataSource: GoogleLoginDataSource = mockk(relaxed = true) + private val nativeDeviceProperty = Properties().apply { + put("os", "android") + } + private val json = Json { encodeDefaults = true } + + private lateinit var repository: PlayStoreRepository + private val expectedLocale = Locale.FRANCE + + @Before + fun setup() { + every { context.resources } returns resources + every { resources.configuration } returns configuration + every { configuration.locales } returns locales + every { locales[0] } returns expectedLocale + + repository = PlayStoreRepository( + context, + gPlayHttpClient, + applicationDataManager, + playStoreSearchHelper, + appLoungeDataStore, + tokenDispenserDataSource, + googleLoginDataSource, + nativeDeviceProperty, + json + ) + } + + @Test + fun `updateToken fetches anonymous token`() = runTest { + val expectedAuthData = AuthData("anon@example.com", "anonymous-token") + every { appLoungeDataStore.getUser() } returns User.ANONYMOUS + coEvery { tokenDispenserDataSource.fetchGToken(nativeDeviceProperty) } returns expectedAuthData + + val result = repository.updateToken() + + assertEquals(expectedAuthData.email, result.email) + assertEquals(expectedAuthData.authToken, result.authToken) + + coVerify(exactly = 1) { tokenDispenserDataSource.fetchGToken(nativeDeviceProperty) } + coVerify(exactly = 1) { appLoungeDataStore.saveAuthData(result) } + } + + @Test + fun `updateToken set locale and format and save authdata`() = runTest { + val expectedAuthData = AuthData("anon@example.com", "anonymous-token") + every { appLoungeDataStore.getUser() } returns User.ANONYMOUS + coEvery { tokenDispenserDataSource.fetchGToken(nativeDeviceProperty) } returns expectedAuthData + + val result = repository.updateToken() + + assertEquals(expectedAuthData.email, result.email) + assertEquals(expectedAuthData.authToken, result.authToken) + assertEquals(expectedLocale, result.locale) + + coVerify(exactly = 1) { appLoungeDataStore.saveAuthData(result) } + } + + @Test + fun `updateToken fetches google login token when aas token present`() = runTest { + val email = "user@google.com" + val oauthToken = "oauth-token" + val aasToken = "aas-token" + val expectedAuthData = AuthData(email, "google-token") + every { appLoungeDataStore.getUser() } returns User.GOOGLE + every { appLoungeDataStore.emailData } returns flowOf(email) + every { appLoungeDataStore.aasToken } returns flowOf(aasToken) + every { appLoungeDataStore.oauthToken } returns flowOf(oauthToken) + coEvery { googleLoginDataSource.googleLogin(email, aasToken, nativeDeviceProperty) } returns expectedAuthData + + val result = repository.updateToken() + + assertEquals(email, result.email) + assertEquals(expectedLocale, result.locale) + + coVerify(exactly = 1) { googleLoginDataSource.googleLogin(email, aasToken, nativeDeviceProperty) } + coVerify(exactly = 1) { appLoungeDataStore.saveAuthData(result) } + + coVerify(exactly = 0) { tokenDispenserDataSource.fetchGToken(any()) } + } + + @Test + fun `updateToken aas token when missing`() = runTest { + val email = "user@google.com" + val oauthToken = "oauth-token" + val newAasToken = "new-aas-token" + val expectedAuthData = AuthData(email, "google-token") + every { appLoungeDataStore.getUser() } returns User.GOOGLE + every { appLoungeDataStore.emailData } returns flowOf(email) + every { appLoungeDataStore.aasToken } returns flowOf("") + every { appLoungeDataStore.oauthToken } returns flowOf(oauthToken) + coEvery { googleLoginDataSource.getAasToken(email, oauthToken) } returns newAasToken + coEvery { appLoungeDataStore.saveAasToken(newAasToken) } returns Unit + coEvery { googleLoginDataSource.googleLogin(email, newAasToken, nativeDeviceProperty) } returns expectedAuthData + + repository.updateToken() + + coVerify(exactly = 1) { googleLoginDataSource.getAasToken(email, oauthToken) } + coVerify(exactly = 1) { appLoungeDataStore.saveAasToken(newAasToken) } + coVerify(exactly = 1) { googleLoginDataSource.googleLogin(email, newAasToken, nativeDeviceProperty) } + } +} diff --git a/app/src/test/java/foundation/e/apps/domain/usecases/LoginUseCaseTest.kt b/app/src/test/java/foundation/e/apps/domain/usecases/LoginUseCaseTest.kt new file mode 100644 index 000000000..c19db5b08 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/domain/usecases/LoginUseCaseTest.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2025 MURENA SAS + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.usecases + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.data.preference.AppLoungeDataStore +import foundation.e.apps.data.preference.AppLoungePreference +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class LoginUseCaseTest { + + private val playStoreRepository: PlayStoreRepository = mockk(relaxed = true) + private val appLoungeDataStore: AppLoungeDataStore = mockk(relaxed = true) + private val appLoungePreference: AppLoungePreference = mockk(relaxed = true) + + @Before + fun setup() { + clearMocks(playStoreRepository, appLoungeDataStore, appLoungePreference) + } + + private fun createLoginUseCase(scope: CoroutineScope) = + LoginUseCase( + playStoreRepository, + appLoungeDataStore, + appLoungePreference, + scope, + ) + + @Test + fun `setGoogleUser persists credentials and user type`() = runTest { + val loginUseCase = createLoginUseCase(this) + + loginUseCase.setGoogleUser("foo@example.com", "oauth-token") + + coVerify(exactly = 1) { appLoungeDataStore.saveGoogleLogin("foo@example.com", "oauth-token") } + coVerify(exactly = 1) { appLoungeDataStore.saveUserType(User.GOOGLE) } + } + + @Test + fun `setAnonymousUser stores google configuration`() = runTest { + val loginUseCase = createLoginUseCase(this) + + loginUseCase.setAnonymousUser() + + coVerify(exactly = 1) { appLoungeDataStore.saveUserType(User.ANONYMOUS) } + } + + @Test + fun `setNoGoogleMode toggles preferences and saves no google user`() = runTest { + val loginUseCase = createLoginUseCase(this) + + loginUseCase.setNoGoogleMode() + + verify(exactly = 1) { + appLoungePreference.disablePlayStore() + appLoungePreference.enableOpenSource() + appLoungePreference.enablePwa() + } + coVerify(exactly = 1) { appLoungeDataStore.saveUserType(User.NO_GOOGLE) } + } + + @Test + fun `fetchAuthObjects reuses saved auth when available`() = runTest { + val loginUseCase = createLoginUseCase(this) + val savedAuth = AuthData("user@example.com", "saved-token") + + every { appLoungeDataStore.getUser() } returns User.GOOGLE + coEvery { appLoungeDataStore.getAuthData() } returns savedAuth + every { appLoungePreference.isOpenSourceSelected() } returns true + every { appLoungePreference.isPWASelected() } returns false + + val authObjects = loginUseCase.fetchAuthObjects(forceRefresh = false) + + assertEquals(2, authObjects.size) + assertTrue(authObjects.any { it is AuthObject.GPlayAuth }) + assertTrue(authObjects.any { it is AuthObject.CleanApk }) + coVerify(exactly = 0) { playStoreRepository.updateToken() } + } + + @Test + fun `fetchAuthObjects refreshes token when forced`() = runTest { + val loginUseCase = createLoginUseCase(this) + + val refreshedAuth = AuthData("user@example.com", "fresh-token") + + every { appLoungeDataStore.getUser() } returns User.GOOGLE + every { appLoungeDataStore.getAuthData() } returns AuthData("", "") + every { appLoungePreference.isOpenSourceSelected() } returns true + every { appLoungePreference.isPWASelected() } returns true + coEvery { playStoreRepository.updateToken() } returns refreshedAuth + + val authObjects = loginUseCase.fetchAuthObjects(forceRefresh = true) + + assertEquals(2, authObjects.size) + assertTrue(authObjects.any { it is AuthObject.GPlayAuth }) + assertTrue(authObjects.any { it is AuthObject.CleanApk }) + coVerify(exactly = 1) { playStoreRepository.updateToken() } + } + + @Test + fun `fetchAuthObjects return gplay errors when refresh fails`() = runTest { + val loginUseCase = createLoginUseCase(this) + + every { appLoungeDataStore.getUser() } returns User.GOOGLE + every { appLoungePreference.isOpenSourceSelected() } returns false + every { appLoungePreference.isPWASelected() } returns true + coEvery { playStoreRepository.updateToken() } throws IllegalStateException("boom") + + val authObjects = loginUseCase.fetchAuthObjects(forceRefresh = true) + + assertEquals(2, authObjects.size) + assertTrue(authObjects.any { it is AuthObject.GPlayAuth && it.result?.getResultStatus() == ResultStatus.UNKNOWN }) + assertTrue(authObjects.any { it is AuthObject.CleanApk }) + + } + + @Test + fun `fetchAuthObjects doesnt returns cleanapk when PWA and OpenSource disabled`() = runTest { + val loginUseCase = createLoginUseCase(this) + + val savedAuth = AuthData("user@example.com", "saved-token") + + every { appLoungeDataStore.getUser() } returns User.ANONYMOUS + coEvery { appLoungeDataStore.getAuthData() } returns savedAuth + every { appLoungePreference.isOpenSourceSelected() } returns false + every { appLoungePreference.isPWASelected() } returns false + + val authObjects = loginUseCase.fetchAuthObjects(forceRefresh = true) + assertEquals(1, authObjects.size) + assertTrue(authObjects.none { it is AuthObject.CleanApk }) + } + + + @Test + fun `fetchAuthObjects doesnt returns gplay when disabled`() = runTest { + val loginUseCase = createLoginUseCase(this) + + + every { appLoungeDataStore.getUser() } returns User.NO_GOOGLE + every { appLoungePreference.isOpenSourceSelected() } returns true + every { appLoungePreference.isPWASelected() } returns false + + val authObjects = loginUseCase.fetchAuthObjects(forceRefresh = true) + assertEquals(1, authObjects.size) + assertTrue(authObjects.first() is AuthObject.CleanApk ) + } + + @Test + fun `logout clears credentials and restores preferences`() = runTest { + val loginUseCase = createLoginUseCase(this) + loginUseCase.logout() + + coVerify(exactly = 1) { appLoungeDataStore.destroyCredentials() } + coVerify(exactly = 1) { appLoungeDataStore.saveUserType(null) } + verify(exactly = 1) { + appLoungePreference.enableOpenSource() + appLoungePreference.enablePwa() + appLoungePreference.enablePlayStore() + } + } +} diff --git a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt index c3f13a29c..aa4383c4e 100644 --- a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt +++ b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt @@ -24,7 +24,7 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.Stores import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.domain.usecases.LoginUseCase import foundation.e.apps.ui.LoginViewModel import okhttp3.Cache import org.junit.Before @@ -36,7 +36,7 @@ import org.mockito.MockitoAnnotations class LoginViewModelTest { @Mock - private lateinit var authenticatorRepository: AuthenticatorRepository + private lateinit var loginUseCase: LoginUseCase @Mock private lateinit var cache: Cache @Mock @@ -51,7 +51,7 @@ class LoginViewModelTest { @Before fun setup() { MockitoAnnotations.openMocks(this) - loginViewModel = LoginViewModel(authenticatorRepository, cache, stores) + loginViewModel = LoginViewModel(loginUseCase, cache, stores) } @Test -- GitLab