Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d3c61d39 authored by Jonathan Klee's avatar Jonathan Klee
Browse files

chore: simplify architecture

parent b528724d
Loading
Loading
Loading
Loading
+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?
}
+48 −4
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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(
@@ -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()
@@ -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"
    }
}
+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."
    }
}
+1 −21
Original line number Diff line number Diff line
@@ -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
@@ -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"
@@ -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
    }
}
+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