From b2914f685a843ddc9e676651e92d1537132efd45 Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Mon, 5 Oct 2026 17:31:30 +0300 Subject: [PATCH] Google sign-in fallback chain: Credential Manager -> legacy API (microG) -> browser - try Credential Manager first; on any failure fall back to the legacy GoogleSignIn API, which microG implements - only when no Google provider is available (isGooglePlayServicesAvailable fails) use the browser flow with the nikogpt:// hand-back - README: document the order and the Android OAuth client requirements - version 1.0.4 --- README.md | 17 ++++++ app/build.gradle.kts | 7 ++- .../java/dev/n1ko/nikogpt/MainActivity.kt | 59 ++++++++++++++++--- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 65475e1..691a241 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,23 @@ apksigner sign --ks nikogpt.jks --out nikogpt-release.apk \ app/build/outputs/apk/release/app-release-unsigned.apk ``` +## Google sign-in + +Native "Continue with Google" is attempted in this order: + +1. **Credential Manager** (modern GMS) — the standard account sheet; +2. **legacy Google Sign-In API** — this is what **microG** implements, so the + native picker works on de-Googled ROMs with microG and a Google account + added there; +3. **browser flow** — used when neither provider is available (no GMS, no + microG, or cancelled); the browser returns to the app through the + `nikogpt://auth` deep link and the session lands in the WebView. + +Native sign-in needs an **Android-type OAuth client** in the same Google Cloud +project, with the app's package name (`dev.n1ko.nikogpt`) and the SHA-1 of the +signing certificate. The web client id (from the server) is passed as +`serverClientId`. + ## Structure | Path | Purpose | diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c05da3c..8b821ec 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "dev.n1ko.nikogpt" minSdk = 29 // Android 10 targetSdk = 35 - versionCode = 3 - versionName = "1.0.3" + versionCode = 4 + versionName = "1.0.4" } buildTypes { @@ -51,4 +51,7 @@ dependencies { implementation("androidx.credentials:credentials:1.3.0") implementation("androidx.credentials:credentials-play-services-auth:1.3.0") implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0") + // Legacy Google Sign-In: microG implements this API, Credential Manager is + // GMS-only. Both are tried before the browser fallback. + implementation("com.google.android.gms:play-services-auth:21.2.0") } diff --git a/app/src/main/java/dev/n1ko/nikogpt/MainActivity.kt b/app/src/main/java/dev/n1ko/nikogpt/MainActivity.kt index 3d1f79a..005c833 100644 --- a/app/src/main/java/dev/n1ko/nikogpt/MainActivity.kt +++ b/app/src/main/java/dev/n1ko/nikogpt/MainActivity.kt @@ -40,6 +40,11 @@ import androidx.credentials.GetCredentialRequest import androidx.credentials.exceptions.GetCredentialException import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.common.api.ApiException import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import kotlinx.coroutines.launch @@ -72,6 +77,23 @@ class MainActivity : AppCompatActivity() { fileCallback = null } + // microG path: the legacy Google Sign-In API, which microG implements. + private var legacyLinkMode = false + private val legacySignIn: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val link = legacyLinkMode + try { + val account = GoogleSignIn.getSignedInAccountFromIntent(result.data) + .getResult(ApiException::class.java) + val token = account?.idToken + if (!token.isNullOrBlank()) deliverToken(token, link) else openGoogleInBrowser(link) + } catch (e: ApiException) { + openGoogleInBrowser(link) + } catch (e: Exception) { + openGoogleInBrowser(link) + } + } + @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -218,12 +240,10 @@ class MainActivity : AppCompatActivity() { } } - // openGooglePicker requests an ID token from Google Play services - // (Credential Manager). Without a usable provider — e.g. microG, no - // accounts, user cancelled — it falls back to the browser flow with the - // nikogpt:// hand-back. + // openGooglePicker tries the native pickers in order: Credential Manager + // (modern GMS) → legacy Google Sign-In (microG) → browser flow. private fun openGooglePicker(clientId: String, link: Boolean) { - if (clientId.isBlank()) { + if (clientId.isBlank() || !googleServicesAvailable()) { openGoogleInBrowser(link) return } @@ -241,14 +261,37 @@ class MainActivity : AppCompatActivity() { .getCredential(this@MainActivity, request) val idToken = GoogleIdTokenCredential.createFrom(response.credential.data).idToken deliverToken(idToken, link) - } catch (e: GetCredentialException) { - openGoogleInBrowser(link) } catch (e: Exception) { - openGoogleInBrowser(link) + // No Credential Manager provider (microG) — try the legacy API. + openLegacyGooglePicker(clientId, link) } } } + // googleServicesAvailable is true for GMS and for microG (which reports + // itself as Play services when signature spoofing is enabled). + private fun googleServicesAvailable(): Boolean { + return try { + GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS + } catch (e: Exception) { + false + } + } + + @Deprecated("microG only implements the legacy Google Sign-In API") + private fun openLegacyGooglePicker(clientId: String, link: Boolean) { + legacyLinkMode = link + try { + val options = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(clientId) + .requestEmail() + .build() + legacySignIn.launch(GoogleSignIn.getClient(this, options).signInIntent) + } catch (e: Exception) { + openGoogleInBrowser(link) + } + } + // deliverToken hands the ID token back to the page, which signs in // through the API inside this WebView (so the cookie stays in the app). private fun deliverToken(idToken: String, link: Boolean) {