Loading app/src/main/java/foundation/e/apps/data/di/bindings/SystemGoogleAccountBindingModule.kt 0 → 100644 +36 −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.di.bindings import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.login.microg.MicrogSystemGoogleAccountGateway import foundation.e.apps.feature.auth.login.SystemGoogleAccountGateway import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) interface SystemGoogleAccountBindingModule { @Binds @Singleton fun bindSystemGoogleAccountGateway( microgSystemGoogleAccountGateway: MicrogSystemGoogleAccountGateway, ): SystemGoogleAccountGateway } app/src/main/java/foundation/e/apps/data/login/microg/MicrogSystemGoogleAccountGateway.kt 0 → 100644 +89 −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.AccountManager import android.app.Activity import android.content.Context import android.content.Intent import foundation.e.apps.data.login.api.MicrogAccountFetchResult import foundation.e.apps.data.login.api.MicrogAccountFetcher import foundation.e.apps.feature.auth.login.SystemGoogleAccountFetchResult import foundation.e.apps.feature.auth.login.SystemGoogleAccountGateway import javax.inject.Inject import javax.inject.Singleton @Singleton class MicrogSystemGoogleAccountGateway @Inject constructor( private val microgAccountFetcher: MicrogAccountFetcher, ) : SystemGoogleAccountGateway { override fun hasSupportedProvider(context: Context): Boolean { return MicrogSupportChecker.hasSupportedMicrog(context) } override fun hasAccount(): Boolean { return microgAccountFetcher.hasMicrogAccount() } override fun createAccountPickerIntent(): Intent { return AccountManager.newChooseAccountIntent( null, null, arrayOf(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE), null, null, null, null, ) } override fun selectedAccountName(data: Intent?): String? { return data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) } override fun startAddAccount( activity: Activity, onComplete: () -> Unit, ) { AccountManager.get(activity).addAccount( MicrogCertUtil.GOOGLE_ACCOUNT_TYPE, null, null, null, activity, { onComplete() }, null, ) } override suspend fun fetchAccount(accountName: String): SystemGoogleAccountFetchResult { return when (val result = microgAccountFetcher.fetchAccount(accountName)) { is MicrogAccountFetchResult.Success -> SystemGoogleAccountFetchResult.Success( email = result.email, oauthToken = result.oauthToken, ) is MicrogAccountFetchResult.RequiresUserAction -> SystemGoogleAccountFetchResult.RequiresUserAction(result.intent) is MicrogAccountFetchResult.Error -> SystemGoogleAccountFetchResult.Error(result.throwable) } } } app/src/main/java/foundation/e/apps/feature/auth/login/GoogleOAuthCredentialsExtractor.kt +20 −3 Original line number Diff line number Diff line Loading @@ -17,8 +17,6 @@ package foundation.e.apps.feature.auth.login import foundation.e.apps.data.playstore.utils.AC2DMUtil internal object GoogleOAuthCredentialsExtractor { const val EMBEDDED_SETUP_URL = "https://accounts.google.com/EmbeddedSetup" private const val AUTH_TOKEN_COOKIE = "oauth_token" Loading @@ -28,7 +26,7 @@ internal object GoogleOAuthCredentialsExtractor { return null } return AC2DMUtil.parseCookieString(cookieHeader)[AUTH_TOKEN_COOKIE] return parseCookieString(cookieHeader)[AUTH_TOKEN_COOKIE] ?.takeUnless(String::isBlank) } Loading @@ -47,4 +45,23 @@ internal object GoogleOAuthCredentialsExtractor { ): String { return "$email|$oauthToken" } private fun parseCookieString(cookies: String): Map<String, String> { return cookies .split(";") .mapNotNull { rawCookie -> val separatorIndex = rawCookie.indexOf("=") if (separatorIndex <= 0) { return@mapNotNull null } val key = rawCookie.substring(0, separatorIndex).trim() val value = rawCookie.substring(separatorIndex + 1).trim() if (key.isBlank() || value.isBlank()) { null } else { key to value } }.toMap() } } app/src/main/java/foundation/e/apps/feature/auth/login/SignInFragment.kt +2 −3 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.login.api.MicrogAccountFetcher import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.navigation.safeNavigate import javax.inject.Inject Loading @@ -27,7 +26,7 @@ class SignInFragment : Fragment() { private val viewModel: LoginViewModel by activityViewModels() @Inject lateinit var microgAccountFetcher: MicrogAccountFetcher lateinit var systemGoogleAccountGateway: SystemGoogleAccountGateway private var dialogState: LoginSelectionDialogState? by mutableStateOf(null) private lateinit var systemWideGoogleLoginLauncher: SystemWideGoogleLoginLauncher Loading @@ -38,7 +37,7 @@ class SignInFragment : Fragment() { super.onCreate(savedInstanceState) systemWideGoogleLoginLauncher = SystemWideGoogleLoginLauncher( fragment = this, microgAccountFetcher = microgAccountFetcher, systemGoogleAccountGateway = systemGoogleAccountGateway, onCredentialsReady = { email, oauthToken -> viewModel.onEvent(LoginUiEvent.MicrogLoginSubmitted(email, oauthToken)) }, Loading app/src/main/java/foundation/e/apps/feature/auth/login/SystemGoogleAccountGateway.kt 0 → 100644 +54 −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.feature.auth.login import android.app.Activity import android.content.Context import android.content.Intent interface SystemGoogleAccountGateway { fun hasSupportedProvider(context: Context): Boolean fun hasAccount(): Boolean fun createAccountPickerIntent(): Intent fun selectedAccountName(data: Intent?): String? fun startAddAccount( activity: Activity, onComplete: () -> Unit, ) suspend fun fetchAccount(accountName: String): SystemGoogleAccountFetchResult } sealed interface SystemGoogleAccountFetchResult { data class Success( val email: String, val oauthToken: String, ) : SystemGoogleAccountFetchResult data class RequiresUserAction( val intent: Intent, ) : SystemGoogleAccountFetchResult data class Error( val throwable: Throwable, ) : SystemGoogleAccountFetchResult } Loading
app/src/main/java/foundation/e/apps/data/di/bindings/SystemGoogleAccountBindingModule.kt 0 → 100644 +36 −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.di.bindings import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.login.microg.MicrogSystemGoogleAccountGateway import foundation.e.apps.feature.auth.login.SystemGoogleAccountGateway import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) interface SystemGoogleAccountBindingModule { @Binds @Singleton fun bindSystemGoogleAccountGateway( microgSystemGoogleAccountGateway: MicrogSystemGoogleAccountGateway, ): SystemGoogleAccountGateway }
app/src/main/java/foundation/e/apps/data/login/microg/MicrogSystemGoogleAccountGateway.kt 0 → 100644 +89 −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.AccountManager import android.app.Activity import android.content.Context import android.content.Intent import foundation.e.apps.data.login.api.MicrogAccountFetchResult import foundation.e.apps.data.login.api.MicrogAccountFetcher import foundation.e.apps.feature.auth.login.SystemGoogleAccountFetchResult import foundation.e.apps.feature.auth.login.SystemGoogleAccountGateway import javax.inject.Inject import javax.inject.Singleton @Singleton class MicrogSystemGoogleAccountGateway @Inject constructor( private val microgAccountFetcher: MicrogAccountFetcher, ) : SystemGoogleAccountGateway { override fun hasSupportedProvider(context: Context): Boolean { return MicrogSupportChecker.hasSupportedMicrog(context) } override fun hasAccount(): Boolean { return microgAccountFetcher.hasMicrogAccount() } override fun createAccountPickerIntent(): Intent { return AccountManager.newChooseAccountIntent( null, null, arrayOf(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE), null, null, null, null, ) } override fun selectedAccountName(data: Intent?): String? { return data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) } override fun startAddAccount( activity: Activity, onComplete: () -> Unit, ) { AccountManager.get(activity).addAccount( MicrogCertUtil.GOOGLE_ACCOUNT_TYPE, null, null, null, activity, { onComplete() }, null, ) } override suspend fun fetchAccount(accountName: String): SystemGoogleAccountFetchResult { return when (val result = microgAccountFetcher.fetchAccount(accountName)) { is MicrogAccountFetchResult.Success -> SystemGoogleAccountFetchResult.Success( email = result.email, oauthToken = result.oauthToken, ) is MicrogAccountFetchResult.RequiresUserAction -> SystemGoogleAccountFetchResult.RequiresUserAction(result.intent) is MicrogAccountFetchResult.Error -> SystemGoogleAccountFetchResult.Error(result.throwable) } } }
app/src/main/java/foundation/e/apps/feature/auth/login/GoogleOAuthCredentialsExtractor.kt +20 −3 Original line number Diff line number Diff line Loading @@ -17,8 +17,6 @@ package foundation.e.apps.feature.auth.login import foundation.e.apps.data.playstore.utils.AC2DMUtil internal object GoogleOAuthCredentialsExtractor { const val EMBEDDED_SETUP_URL = "https://accounts.google.com/EmbeddedSetup" private const val AUTH_TOKEN_COOKIE = "oauth_token" Loading @@ -28,7 +26,7 @@ internal object GoogleOAuthCredentialsExtractor { return null } return AC2DMUtil.parseCookieString(cookieHeader)[AUTH_TOKEN_COOKIE] return parseCookieString(cookieHeader)[AUTH_TOKEN_COOKIE] ?.takeUnless(String::isBlank) } Loading @@ -47,4 +45,23 @@ internal object GoogleOAuthCredentialsExtractor { ): String { return "$email|$oauthToken" } private fun parseCookieString(cookies: String): Map<String, String> { return cookies .split(";") .mapNotNull { rawCookie -> val separatorIndex = rawCookie.indexOf("=") if (separatorIndex <= 0) { return@mapNotNull null } val key = rawCookie.substring(0, separatorIndex).trim() val value = rawCookie.substring(separatorIndex + 1).trim() if (key.isBlank() || value.isBlank()) { null } else { key to value } }.toMap() } }
app/src/main/java/foundation/e/apps/feature/auth/login/SignInFragment.kt +2 −3 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.login.api.MicrogAccountFetcher import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.navigation.safeNavigate import javax.inject.Inject Loading @@ -27,7 +26,7 @@ class SignInFragment : Fragment() { private val viewModel: LoginViewModel by activityViewModels() @Inject lateinit var microgAccountFetcher: MicrogAccountFetcher lateinit var systemGoogleAccountGateway: SystemGoogleAccountGateway private var dialogState: LoginSelectionDialogState? by mutableStateOf(null) private lateinit var systemWideGoogleLoginLauncher: SystemWideGoogleLoginLauncher Loading @@ -38,7 +37,7 @@ class SignInFragment : Fragment() { super.onCreate(savedInstanceState) systemWideGoogleLoginLauncher = SystemWideGoogleLoginLauncher( fragment = this, microgAccountFetcher = microgAccountFetcher, systemGoogleAccountGateway = systemGoogleAccountGateway, onCredentialsReady = { email, oauthToken -> viewModel.onEvent(LoginUiEvent.MicrogLoginSubmitted(email, oauthToken)) }, Loading
app/src/main/java/foundation/e/apps/feature/auth/login/SystemGoogleAccountGateway.kt 0 → 100644 +54 −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.feature.auth.login import android.app.Activity import android.content.Context import android.content.Intent interface SystemGoogleAccountGateway { fun hasSupportedProvider(context: Context): Boolean fun hasAccount(): Boolean fun createAccountPickerIntent(): Intent fun selectedAccountName(data: Intent?): String? fun startAddAccount( activity: Activity, onComplete: () -> Unit, ) suspend fun fetchAccount(accountName: String): SystemGoogleAccountFetchResult } sealed interface SystemGoogleAccountFetchResult { data class Success( val email: String, val oauthToken: String, ) : SystemGoogleAccountFetchResult data class RequiresUserAction( val intent: Intent, ) : SystemGoogleAccountFetchResult data class Error( val throwable: Throwable, ) : SystemGoogleAccountFetchResult }