Loading app/src/main/java/foundation/e/apps/data/login/api/LoginManager.kt +4 −2 Original line number Diff line number Diff line package foundation.e.apps.data.login.api interface LoginManager<T> { suspend fun login(): T import com.aurora.gplayapi.data.models.AuthData interface LoginManager { suspend fun login(): AuthData? } app/src/main/java/foundation/e/apps/data/login/microg/MicrogLoginManager.kt +48 −4 Original line number Diff line number Diff line Loading @@ -27,8 +27,12 @@ import android.os.Bundle import android.os.Parcelable import android.util.Base64 import androidx.core.os.BundleCompat import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.login.api.LoginManager import foundation.e.apps.data.login.playstore.OauthAuthDataBuilder import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject Loading @@ -42,8 +46,10 @@ data class MicrogAccount( @Singleton class MicrogLoginManager @Inject constructor( @ApplicationContext val context: Context, private val accountManager: AccountManager ) : LoginManager<MicrogLoginManager.FetchResult> { private val accountManager: AccountManager, private val oauthAuthDataBuilder: OauthAuthDataBuilder, private val appLoungeDataStore: AppLoungeDataStore ) : LoginManager { sealed interface FetchResult { data class Success(val microgAccount: MicrogAccount) : FetchResult data class RequiresUserAction(val intent: Intent) : FetchResult Loading @@ -54,8 +60,21 @@ class MicrogLoginManager @Inject constructor( return accountManager.getAccountsByType(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE).isNotEmpty() } override suspend fun login(): FetchResult { return fetchMicrogAccount() override suspend fun login(): AuthData? { val oldToken = appLoungeDataStore.oauthToken.getSync() val shouldRefresh = hasMicrogAccount() && oldToken.startsWith(MICROG_TOKEN_PREFIX) val oauthToken = if (shouldRefresh) { fetchRefreshedToken(oldToken) } else { oldToken } if (oauthToken.isBlank()) { error(MICROG_TOKEN_MISSING) } return oauthAuthDataBuilder.build(oauthToken) ?: error(MICROG_TOKEN_MISSING) } suspend fun fetchMicrogAccount( Loading Loading @@ -113,6 +132,25 @@ class MicrogLoginManager @Inject constructor( } } private suspend fun fetchRefreshedToken(oldToken: String): String { val accountName = appLoungeDataStore.emailData.getSync().ifBlank { "" } invalidateAuthToken(accountName, oldToken) val result = fetchMicrogAccount(accountName) return when (result) { is FetchResult.Success -> { appLoungeDataStore.saveGoogleLogin( result.microgAccount.account.name, result.microgAccount.oauthToken ) appLoungeDataStore.saveAasToken("") result.microgAccount.oauthToken } is FetchResult.RequiresUserAction -> error(MICROG_TOKEN_REFRESH_FAILURE) is FetchResult.Error -> error(MICROG_TOKEN_REFRESH_FAILURE) } } private fun resolveAccount(accounts: Array<Account>, accountName: String): Account { return if (accountName.isNotBlank()) { accounts.firstOrNull { it.name == accountName } ?: accounts.first() Loading @@ -138,4 +176,10 @@ class MicrogLoginManager @Inject constructor( BundleCompat.getParcelable(this, key, clazz) } } companion object { const val MICROG_TOKEN_PREFIX = "ya29." const val MICROG_TOKEN_MISSING = "MicroG token is missing" const val MICROG_TOKEN_REFRESH_FAILURE = "MicroG refresh failed" } } app/src/main/java/foundation/e/apps/data/login/microg/MicrogTokenRefresher.ktdeleted 100644 → 0 +0 −71 Original line number Diff line number Diff line package foundation.e.apps.data.login.microg import foundation.e.apps.data.enums.User import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync import javax.inject.Inject import javax.inject.Singleton @Singleton class MicrogTokenRefresher @Inject constructor( private val appLoungeDataStore: AppLoungeDataStore, private val microgLoginManager: MicrogLoginManager ) { suspend fun refreshIfNeeded(user: User): Boolean { val hasMicrogGoogleAccount = hasMicrogGoogleAccount(user) val oldToken = readOldToken(hasMicrogGoogleAccount) val shouldRefresh = shouldRefreshToken(hasMicrogGoogleAccount, oldToken) if (!shouldRefresh) { return true } val accountName = appLoungeDataStore.emailData.getSync().ifBlank { "" } return refreshToken(accountName, oldToken) } private suspend fun refreshToken(accountName: String, oldToken: String): Boolean { microgLoginManager.invalidateAuthToken(accountName, oldToken) val result = microgLoginManager.fetchMicrogAccount(accountName) return handleRefreshResult(result) } private suspend fun handleRefreshResult(result: MicrogLoginManager.FetchResult): Boolean { return when (result) { is MicrogLoginManager.FetchResult.Success -> { updateStoredTokens(result.microgAccount) true } is MicrogLoginManager.FetchResult.RequiresUserAction -> false is MicrogLoginManager.FetchResult.Error -> false } } private suspend fun updateStoredTokens(microgAccount: MicrogAccount) { appLoungeDataStore.saveGoogleLogin( microgAccount.account.name, microgAccount.oauthToken ) appLoungeDataStore.saveAasToken("") } private fun hasMicrogGoogleAccount(user: User): Boolean { return user == User.GOOGLE && microgLoginManager.hasMicrogAccount() } private fun readOldToken(hasMicrogGoogleAccount: Boolean): String { return if (hasMicrogGoogleAccount) { appLoungeDataStore.oauthToken.getSync() } else { "" } } private fun shouldRefreshToken(hasMicrogGoogleAccount: Boolean, oldToken: String): Boolean { return hasMicrogGoogleAccount && oldToken.startsWith(MICROG_TOKEN_PREFIX) } private companion object { private const val MICROG_TOKEN_PREFIX = "ya29." } } app/src/main/java/foundation/e/apps/data/login/playstore/AnonymousLoginManager.kt +1 −21 Original line number Diff line number Diff line Loading @@ -19,16 +19,13 @@ package foundation.e.apps.data.login.playstore 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.api.LoginManager import foundation.e.apps.data.login.core.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 timber.log.Timber import java.net.HttpURLConnection import java.util.Locale import java.util.Properties Loading @@ -38,7 +35,7 @@ class AnonymousLoginManager @Inject constructor( private val gPlayHttpClient: GPlayHttpClient, private val nativeDeviceProperty: Properties, private val json: Json, ) : LoginManager<AuthData?> { ) : LoginManager { companion object { private const val TOKEN_DISPENSER_URL = "https://eu.gtoken.ecloud.global" Loading Loading @@ -77,21 +74,4 @@ class AnonymousLoginManager @Inject constructor( } return authData } suspend fun validate(authData: AuthData?): PlayResponse { if (authData == null) { return PlayResponse() } var result = PlayResponse() withContext(Dispatchers.IO) { try { val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) result = authValidator.getValidityResponse() } catch (e: Exception) { Timber.e(e, "AuthData validation failed for anonymous login") throw e } } return result } } app/src/main/java/foundation/e/apps/data/login/playstore/AuthDataProviders.ktdeleted 100644 → 0 +0 −116 Original line number Diff line number Diff line /* * Copyright (C) 2026 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 <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.login.playstore import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync interface AuthDataProvider { suspend fun fetch(): ResultSupreme<AuthData?> } class AnonymousAuthDataProvider( private val loginProvider: suspend () -> ResultSupreme<AuthData?>, private val formatter: (AuthData) -> AuthData ) : AuthDataProvider { override suspend fun fetch(): ResultSupreme<AuthData?> { return loginProvider().run { if (isSuccess()) { ResultSupreme.Success(formatter(this.data!!)) } else { this } } } } class GoogleAasAuthDataProvider( private val appLoungeDataStore: AppLoungeDataStore, private val loginProvider: suspend () -> ResultSupreme<AuthData?> ) : AuthDataProvider { override suspend fun fetch(): ResultSupreme<AuthData?> { val aasToken = appLoungeDataStore.aasToken.getSync() if (aasToken.isNotBlank()) { return loginProvider() } return ResultSupreme.Error("AAS token is missing") } } class GoogleOauthAuthDataProvider( private val appLoungeDataStore: AppLoungeDataStore, private val loginProvider: suspend () -> ResultSupreme<AuthData?>, private val googleLoginManager: GoogleLoginManager, private val aasTokenProvider: suspend (GoogleLoginManager, String, String) -> ResultSupreme<String>, private val formatter: (AuthData) -> AuthData ) : AuthDataProvider { override suspend fun fetch(): ResultSupreme<AuthData?> { val email = appLoungeDataStore.emailData.getSync() val oauthToken = appLoungeDataStore.oauthToken.getSync() val aasTokenResponse = aasTokenProvider(googleLoginManager, email, oauthToken) return if (aasTokenResponse.isSuccess()) { fetchWithAasToken(aasTokenResponse.data.orEmpty()) } else { fetchWithOauthFallback(aasTokenResponse, oauthToken) } } private suspend fun fetchWithAasToken(aasTokenFetched: String): ResultSupreme<AuthData?> { if (aasTokenFetched.isBlank()) { return ResultSupreme.Error("Fetched AAS Token is blank") } appLoungeDataStore.saveAasToken(aasTokenFetched) return loginProvider().run { if (isSuccess()) { ResultSupreme.Success(formatter(this.data!!)) } else { this } } } private suspend fun fetchWithOauthFallback( aasTokenResponse: ResultSupreme<String>, oauthToken: String ): ResultSupreme<AuthData?> { val fallbackAuth = googleLoginManager.buildAuthDataFromOauthToken(oauthToken) return if (fallbackAuth != null) { ResultSupreme.Success(formatter(fallbackAuth)) } else { ResultSupreme.replicate(aasTokenResponse, null) } } } class GoogleAuthDataProvider( private val appLoungeDataStore: AppLoungeDataStore, private val aasProvider: AuthDataProvider, private val oauthProvider: AuthDataProvider ) : AuthDataProvider { override suspend fun fetch(): ResultSupreme<AuthData?> { return if (appLoungeDataStore.aasToken.getSync().isNotBlank()) { aasProvider.fetch() } else { oauthProvider.fetch() } } } Loading
app/src/main/java/foundation/e/apps/data/login/api/LoginManager.kt +4 −2 Original line number Diff line number Diff line package foundation.e.apps.data.login.api interface LoginManager<T> { suspend fun login(): T import com.aurora.gplayapi.data.models.AuthData interface LoginManager { suspend fun login(): AuthData? }
app/src/main/java/foundation/e/apps/data/login/microg/MicrogLoginManager.kt +48 −4 Original line number Diff line number Diff line Loading @@ -27,8 +27,12 @@ import android.os.Bundle import android.os.Parcelable import android.util.Base64 import androidx.core.os.BundleCompat import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.login.api.LoginManager import foundation.e.apps.data.login.playstore.OauthAuthDataBuilder import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject Loading @@ -42,8 +46,10 @@ data class MicrogAccount( @Singleton class MicrogLoginManager @Inject constructor( @ApplicationContext val context: Context, private val accountManager: AccountManager ) : LoginManager<MicrogLoginManager.FetchResult> { private val accountManager: AccountManager, private val oauthAuthDataBuilder: OauthAuthDataBuilder, private val appLoungeDataStore: AppLoungeDataStore ) : LoginManager { sealed interface FetchResult { data class Success(val microgAccount: MicrogAccount) : FetchResult data class RequiresUserAction(val intent: Intent) : FetchResult Loading @@ -54,8 +60,21 @@ class MicrogLoginManager @Inject constructor( return accountManager.getAccountsByType(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE).isNotEmpty() } override suspend fun login(): FetchResult { return fetchMicrogAccount() override suspend fun login(): AuthData? { val oldToken = appLoungeDataStore.oauthToken.getSync() val shouldRefresh = hasMicrogAccount() && oldToken.startsWith(MICROG_TOKEN_PREFIX) val oauthToken = if (shouldRefresh) { fetchRefreshedToken(oldToken) } else { oldToken } if (oauthToken.isBlank()) { error(MICROG_TOKEN_MISSING) } return oauthAuthDataBuilder.build(oauthToken) ?: error(MICROG_TOKEN_MISSING) } suspend fun fetchMicrogAccount( Loading Loading @@ -113,6 +132,25 @@ class MicrogLoginManager @Inject constructor( } } private suspend fun fetchRefreshedToken(oldToken: String): String { val accountName = appLoungeDataStore.emailData.getSync().ifBlank { "" } invalidateAuthToken(accountName, oldToken) val result = fetchMicrogAccount(accountName) return when (result) { is FetchResult.Success -> { appLoungeDataStore.saveGoogleLogin( result.microgAccount.account.name, result.microgAccount.oauthToken ) appLoungeDataStore.saveAasToken("") result.microgAccount.oauthToken } is FetchResult.RequiresUserAction -> error(MICROG_TOKEN_REFRESH_FAILURE) is FetchResult.Error -> error(MICROG_TOKEN_REFRESH_FAILURE) } } private fun resolveAccount(accounts: Array<Account>, accountName: String): Account { return if (accountName.isNotBlank()) { accounts.firstOrNull { it.name == accountName } ?: accounts.first() Loading @@ -138,4 +176,10 @@ class MicrogLoginManager @Inject constructor( BundleCompat.getParcelable(this, key, clazz) } } companion object { const val MICROG_TOKEN_PREFIX = "ya29." const val MICROG_TOKEN_MISSING = "MicroG token is missing" const val MICROG_TOKEN_REFRESH_FAILURE = "MicroG refresh failed" } }
app/src/main/java/foundation/e/apps/data/login/microg/MicrogTokenRefresher.ktdeleted 100644 → 0 +0 −71 Original line number Diff line number Diff line package foundation.e.apps.data.login.microg import foundation.e.apps.data.enums.User import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync import javax.inject.Inject import javax.inject.Singleton @Singleton class MicrogTokenRefresher @Inject constructor( private val appLoungeDataStore: AppLoungeDataStore, private val microgLoginManager: MicrogLoginManager ) { suspend fun refreshIfNeeded(user: User): Boolean { val hasMicrogGoogleAccount = hasMicrogGoogleAccount(user) val oldToken = readOldToken(hasMicrogGoogleAccount) val shouldRefresh = shouldRefreshToken(hasMicrogGoogleAccount, oldToken) if (!shouldRefresh) { return true } val accountName = appLoungeDataStore.emailData.getSync().ifBlank { "" } return refreshToken(accountName, oldToken) } private suspend fun refreshToken(accountName: String, oldToken: String): Boolean { microgLoginManager.invalidateAuthToken(accountName, oldToken) val result = microgLoginManager.fetchMicrogAccount(accountName) return handleRefreshResult(result) } private suspend fun handleRefreshResult(result: MicrogLoginManager.FetchResult): Boolean { return when (result) { is MicrogLoginManager.FetchResult.Success -> { updateStoredTokens(result.microgAccount) true } is MicrogLoginManager.FetchResult.RequiresUserAction -> false is MicrogLoginManager.FetchResult.Error -> false } } private suspend fun updateStoredTokens(microgAccount: MicrogAccount) { appLoungeDataStore.saveGoogleLogin( microgAccount.account.name, microgAccount.oauthToken ) appLoungeDataStore.saveAasToken("") } private fun hasMicrogGoogleAccount(user: User): Boolean { return user == User.GOOGLE && microgLoginManager.hasMicrogAccount() } private fun readOldToken(hasMicrogGoogleAccount: Boolean): String { return if (hasMicrogGoogleAccount) { appLoungeDataStore.oauthToken.getSync() } else { "" } } private fun shouldRefreshToken(hasMicrogGoogleAccount: Boolean, oldToken: String): Boolean { return hasMicrogGoogleAccount && oldToken.startsWith(MICROG_TOKEN_PREFIX) } private companion object { private const val MICROG_TOKEN_PREFIX = "ya29." } }
app/src/main/java/foundation/e/apps/data/login/playstore/AnonymousLoginManager.kt +1 −21 Original line number Diff line number Diff line Loading @@ -19,16 +19,13 @@ package foundation.e.apps.data.login.playstore 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.api.LoginManager import foundation.e.apps.data.login.core.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 timber.log.Timber import java.net.HttpURLConnection import java.util.Locale import java.util.Properties Loading @@ -38,7 +35,7 @@ class AnonymousLoginManager @Inject constructor( private val gPlayHttpClient: GPlayHttpClient, private val nativeDeviceProperty: Properties, private val json: Json, ) : LoginManager<AuthData?> { ) : LoginManager { companion object { private const val TOKEN_DISPENSER_URL = "https://eu.gtoken.ecloud.global" Loading Loading @@ -77,21 +74,4 @@ class AnonymousLoginManager @Inject constructor( } return authData } suspend fun validate(authData: AuthData?): PlayResponse { if (authData == null) { return PlayResponse() } var result = PlayResponse() withContext(Dispatchers.IO) { try { val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) result = authValidator.getValidityResponse() } catch (e: Exception) { Timber.e(e, "AuthData validation failed for anonymous login") throw e } } return result } }
app/src/main/java/foundation/e/apps/data/login/playstore/AuthDataProviders.ktdeleted 100644 → 0 +0 −116 Original line number Diff line number Diff line /* * Copyright (C) 2026 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 <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.login.playstore import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync interface AuthDataProvider { suspend fun fetch(): ResultSupreme<AuthData?> } class AnonymousAuthDataProvider( private val loginProvider: suspend () -> ResultSupreme<AuthData?>, private val formatter: (AuthData) -> AuthData ) : AuthDataProvider { override suspend fun fetch(): ResultSupreme<AuthData?> { return loginProvider().run { if (isSuccess()) { ResultSupreme.Success(formatter(this.data!!)) } else { this } } } } class GoogleAasAuthDataProvider( private val appLoungeDataStore: AppLoungeDataStore, private val loginProvider: suspend () -> ResultSupreme<AuthData?> ) : AuthDataProvider { override suspend fun fetch(): ResultSupreme<AuthData?> { val aasToken = appLoungeDataStore.aasToken.getSync() if (aasToken.isNotBlank()) { return loginProvider() } return ResultSupreme.Error("AAS token is missing") } } class GoogleOauthAuthDataProvider( private val appLoungeDataStore: AppLoungeDataStore, private val loginProvider: suspend () -> ResultSupreme<AuthData?>, private val googleLoginManager: GoogleLoginManager, private val aasTokenProvider: suspend (GoogleLoginManager, String, String) -> ResultSupreme<String>, private val formatter: (AuthData) -> AuthData ) : AuthDataProvider { override suspend fun fetch(): ResultSupreme<AuthData?> { val email = appLoungeDataStore.emailData.getSync() val oauthToken = appLoungeDataStore.oauthToken.getSync() val aasTokenResponse = aasTokenProvider(googleLoginManager, email, oauthToken) return if (aasTokenResponse.isSuccess()) { fetchWithAasToken(aasTokenResponse.data.orEmpty()) } else { fetchWithOauthFallback(aasTokenResponse, oauthToken) } } private suspend fun fetchWithAasToken(aasTokenFetched: String): ResultSupreme<AuthData?> { if (aasTokenFetched.isBlank()) { return ResultSupreme.Error("Fetched AAS Token is blank") } appLoungeDataStore.saveAasToken(aasTokenFetched) return loginProvider().run { if (isSuccess()) { ResultSupreme.Success(formatter(this.data!!)) } else { this } } } private suspend fun fetchWithOauthFallback( aasTokenResponse: ResultSupreme<String>, oauthToken: String ): ResultSupreme<AuthData?> { val fallbackAuth = googleLoginManager.buildAuthDataFromOauthToken(oauthToken) return if (fallbackAuth != null) { ResultSupreme.Success(formatter(fallbackAuth)) } else { ResultSupreme.replicate(aasTokenResponse, null) } } } class GoogleAuthDataProvider( private val appLoungeDataStore: AppLoungeDataStore, private val aasProvider: AuthDataProvider, private val oauthProvider: AuthDataProvider ) : AuthDataProvider { override suspend fun fetch(): ResultSupreme<AuthData?> { return if (appLoungeDataStore.aasToken.getSync().isNotBlank()) { aasProvider.fetch() } else { oauthProvider.fetch() } } }