Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stuck in Connection Failed loop with GoogleApiClient

I am trying to implement the com.google.android.gms.common.api.GoogleApiClient in my project.

The problem is that every time I try to connect, I get the call back to the onConnectionFailed listener with a pending intent that I execute. On a clean install, the very first pending intent will launch an account select screen. This is expected. Every subsequent restart of the app will bypass the account selection, unless the app's data is cleared in the Application Manager.

After the account-select screen, the signing-in screen will appear breifly. It never signs in though. The onActivityResult will be called after the signing-in screen flashes, which tries to connect the client. It doesn't connect, and calls the onConnectionFailed listener again.

If I keep trying to execute the intents, I get stuck in loop with the signing in screen breifly appearing, then disappearing, but never connecting or getting signed in. The ConnectionResult.toString indicates "Sign_In_Required", and returns an error code of 4 (the same as the Sign_In_Required constant.

On the API console, I've implemented an Ouath 2.0 client ID, and a public API access key for android applications. Notably, my app works using the older com.google.api.services.drive.Drive client.

As for my code:

I've tried using two different implementations here and here. I tried to implement the second example making as few changes as possible. It is reproduced below:

public class MainActivity extends Activity implements ConnectionCallbacks,
    OnConnectionFailedListener {

private static final String TAG = "android-drive-quickstart";
private static final int REQUEST_CODE_CAPTURE_IMAGE = 1;
private static final int REQUEST_CODE_CREATOR = 2;
private static final int REQUEST_CODE_RESOLUTION = 3;

private GoogleApiClient mGoogleApiClient;
private Bitmap mBitmapToSave;

/**
 * Create a new file and save it to Drive.
 */
private void saveFileToDrive() {
    // Start by creating a new contents, and setting a callback.
    Log.i(TAG, "Creating new contents.");
    final Bitmap image = mBitmapToSave;

    Drive.DriveApi.newContents(mGoogleApiClient).setResultCallback(new ResultCallback<DriveApi.ContentsResult>() {

        @Override
        public void onResult(DriveApi.ContentsResult result) {

            // If the operation was not successful, we cannot do anything
            // and must
            // fail.
            if (!result.getStatus().isSuccess()) {
                Log.i(TAG, "Failed to create new contents.");
                return;
            }
            // Otherwise, we can write our data to the new contents.
            Log.i(TAG, "New contents created.");
            // Get an output stream for the contents.
            OutputStream outputStream = result.getContents().getOutputStream();
            // Write the bitmap data from it.
            ByteArrayOutputStream bitmapStream = new ByteArrayOutputStream();
            image.compress(Bitmap.CompressFormat.PNG, 100, bitmapStream);
            try {
                outputStream.write(bitmapStream.toByteArray());
            } catch (IOException e1) {
                Log.i(TAG, "Unable to write file contents.");
            }
            // Create the initial metadata - MIME type and title.
            // Note that the user will be able to change the title later.
            MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
                    .setMimeType("image/jpeg").setTitle("Android Photo.png").build();
            // Create an intent for the file chooser, and start it.
            IntentSender intentSender = Drive.DriveApi
                    .newCreateFileActivityBuilder()
                    .setInitialMetadata(metadataChangeSet)
                    .setInitialContents(result.getContents())
                    .build(mGoogleApiClient);
            try {
                startIntentSenderForResult(
                        intentSender, REQUEST_CODE_CREATOR, null, 0, 0, 0);
            } catch (SendIntentException e) {
                Log.i(TAG, "Failed to launch file chooser.");
            }
        }
    });
}

@Override
protected void onResume() {
    super.onResume();
    if (mGoogleApiClient == null) {
        // Create the API client and bind it to an instance variable.
        // We use this instance as the callback for connection and connection
        // failures.
        // Since no account name is passed, the user is prompted to choose.
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Drive.API)
                .addScope(Drive.SCOPE_FILE)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .build();
    }
    // Connect the client. Once connected, the camera is launched.
    mGoogleApiClient.connect();
}

@Override
protected void onPause() {
    if (mGoogleApiClient != null) {
        mGoogleApiClient.disconnect();
    }
    super.onPause();
}

@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
    switch (requestCode) {
        case REQUEST_CODE_CAPTURE_IMAGE:
            // Called after a photo has been taken.
            if (resultCode == Activity.RESULT_OK) {
                // Store the image data as a bitmap for writing later.
                mBitmapToSave = (Bitmap) data.getExtras().get("data");
            }
            break;
        case REQUEST_CODE_CREATOR:
            // Called after a file is saved to Drive.
            if (resultCode == RESULT_OK) {
                Log.i(TAG, "Image successfully saved.");
                mBitmapToSave = null;
                // Just start the camera again for another photo.
                startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE),
                        REQUEST_CODE_CAPTURE_IMAGE);
            }
            break;
    }
}

