Google sign-in fallback chain: Credential Manager -> legacy API (microG) -> browser
Some checks failed
build / apk (push) Has been cancelled

- 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
This commit is contained in:
Niko Marmeladkov 2026-10-05 17:31:30 +03:00
parent 147b17dc44
commit b2914f685a
3 changed files with 73 additions and 10 deletions

View file

@ -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 |

View file

@ -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")
}

View file

@ -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<Intent> =
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) {