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

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

Merge branch '0000-a16-microG-signin' into 'main'

feat: use already existing microG account if possible

See merge request !665
parents 8df52825 14781896
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="foundation.e.pwaplayer.provider.READ_WRITE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
+4 −0
Original line number Diff line number Diff line
@@ -72,6 +72,10 @@ class AuthenticatorRepository @Inject constructor(
        loginCommon.saveGoogleLogin(email, oauth)
    }

    suspend fun saveAasToken(aasToken: String) {
        appLoungeDataStore.saveAasToken(aasToken)
    }

    suspend fun setNoGoogleMode() {
        loginCommon.setNoGoogleMode()
    }
+12 −2
Original line number Diff line number Diff line
@@ -183,8 +183,9 @@ class PlayStoreAuthenticator @Inject constructor(
        /*
         * If aasToken is not yet saved / made, fetch it from email and oauthToken.
         */
        val googleLoginManager = loginManager as GoogleLoginManager
        val aasTokenResponse = loginWrapper.getAasToken(
            loginManager as GoogleLoginManager,
            googleLoginManager,
            email,
            oauthToken
        )
@@ -195,7 +196,16 @@ class PlayStoreAuthenticator @Inject constructor(
         * in the aasTokenResponse.
         */
        if (!aasTokenResponse.isSuccess()) {
            return ResultSupreme.replicate(aasTokenResponse, null)
            /*
             * Fallback: try building auth data directly from the oauthtoken without
             * converting to AAS.
             */
            val fallbackAuth = googleLoginManager.buildAuthDataFromOauthToken(oauthToken)
            return if (fallbackAuth != null) {
                ResultSupreme.Success(formatAuthData(fallbackAuth))
            } else {
                ResultSupreme.replicate(aasTokenResponse, null)
            }
        }

        val aasTokenFetched = aasTokenResponse.data ?: ""
+30 −2
Original line number Diff line number Diff line
@@ -62,13 +62,41 @@ class GoogleLoginManager(
    override suspend fun login(): AuthData? {
        val email = appLoungeDataStore.emailData.getSync()
        val aasToken = appLoungeDataStore.aasToken.getSync()
        val oauthToken = appLoungeDataStore.oauthToken.getSync()

        var authData: AuthData?
        withContext(Dispatchers.IO) {
            // Prefer AAS if present, otherwise fall back to an AUTH token (microG ya29.*).
            val (token, tokenType) = when {
                aasToken.isNotBlank() && aasToken.startsWith("ya29.") ->
                    aasToken to AuthHelper.Token.AUTH
                aasToken.isNotBlank() ->
                    aasToken to AuthHelper.Token.AAS
                oauthToken.isNotBlank() ->
                    oauthToken to AuthHelper.Token.AUTH
                else -> "" to AuthHelper.Token.AAS
            }

            authData = AuthHelper.build(
                email,
                aasToken,
                tokenType = AuthHelper.Token.AAS,
                token,
                tokenType = tokenType,
                isAnonymous = false,
                properties = nativeDeviceProperty
            )
        }
        return authData
    }

    suspend fun buildAuthDataFromOauthToken(oauthToken: String): AuthData? {
        val email = appLoungeDataStore.emailData.getSync()
        var authData: AuthData?
        withContext(Dispatchers.IO) {
            authData = AuthHelper.build(
                email = email,
                token = oauthToken,
                tokenType = AuthHelper.Token.AUTH,
                isAnonymous = false,
                properties = nativeDeviceProperty
            )
        }
+110 −0
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.microg

import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.util.Base64
import androidx.core.os.BundleCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton

data class MicrogAccount(
    val account: Account,
    val oauthToken: String
)

sealed class MicrogAccountFetchResult {
    data class Success(val microgAccount: MicrogAccount) : MicrogAccountFetchResult()
    data class RequiresUserAction(val intent: Intent) : MicrogAccountFetchResult()
    data class Error(val throwable: Throwable) : MicrogAccountFetchResult()
}

@Singleton
class MicrogAccountManager @Inject constructor(
    @ApplicationContext val context: Context,
    private val accountManager: AccountManager
) {

    fun hasMicrogAccount(): Boolean {
        return accountManager.getAccountsByType(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE).isNotEmpty()
    }

    suspend fun fetchMicrogAccount(
        accountName: String? = null
    ): MicrogAccountFetchResult = withContext(Dispatchers.IO) {
        val accounts = accountManager.getAccountsByType(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE)
        if (accounts.isEmpty()) {
            return@withContext MicrogAccountFetchResult.Error(
                IllegalStateException("No Google accounts available")
            )
        }

        val account = accountName?.let { name ->
            accounts.firstOrNull { it.name == name }
        } ?: accounts.first()

        return@withContext runCatching {
            val bundle = accountManager.getAuthToken(
                account,
                MicrogCertUtil.PLAY_AUTH_SCOPE,
                Bundle().apply {
                    putString("overridePackage", MicrogCertUtil.GOOGLE_PLAY_PACKAGE)
                    putByteArray(
                        "overrideCertificate",
                        Base64.decode(MicrogCertUtil.GOOGLE_PLAY_CERT_BASE64, Base64.DEFAULT)
                    )
                },
                false,
                null,
                null
            ).result

            val intent = bundle.parcelableCompat(AccountManager.KEY_INTENT, Intent::class.java)
            if (intent != null) {
                return@withContext MicrogAccountFetchResult.RequiresUserAction(intent)
            }

            val token = bundle.getString(AccountManager.KEY_AUTHTOKEN)
                ?: return@withContext MicrogAccountFetchResult.Error(
                    IllegalStateException("microG returned an empty token")
                )

            MicrogAccountFetchResult.Success(MicrogAccount(account, token))
        }.getOrElse { throwable ->
            MicrogAccountFetchResult.Error(throwable)
        }
    }

    private fun <T : Parcelable> Bundle.parcelableCompat(key: String, clazz: Class<T>): T? {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            getParcelable(key, clazz)
        } else {
            BundleCompat.getParcelable(this, key, clazz)
        }
    }
}
Loading