@Override
public void onConnectionFailed(ConnectionResult result) {
    // Called whenever the API client fails to connect.
    Log.i(TAG, "GoogleApiClient connection failed: " + result.toString());
    if (!result.hasResolution()) {
        // show the localized error dialog.
        GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), this, 0).show();
        return;
    }
    // The failure has a resolution. Resolve it.
    // Called typically when the app is not yet authorized, and an
    // authorization
    // dialog is displayed to the user.
    try {
        result.startResolutionForResult(this, REQUEST_CODE_RESOLUTION);
    } catch (SendIntentException e) {
        Log.e(TAG, "Exception while starting resolution activity", e);
    }
}

@Override
public void onConnected(Bundle connectionHint) {
    Log.i(TAG, "API client connected.");
    if (mBitmapToSave == null) {
        // This activity has no UI of its own. Just start the camera.
        startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE),
                REQUEST_CODE_CAPTURE_IMAGE);
        return;
    }
    saveFileToDrive();
}

@Override
public void onConnectionSuspended(int cause) {
    Log.i(TAG, "GoogleApiClient connection suspended");
}

}

like image 460
NameSpace Avatar asked Nov 05 '14 15:11

NameSpace


2 Answers

This happens because after the first login/authorization android keeps using the same default account parameters. If you want to avoid the loop and make sure the picker shows again you must clear completely the default account by calling Plus.AccountApi.clearDefaultAccount(mGoogleApiClient) before reconnecting again.

To achieve this, you must add the Plus.API scope to the GoogleApiClient builder:

        mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addApi(Drive.API)
            .addApi(Plus.API)
            .addScope(Drive.SCOPE_FILE)
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .build();

And then you can clear the default account before rebuilding the api client and connecting to a different account (rebuilding the api client when changing accounts avoids problems):

    // if the api client existed, we terminate it
    if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
        Plus.AccountApi.clearDefaultAccount(mGoogleApiClient);
        mGoogleApiClient.disconnect();
    }
    // build new api client to avoid problems reusing it
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addApi(Drive.API)
            .addApi(Plus.API)
            .addScope(Drive.SCOPE_FILE)
            .setAccountName(account)
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .build();
    mGoogleApiClient.connect();

No additional permissions or api activations are needed for using the Plus.API scope this way. I hope this helps with your problem.

like image 97
jmart Avatar answered Oct 22 '22 23:10

jmart


It is a tough one, since I don't have time to completely re-run and analyze your code. And without running it, I don't see anything obvious.

But, since I have this stuff up and running in my app, I'd like to help. Unfortunately the Google Play services connection and authorization code is scattered all over my app's fragments and activities. So, I made an attempt to create a dummy activity and pull all the stuff in it. By 'all the stuff' I mean the account manager wrapper (GA) and associated account picker code.

The result is some 300 lines of gibberish that may work, but I don't make any claims it will. Take a look and good luck.

package com.......;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;

import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.common.AccountPicker;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;

