Qt 6.5.3 - Implementing Google One Tap Sign On for Android
-
I've been trying to implement Google's one tap sign on for android. I've been trying to follow the steps listed in their docs https://developers.google.com/identity/one-tap/android/get-saved-credentials#java, but I don't really know anything about java so I've been flying by the seat of my pants.
I have my java file
GoogleAuth.java
(shown below), and I try to callDisplayOneTapSignIn
in my cpp code:// GoogleAuth.cpp if (QJniObject::isClassAvailable("com/test/google/GoogleAuth")) { qDebug() << "Found GoogleAuth Class"; QJniObject googleAuth = QJniObject("com/test/google/GoogleAuth"); googleAuth.callObjectMethod<void>("DisplayOneTapSignIn"); } else { qDebug() << "Could not find GoogleAuth Class"; }
This doesn't work, though and fails with:
Can't create handler inside thread that has not called Looper.prepare()
I don't really know how to proceed from here. From what I've read I need to run my java class in a UI thread. I tried a few things in the java class that I found on threads, but none of them work.
This lead me to find this from Qt 5.15 https://doc.qt.io/qt-5/qtandroidextras-customactivity-example.html
Do I need to proceed by doing something along the lines in the Qt 5.15 example link above since my java class
extends AppCompatActivity
? Or should I be doing something via java like creating a wrapper class that runs myGoogleAuth
class in a ui thread? If the latter, any suggestions on how to do that?// GoogleAuth.java package com.test.google; import android.util.Log; import com.google.android.gms.auth.api.identity.SignInClient; import com.google.android.gms.auth.api.identity.BeginSignInRequest; import com.google.android.gms.auth.api.identity.BeginSignInRequest.Builder; import com.google.android.gms.auth.api.identity.BeginSignInRequest.GoogleIdTokenRequestOptions; import com.google.android.gms.auth.api.identity.BeginSignInResult; import com.google.android.gms.auth.api.identity.Identity; import android.os.PersistableBundle; import android.os.Bundle; import android.os.Looper; import androidx.annotation.Nullable; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import com.google.android.gms.tasks.OnSuccessListener; import com.google.android.gms.tasks.OnFailureListener; public class GoogleAuth extends AppCompatActivity { // ... private static final String TAG = "GoogleAuth"; private static final int REQ_ONE_TAP = 2; private boolean showOneTapUI = true; private SignInClient oneTapClient; private BeginSignInRequest signInRequest; @Override public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { super.onCreate(savedInstanceState, persistentState); Log.d(TAG, "onCreate start..."); // This is where your UI code goes. oneTapClient = Identity.getSignInClient(this); signInRequest = BeginSignInRequest.builder() // .setPasswordRequestOptions(PasswordRequestOptions.builder() // .setSupported(true) // .build()) .setGoogleIdTokenRequestOptions(GoogleIdTokenRequestOptions.builder() .setSupported(true) // Your server's client ID, not your Android client ID. .setServerClientId("my-client-id") // Only show accounts previously used to sign in. .setFilterByAuthorizedAccounts(true) .build()) // Automatically sign in when exactly one credential is retrieved. .setAutoSelectEnabled(true) .build(); Log.d(TAG, "onCreate end..."); } public void DisplayOneTapSignIn() { Log.d(TAG, "DisplayOneTapSignIn start..."); oneTapClient.beginSignIn(signInRequest) .addOnSuccessListener(this, new OnSuccessListener<BeginSignInResult>() { @Override public void onSuccess(BeginSignInResult result) { try { startIntentSenderForResult( result.getPendingIntent().getIntentSender(), REQ_ONE_TAP, null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "Couldn't start One Tap UI: " + e.getLocalizedMessage()); } } }) .addOnFailureListener(this, new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // No saved credentials found. Launch the One Tap sign-up flow, or // do nothing and continue presenting the signed-out UI. Log.d(TAG, e.getLocalizedMessage()); } }); Log.d(TAG, "DisplayOneTapSignIn end..."); } // ... }
-
A little progress:
Running
setup
below works without error, but then when runningshowOneTapSignOn
I get the following error:E AndroidRuntime: FATAL EXCEPTION: main E AndroidRuntime: Process: com.auth.google, PID: 16379 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'android.app.ActivityThread$ApplicationThread android.app.ActivityThread.getApplicationThread()' on a null object reference E AndroidRuntime: at android.app.Activity.startIntentSenderForResultInner(Activity.java:5970) E AndroidRuntime: at android.app.Activity.startIntentSenderForResult(Activity.java:5941) E AndroidRuntime: at androidx.activity.ComponentActivity.startIntentSenderForResult(ComponentActivity.java:772) E AndroidRuntime: at androidx.core.app.ActivityCompat$Api16Impl.startIntentSenderForResult(ActivityCompat.java:816) E AndroidRuntime: at androidx.core.app.ActivityCompat.startIntentSenderForResult(ActivityCompat.java:283) E AndroidRuntime: at androidx.activity.ComponentActivity$2.onLaunch(ComponentActivity.java:228) E AndroidRuntime: at androidx.activity.result.ActivityResultRegistry$2.launch(ActivityResultRegistry.java:175) E AndroidRuntime: at androidx.activity.result.ActivityResultLauncher.launch(ActivityResultLauncher.java:47) E AndroidRuntime: at com.auth.google.GoogleAuth$3.onSuccess(GoogleAuth.java:196) E AndroidRuntime: at com.auth.google.GoogleAuth$3.onSuccess(GoogleAuth.java:191) E AndroidRuntime: at com.google.android.gms.tasks.zzm.run(com.google.android.gms:play-services-tasks@@18.0.2:1) E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:958) E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:99) E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:205) E AndroidRuntime: at android.os.Looper.loop(Looper.java:294) E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8177) E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
C++ side:
GoogleAuth::GoogleAuth(QObject *parent) : QObject{parent} { QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() { if (QJniObject::isClassAvailable("com/auth/google/GoogleAuth")) { qDebug() << "Found GoogleAuth Class"; m_gauth = QJniObject("com/auth/google/GoogleAuth"); if (!m_gauth.isValid()) { qDebug() << "m_gauth object not valid"; } else { qDebug() << "m_gauth Valid"; m_gauth.callMethod<void>("setup", QNativeInterface::QAndroidApplication::context()); } } else { qDebug() << "Could not find GoogleAuth Class"; } }).waitForFinished(); } ... void GoogleAuth::Auth() { QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() { m_gauth.callMethod<void>("showOneTapSignOn", QNativeInterface::QAndroidApplication::context()); }).waitForFinished(); }
Java side:
Note that I've altered the function below based on this video which says the Google docs uses a deprecated function: https://www.youtube.com/watch?v=Zz3412C4BSA
public class GoogleAuth extends AppCompatActivity { private SignInClient oneTapClient; private BeginSignInRequest signUpRequest; private ActivityResultLauncher<IntentSenderRequest> activityResultLauncher; private static final String TAG = "GoogleAuth"; private static final int REQ_ONE_TAP = 2; public GoogleAuth() { Log.i(TAG, "GoogleAuth ctor running"); } public void setup(Context qtContext) { if (qtContext == null) { Log.i(TAG, "qtContext == null"); } oneTapClient = Identity.getSignInClient(qtContext); Log.i(TAG, "oneTapClient created"); signUpRequest = BeginSignInRequest.builder() .setGoogleIdTokenRequestOptions(GoogleIdTokenRequestOptions.builder() .setSupported(true) // Your server's client ID, not your Android client ID. .setServerClientId("<server client id>") .setFilterByAuthorizedAccounts(false) .build()) .build(); activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartIntentSenderForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { Log.i(TAG, "onActivityResult start"); if (result.getResultCode() == Activity.RESULT_OK) { Log.i(TAG, "result.getResultCode() == Activity.RESULT_OK"); try { SignInCredential credential = oneTapClient.getSignInCredentialFromIntent(result.getData()); String idToken = credential.getGoogleIdToken(); if (idToken != null) { Log.i(TAG, "Got ID token"); // authenticate with backend server here! String email = credential.getId(); Log.i(TAG, "Email = " + email); } } catch (ApiException e) { Log.i(TAG, "ApiException!!"); e.printStackTrace(); } } } }); } public void showOneTapSignOn(Context qtContext) { Log.i(TAG, "showOneTapSignOnSTART!"); oneTapClient.beginSignIn(signUpRequest) .addOnSuccessListener((Activity)qtContext, new OnSuccessListener<BeginSignInResult>() { @Override public void onSuccess(BeginSignInResult result) { IntentSenderRequest intentSenderRequest = new IntentSenderRequest.Builder(result.getPendingIntent().getIntentSender()).build(); activityResultLauncher.launch(intentSenderRequest); } }) .addOnFailureListener((Activity)qtContext, new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // No Google Accounts found. Just continue presenting the signed-out UI. Log.d(TAG, e.getLocalizedMessage()); } }); Log.i(TAG, "showOneTapSignOn END!"); } }