23

I’ve implemented the Google Sign-In SDK into my application and it works fine. When I click on the sign-in button, a window opens displaying the already stored accounts. Selecting one of those accounts successfully ends the sign-in process.

The one use case that does not pass is when the user gets to the sign-in dialog and clicks on an account that has an invalid password. I’m not sure how to solve this issue.


I followed with Google instruction "implement Sign-in SDK" and after calling those lines:

Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
GoogleSignInAccount googleSignInAccount = task.getResult(ApiException.class);

I catch exception with status code 12501 SIGN_IN_CANCELLED.

As I said before, it happens because one of the stored accounts has invalid password.

Here are the steps to reproduce:

  1. user logged in once
  2. dialog stored his credentials
  3. meanwhile user changed his account's password on www
  4. user selects saved credentials
  5. unrelated error code occurs).

How could I make user to redirect to this blue Google Sign-In page and keep the current flow?

For example, AliExpress somehow can handle this and redirects user to blue page with asking user to sign in again.

enter image description here

My code is not much different than in Google's instruction. This is my code flow. All start from onClick():

In onClick() method:

// Logout before all operations
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
if (account != null) {
    mGoogleSignInClient.signOut();
}

// Call to sign in
Intent signInIntent = mGoogleSignInClient.getSignInIntent();
startActivityForResult(signInIntent, RequestCodes.RC_GOOGLE_SIGN_IN);

In onActivityResult section:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");

    if (requestCode == RequestCodes.RC_GOOGLE_SIGN_IN) {

        try {

            // Call to take account data
            Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);

            // Fetch account data
            GoogleSignInAccount googleSignInAccount = task.getResult(ApiException.class);

            Account account = googleSignInAccount.getAccount();

            // Calling to get short lived token
            String shortLivedToken = GoogleAuthUtil.getToken(mContext, account, "oauth2:" + Scopes.PROFILE + " " + Scopes.EMAIL);

            // Further calls here...

        } catch (ApiException e) {

            //https://developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInStatusCodes

            if (e.getStatusCode() == 12501) {
                Log.e(TAG, "SIGN_IN_CANCELLED");
            } else if (e.getStatusCode() == 12502) {
                Log.e(TAG, "SIGN_IN_CURRENTLY_IN_PROGRESS");
            } else if (e.getStatusCode() == 12500) {
                Log.e(TAG, "SIGN_IN_FAILED");
            } else {
                e.printStackTrace();
            }

        } catch (GoogleAuthException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    } else {
        super.onActivityResult(requestCode, resultCode, data);
    }
}
deadfish
  • 11,996
  • 12
  • 87
  • 136
  • I dont know about native but in the js api, you can call signout first, then the user has to choose an account and enter his credentials during the next login. Maybe that is the direction to go. – Chris Feb 23 '19 at 19:29
  • I'm afraid this is not my scenario. Signing out doesn't reset or delete user from dialog with accounts list. – deadfish Feb 23 '19 at 19:30
  • I don't think that you can do anything about it except notify user with a message that he has to re login to his google account or something like that – Antonis Radz Mar 01 '19 at 14:20
  • @AntonisRadz well I know it is possible because it has been proven. Leaving just a message is a bad UX. – deadfish Mar 01 '19 at 15:11
  • @deadfish what if would simply revoke account like here https://developers.google.com/identity/sign-in/android/disconnect and then again ask for login? or after revoking try to do normal login – Antonis Radz Mar 01 '19 at 16:26
  • Thanks, I will try that but it won't be the same as they do in AliExpress app. – deadfish Mar 01 '19 at 22:36
  • From the screenshots: it looks like its: 1. Checking credentials, 2. Signs-out when something went wrong(e.g. wrong password). 3. Asks to sign-in again. and also it is quite strange that you receive [SIGN_IN_CANCELLED](https://developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInStatusCodes#SIGN_IN_CANCELLED), since its returned only when its cancelled by user(maybe your code). – BladeMight Mar 10 '19 at 20:34
  • Looks like SIGN_IN_CANCELLED can be returned also when the saved user's password is wrong. – deadfish Mar 11 '19 at 09:22

1 Answers1

6

Disclaimer I am not a Google employee. Everything I say below are my conclusions from investigating similar issues.

Short answer

You are doing everything right. It is the recommended way of logging into google account. Unfortunately there is no actual callback in this mechanism to specify what actually went wrong in your case. The way Google Play Services handles it is by notifying user that his credentials became obsolete by notificaion you can see below (right after the password was changed).

notification

I would recommend filing a bug for adding extra result codes for your case on https://issuetracker.google.com as it seems like a sensible improvement.

Long answer

Google uses Android account API just like everybody else (you can try it yourself). Behind the scenes it's just a oauth token retrieval and storage mechanism.

When the password was changed, the token is no longer valid and you get errors trying to use it.

The way it works is the way Google Play Services developers chose to implement it (hence I recommend you to file a bug).

For example, AliExpress somehow can handle this and redirects user to blue page with asking user to sign in again.

Aliexpress uses the deprecated API. As you can see the dialog for picking account has different color and no avatars. The API is is still usable, but may be shut down anytime (or not). I do not recommend you to use this, but here is how it works:

import com.google.android.gms.common.AccountPicker;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.UserRecoverableAuthException;

void chooseAccount() {
    Intent signInIntent = AccountPicker.newChooseAccountIntent(null, null, new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE}, true, null, null, null, null);
    startActivityForResult(signInIntent, REQ_CHOOSE_ACCOUNT);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {

    if (requestCode == REQ_CHOOSE_ACCOUNT) {

        String email = data.getExtras().getString("authAccount");
        // better do this in background thread
        try {
            GoogleAuthUtil.getToken(this, new Account(email, GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE), "oauth2:https://www.googleapis.com/auth/userinfo.profile");
        } catch (UserRecoverableAuthException recEx) {
            Intent recoverIntent = recEx.getIntent();
            // Will redirect to login activity
            startActivityForResult(recoverIntent, REQ_RECOVER);
        } catch (Exception e) {
            Log.d(TAG, "caught exception", e);
        }

    }
}

Hope it helps!

UPD: new Google Play APIs have ResolvableApiException, which extends ApiException you are catching. It has method startResolutionForResult() similar to the one used in older APIs. But the Bundle you receive contains no resolution info.

Bundle[{googleSignInStatus=Status{statusCode=unknown status code: 12501, resolution=null}}]

If you will file a bug, post it here, we will star it)