public class GooApiClient extends Activity  implements 
                GoogleApiClient.OnConnectionFailedListener, GoogleApiClient.ConnectionCallbacks {

  private static final String DIALOG_ERROR = "dialog_error";
  private static final String REQUEST_CODE = "request_code";

  private static final int REQ_ACCPICK = 1;
  private static final int REQ_AUTH    = 2;
  private static final int REQ_RECOVER = 3;

  private GoogleApiClient mGooApiClient;
  private boolean mIsInAuth; //block re-entrancy

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (checkPlayServices() && checkUserAccount()) {
      gooInit();
      gooConnect(true);
    }
  }

  @Override
  public void onConnected(Bundle bundle) {
    Log.d("_", "connected");
  }
  @Override
  public void onConnectionSuspended(int i) { }
  @Override
  public void onConnectionFailed(ConnectionResult result) {
    Log.d("_", "failed " + result.hasResolution());
    if (!mIsInAuth) {
      if (result.hasResolution()) {
        try {
          mIsInAuth = true;
          result.startResolutionForResult(this, REQ_AUTH);
        } catch (IntentSender.SendIntentException e) {
          suicide("authorization fail");
        }
      } else {
        suicide("authorization fail");
      }
    }
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent it) {
    Log.d("_", "activity result " + requestCode + " " + resultCode);

    switch (requestCode) {
      case REQ_AUTH: case REQ_RECOVER: {
        mIsInAuth = false;
        if (resultCode == Activity.RESULT_OK) {
          gooConnect(true);
        } else if (resultCode == RESULT_CANCELED) {
          suicide("authorization fail");
        }
        return;
      }

      case REQ_ACCPICK: {  // return from account picker
        if (resultCode == Activity.RESULT_OK && it != null) {
          String emil = it.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
          if (GA.setEmil(this, emil) == GA.CHANGED) {
            gooInit();
            gooConnect(true);
          }
        } else if (GA.getActiveEmil(this) == null) {
          suicide("selection failed");
        }
        return;
      }
    }
    super.onActivityResult(requestCode, resultCode, it); // DO NOT REMOVE
  }

  private boolean checkPlayServices() {
    Log.d("_", "check PS");
    int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
    if (status != ConnectionResult.SUCCESS) {
      if (GooglePlayServicesUtil.isUserRecoverableError(status)) {
        mIsInAuth = true;
        errorDialog(status, LstActivity.REQ_RECOVER);
      } else {
        suicide("play services failed");
      }
      return false;
    }
    return true;
  }
  private boolean checkUserAccount() {
    String emil = GA.getActiveEmil(this);
    Account accnt = GA.getPrimaryAccnt(this, true);
    Log.d("_", "check user account " + emil + " " + accnt);

    if (emil == null) {    // no emil (after install)
      if (accnt == null) {    // multiple or no accounts available, go pick one
        accnt = GA.getPrimaryAccnt(this, false);
        Intent it = AccountPicker.newChooseAccountIntent(accnt, null,
         new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE}, true, null, null, null, null
        );
        this.startActivityForResult(it, LstActivity.REQ_ACCPICK);
        return false;  //--------------------->>>

      } else {  // there's only one goo account registered with the device, skip the picker
        GA.setEmil(this, accnt.name);
      }

    // UNLIKELY BUT POSSIBLE, emil's OK, but the account have been removed since (through settings)
    } else {
      accnt = GA.getActiveAccnt(this);
      if (accnt == null) {
        accnt = GA.getPrimaryAccnt(this, false);
        Intent it = AccountPicker.newChooseAccountIntent(accnt, null,
         new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE}, true, null, null, null, null
        );
        this.startActivityForResult(it, LstActivity.REQ_ACCPICK);
        return false;  //------------------>>>
      }
    }
    return true;
  }

  private void gooInit(){
    String emil = GA.getActiveEmil(this);
    Log.d("_", "goo init " + emil);
    if (emil != null){
      mGooApiClient = new GoogleApiClient.Builder(this)
       .setAccountName(emil).addApi(com.google.android.gms.drive.Drive.API)
       .addScope(com.google.android.gms.drive.Drive.SCOPE_FILE)
       .addConnectionCallbacks(this).addOnConnectionFailedListener(this)
       .build();
    }
  }

  private void gooConnect(boolean bConnect) {
    Log.d("_", "goo connect " + bConnect);
    if (mGooApiClient != null) {
      if (!bConnect) {
        mGooApiClient.disconnect();
      } else if (! (mGooApiClient.isConnecting() || mGooApiClient.isConnected())){
        mGooApiClient.connect();
      }
    }
  }

  private void suicide(String msg) {
    GA.removeActiveAccnt(this);
    Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
    finish();
  }

  private void errorDialog(int errorCode, int requestCode) {
    Bundle args = new Bundle();
    args.putInt(DIALOG_ERROR, errorCode);
    args.putInt(REQUEST_CODE, requestCode);
    ErrorDialogFragment dialogFragment = new ErrorDialogFragment();
    dialogFragment.setArguments(args);
    dialogFragment.show(getFragmentManager(), "errordialog");
  }
  public static class ErrorDialogFragment extends DialogFragment {
    public ErrorDialogFragment() { }
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
      int errorCode = getArguments().getInt(DIALOG_ERROR);
      int requestCode = getArguments().getInt(DIALOG_ERROR);
      return GooglePlayServicesUtil.getErrorDialog(errorCode, getActivity(), requestCode);
    }
    @Override
    public void onDismiss(DialogInterface dialog) {
      getActivity().finish();
    }
  }

  private static class GA {
    private static final String ACC_NAME = "account_name";
    public static final int FAIL = -1;
    public static final int UNCHANGED =  0;
    public static final int CHANGED = +1;

    private static String mCurrEmil = null;  // cache locally
    private static String mPrevEmil = null;  // cache locally

    public static Account[] getAllAccnts(Context ctx) {
      return AccountManager.get(acx(ctx)).getAccountsByType(GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE);
    }

    public static Account getPrimaryAccnt(Context ctx, boolean bOneOnly) {
      Account[] accts = getAllAccnts(ctx);
      if (bOneOnly)
        return accts == null || accts.length != 1 ? null : accts[0];
      return accts == null || accts.length == 0 ? null : accts[0];
    }

    public static Account getActiveAccnt(Context ctx) {
      return emil2Accnt(ctx, getActiveEmil(ctx));
    }

    public static String getActiveEmil(Context ctx) {
      if (mCurrEmil != null) {
        return mCurrEmil;
      }
      mCurrEmil = ctx == null ? null : pfs(ctx).getString(ACC_NAME, null);
      return mCurrEmil;
    }

    public static Account getPrevEmil(Context ctx) {
      return emil2Accnt(ctx, mPrevEmil);
    }

    public static Account emil2Accnt(Context ctx, String emil) {
      if (emil != null) {
        Account[] accounts =
         AccountManager.get(acx(ctx)).getAccountsByType(GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE);
        for (Account account : accounts) {
          if (emil.equalsIgnoreCase(account.name)) {
            return account;
          }
        }
      }
      return null;
    }

    /**
     * Stores a new email in persistent app storage, reporting result
     * @param newEmil new email, optionally null
     * @param ctx activity context
     * @return FAIL, CHANGED or UNCHANGED (based on the following table)
     * OLD    NEW   SAVED   RESULT
     * ERROR                FAIL
     * null   null  null    FAIL
     * null   new   new     CHANGED
     * old    null  old     UNCHANGED
     * old != new   new     CHANGED
     * old == new   new     UNCHANGED
     */
    public static int setEmil(Context ctx, String newEmil) {
      int result = FAIL;  // 0  0

      mPrevEmil = getActiveEmil(ctx);
      if        ((mPrevEmil == null) && (newEmil != null)) {
        result = CHANGED;
      } else if ((mPrevEmil != null) && (newEmil == null)) {
        result = UNCHANGED;
      } else if ((mPrevEmil != null) && (newEmil != null)) {
        result = mPrevEmil.equalsIgnoreCase(newEmil) ? UNCHANGED : CHANGED;
      }
      if (result == CHANGED) {
        mCurrEmil = newEmil;
        pfs(ctx).edit().putString(ACC_NAME, newEmil).apply();
      }
      return result;
    }
    public static void removeActiveAccnt(Context ctx) {
      mCurrEmil = null;
      pfs(ctx).edit().remove(ACC_NAME).apply();
    }

    private static Context acx(Context ctx) {
      return ctx == null ? null : ctx.getApplicationContext();
    }
    private static SharedPreferences pfs(Context ctx) {
      return ctx == null ? null : PreferenceManager.getDefaultSharedPreferences(acx(ctx));
    }
  }
}

BTW, I know how to spell 'email', 'Emil' just happened to be my uncle's name and I couldn't resist :-)

UPDATE (2015-Apr-11):

I've recently re-visited the code that handles Google Drive Authorization and Account switching. The result can be found here and it supports both REST and GDAA apis.

like image 3
seanpj Avatar answered Oct 22 '22 23:10

seanpj