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 908fe70b5fde7e63ef2f2e33bd6ba495b7c0b109..0000000000000000000000000000000000000000 --- 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 deleted file mode 100644 index 6c98a3801d495ee7cefa86099333c3cd2ab8231f..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/login/AuthenticatorRepository.kt +++ /dev/null @@ -1,95 +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 loginCommon: LoginCommon, - private val authenticators: List, - 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) - } - - 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 8dac6ac1aa04b04be7c7cb0e75d810efb16f8e71..0000000000000000000000000000000000000000 --- 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/LoginCommon.kt b/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt deleted file mode 100644 index f2ef1fe12ab151638e74fa856d4d185d217492b1..0000000000000000000000000000000000000000 --- 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 8de91e2994c6c9d20088d55294c9948b2135d436..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt +++ /dev/null @@ -1,227 +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, - private val appLoungePreference: AppLoungePreference, -) : StoreAuthenticator, AuthDataValidator { - - @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] - - 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. - * 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) - } - - private suspend fun getAuthDataAnonymously(): ResultSupreme { - return loginWrapper.login(locale).run { - if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) - else this - } - } - - private 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 - } - } - - 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/StoreAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/StoreAuthenticator.kt deleted file mode 100644 index bc0d18106951b98abf0728abc28e39ba51842764..0000000000000000000000000000000000000000 --- 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 deleted file mode 100644 index 08f81649e7dc16eefcadc048e1c6182e87d78bbd..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginManager.kt +++ /dev/null @@ -1,94 +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.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 -import kotlinx.serialization.json.Json -import java.util.Locale -import java.util.Properties - -class AnonymousLoginManager( - private val gPlayHttpClient: GPlayHttpClient, - private val nativeDeviceProperty: Properties, - private val json: Json, -) : PlayStoreLoginManager { - - private val tokenUrl: String = "https://eu.gtoken.ecloud.global" - - /** - * Log anonymously a user - * - * @return authData: authentication data - */ - override suspend fun login(): AuthData? { - var authData: AuthData? = null - 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 "" - } - ) - } else { - val auth = json.decodeFromString(String(response.responseBytes)) - authData = AuthHelper.build( - email = auth.email, - token = auth.auth, - tokenType = AuthHelper.Token.AUTH, - isAnonymous = true, - properties = nativeDeviceProperty, - locale = Locale.getDefault() - ) - } - } - 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 deleted file mode 100644 index 757ea13a49c3d62df84c5db17d8543e211880c3e..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt +++ /dev/null @@ -1,89 +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.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 -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, -) : 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 - } - - /** - * 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 deleted file mode 100644 index 12fad6b6d7ea8b93204fcabb77d82544dcbf26ad..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManager.kt +++ /dev/null @@ -1,26 +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? - suspend fun validate(authData: AuthData): PlayResponse -} 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 121b9d626204d838899ad61b85e9962b86006ca8..0000000000000000000000000000000000000000 --- 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(gPlayHttpClient, 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 bf8820e024f1cefa62fa67208341426bb8704e6e..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt +++ /dev/null @@ -1,138 +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 - } - } - } - } - - /** - * 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]. - * - * 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 50% 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 f1e3eb3eb785c80a96759fdc4dc492a0460509b0..8f21743a2eb6715416ba0af889f3ba2ad97f6ae7 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,18 @@ -/* - * 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.login.exceptions.GPlayException +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 +21,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 +47,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 GPlayException(false, error) + } + return aasToken + } + + suspend fun googleLogin(email: String, aasToken: String, nativeDeviceProperty: Properties): AuthData { + return AuthHelper.build(email, aasToken, tokenType = AuthHelper.Token.AAS, properties = nativeDeviceProperty) } } 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 2775e3291203f663162d819acd2312629946842f..92093ad285622c67cd3e9f0039b4499214e3a0f0 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,10 +19,13 @@ 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.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 @@ -42,27 +45,78 @@ 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.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.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 -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LongParameterList") class PlayStoreRepository @Inject constructor( @ApplicationContext private val context: Context, private val gPlayHttpClient: GPlayHttpClient, - private val authenticatorRepository: AuthenticatorRepository, private val applicationDataManager: ApplicationDataManager, - private val playStoreSearchHelper: PlayStoreSearchHelper + private val playStoreSearchHelper: PlayStoreSearchHelper, + 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.getUser() + + val rawAuthToken = when (userType) { + User.GOOGLE -> fetchGoogleLoginToken() + User.ANONYMOUS -> tokenDispenserDataSource.fetchGToken(nativeDeviceProperty) + 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] + + 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()) + 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>() @@ -199,16 +253,17 @@ 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() { Timber.i("Refreshing authentication.") - authenticatorRepository.fetchAuthObjects(listOf(PlayStoreAuthenticator::class.java.simpleName)) + updateToken() } suspend fun getAppDetailsWeb(packageName: String): Application? { @@ -270,15 +325,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, @@ -286,39 +344,64 @@ 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) + } + } + } + + 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. + 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/data/playstore/TokenDispenserDataSource.kt b/app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d127fde62e26918d21b2238d1f5479816ebc967 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/playstore/TokenDispenserDataSource.kt @@ -0,0 +1,60 @@ +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 +import java.util.Locale +import java.util.Properties +import javax.inject.Inject +import javax.inject.Singleton + +// 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 json: Json, +) { + + private val tokenUrl: String = "https://eu.gtoken.ecloud.global" + + /** + * Log anonymously a user + * + * @return authData: authentication data + */ + suspend fun fetchGToken(nativeDeviceProperty: Properties): AuthData { + return withContext(Dispatchers.IO) { + val response = gPlayHttpClient.postAuth( + tokenUrl, + json.encodeToString(nativeDeviceProperty).toByteArray() + ) + 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 "" + } + ) + } else { + val auth = json.decodeFromString(String(response.responseBytes)) + AuthHelper.build( + email = auth.email, + token = auth.auth, + tokenType = AuthHelper.Token.AUTH, + isAnonymous = true, + properties = nativeDeviceProperty, + locale = Locale.getDefault() + ) + } + } + } +} 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 e029c196e32685470aeef02d021a98d7456fd1cd..0000000000000000000000000000000000000000 --- 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()) - } -} 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 235e9ab52a58b156ea88c2e8f6b1d7ca1f6c0066..0f504158a77e95e8ccf6032677d5bff247e012a6 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 871f9fae3a242c5ac9ff4d562c23ffed0f2d6320..8d8e9606f56f893272467ed1483eb4a421ef57a2 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/di/LoginModule.kt b/app/src/main/java/foundation/e/apps/di/LoginModule.kt deleted file mode 100644 index ac964187dbbfebeb94c51bb6e2773ebff850829c..0000000000000000000000000000000000000000 --- 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/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index ab94f3cd25315a1346bd6afd35fb3481309759ad..3b7e1dd112c922b05595003843f04893e82be3a7 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 25f6ee48480d9d413fbd2fc5e7315dd2d3b5dac5..a7b66026602f6e0587984225e0c0629259da7ddc 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/usecases/LoginUseCase.kt b/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..d7518144572ced335bfb49446c7bf8ea162cf79e --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/usecases/LoginUseCase.kt @@ -0,0 +1,110 @@ +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.playstore.PlayStoreRepository +import foundation.e.apps.data.preference.AppLoungeDataStore +import foundation.e.apps.data.preference.AppLoungePreference +import foundation.e.apps.di.qualifiers.IoCoroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LoginUseCase @Inject constructor( + private val playStoreRepository: PlayStoreRepository, + 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.ANONYMOUS) + } + } + + suspend fun setNoGoogleMode() { + withContext(ioCoroutineScope.coroutineContext) { + appLoungePreference.run { + disablePlayStore() + enableOpenSource() + enablePwa() + } + appLoungeDataStore.saveUserType(User.NO_GOOGLE) + } + } + + suspend fun fetchAuthObjects(forceRefresh: Boolean): List { + 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() } + + buildAuthObjectList(result, userType) + } + } + + private fun buildAuthObjectList(gPlayloginResult: Result, userType: User): List { + val authObjectsLocal = ArrayList() + + // Google Login case: + 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() + ) + ) { + 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() + } + } +} 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 f2907200fad013b69d98450d14be62fa2262d5ab..0015741c813c4f6b8019bfc11872d40348b4b789 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,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.login.AuthenticatorRepository +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 @@ -32,16 +32,17 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import timber.log.Timber +@Suppress("LongParameterList") @HiltWorker class UpdatesWorker @AssistedInject constructor( @Assisted private val context: Context, @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, + private val playStoreRepository: PlayStoreRepository, ) : CoroutineWorker(context, params) { companion object { @@ -109,7 +110,9 @@ class UpdatesWorker @AssistedInject constructor( val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val appsNeededToUpdate = mutableListOf() val user = getUser() - val authData = authenticatorRepository.getValidatedAuthData().data + val authData = appLoungeDataStore.getAuthData().takeIf { + runCatching { playStoreRepository.sendCommonErrorsProbe() }.isSuccess + } val resultStatus: ResultStatus if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { 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 2831499ed01872614a8a2575a72fb0e2b647e437..07e66676ffefdfaf6658cd05b72b65c77a7e3ecf 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 e6785d4a221c030752a2af5a547bba313aa2d42b..cbad8dce2190c8f472071e7ebed09b7a5e5ebd63 100644 --- a/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt +++ b/app/src/main/java/foundation/e/apps/provider/AgeRatingProvider.kt @@ -46,15 +46,13 @@ 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 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 @@ -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 @@ -213,10 +209,11 @@ 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()) { - authenticatorRepository.setGPlayAuth(authData) + appLoungeDataStore.saveAuthData(authData) } } @@ -301,9 +298,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 } @@ -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/receivers/DumpAuthData.kt b/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt index b523b4330aa1af5f0a16f3b81851041fa1d0fff8..37f413ca10cf56014c33f42fc7a6d303c59c7eaf 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/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt index 211e45bdfe3813c8dd448692c1ea669a5b467feb..dc46059606e1a3bdd2ae6a4fd68cc693d24ddc3d 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,8 @@ 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.usecases.LoginUseCase import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.launch import okhttp3.Cache @@ -37,7 +36,7 @@ import javax.inject.Inject */ @HiltViewModel class LoginViewModel @Inject constructor( - private val authenticatorRepository: AuthenticatorRepository, + private val loginUseCase: LoginUseCase, private val cache: Cache, private val stores: Stores ) : ViewModel() { @@ -57,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 = authenticatorRepository.fetchAuthObjects(clearList) + val authObjectsLocal = loginUseCase.fetchAuthObjects(forceRefresh) authObjects.postValue(authObjectsLocal) } } @@ -72,7 +71,7 @@ class LoginViewModel @Inject constructor( fun initialAnonymousLogin(onUserSaved: () -> Unit) { viewModelScope.launch { stores.enableStore(Source.PLAY_STORE) - authenticatorRepository.saveUserType(User.ANONYMOUS) + loginUseCase.setAnonymousUser() onUserSaved() startLoginFlow() } @@ -87,8 +86,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) + loginUseCase.setGoogleUser(email, oauthToken) onUserSaved() startLoginFlow() } @@ -104,7 +102,7 @@ class LoginViewModel @Inject constructor( fun initialNoGoogleLogin(onUserSaved: () -> Unit) { viewModelScope.launch { stores.disableStore(Source.PLAY_STORE) - authenticatorRepository.setNoGoogleMode() + loginUseCase.setNoGoogleMode() onUserSaved() startLoginFlow() } @@ -139,7 +137,7 @@ class LoginViewModel @Inject constructor( fun logout() { viewModelScope.launch { cache.evictAll() - authenticatorRepository.logout() + loginUseCase.logout() authObjects.postValue(listOf()) } } 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 74e3468891c429e83f14a740fba440b594176a44..f93417665cf4724eef2742da0c10accaa37e61d6 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 42c17f9762c2558501dcf7bd4fdc1dd813924dc0..841815c1d41bb5ecb2a4ff81913821f4f336da56 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) } /** 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 e8fd90ba5ad5d92f96afc11483a3faf182a3674e..385d61d535a50b3fb2171a9f8c9d7062e301e40e 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 0000000000000000000000000000000000000000..c57a360ab760bdf7d30cd2851450409725fbc9ac --- /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 0000000000000000000000000000000000000000..c19db5b08dc055c883a6c134c55e2f34ab0dcfef --- /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/installProcessor/AppInstallProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt index a11efa092d2ec44beca293c126d1421b503004ee..919530224e091c2e169dfbb5e6430ca315c6b375 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 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 c3f13a29c5e057cde103f78271125dc64f4471d7..aa4383c4e6bc4ce6f90fc16a56c3109da1b02598 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