You can also show the "Choose account" dialog using default Android API (min API 23)

The code below may be sued to show a "choose account dialog" using default Android Account Management APIs. This is new and (hopefully) not going to be deprecated for a while.

import android.accounts.Account;
import android.accounts.AccountManager;

// Unfortunately can be used only on API 23 and higher
Intent signInIntent = AccountManager.newChooseAccountIntent(
            null,
            null,
            new String[] { "com.google" },
            "Please select your account",
            null,
            null,
            new Bundle());

startActivityForResult(signInIntent, REQ_SELECT_ACCOUNT);

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQ_SELECT_ACCOUNT) {
            String accountName = data.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME);
            String accountType = data.getExtras().getString(AccountManager.KEY_ACCOUNT_TYPE);
            // now you can call GoogleAuthUtil as in example above
        }
    }

You can also get list of Google accounts, visible to your app

Account becomes visible to your app after user have tried to sign in with that kind of account into your app, using one of the methods above. Event if sign in was not successful (e.g. password is expired), you will see this account in the list (won't be able to distinguish which one in case of multiple accounts though). So this may be used as workaround, but in a limited way.

import android.accounts.Account;
import android.accounts.AccountManager;

try {
        // requires android.permission.GET_ACCOUNTS
        Account[] accounts = AccountManager.get(this).getAccountsByType("com.google");
        for (Account account : accounts) {
            Log.d(TAG, "account: " + account.name);
        }
    } catch (Exception e) {
        Log.i("Exception", "Exception:" + e);
    }

Conclusion Unfortunately I've found no other ways to access google account data to work around your case using modern Google Sign-In APIs. All advanced AccountManager APIs require you to have the same signature as account owner app (GMS - Google Mobile Services) which is not the case. So we can only request this from Google and hope for it to be implemented :(

Amaksoft
  • 954
  • 9
  • 16