Android - Google Login with Login-Screen instead of Automatic Login
android, authentication, google-api-client, google-play-services, google-plus
Solution
The correct answer is here.
One needs to call `mClient.clearDefaultAccountAndReconnect()` to clear previously cached account. It is a good practice to do this everytime a user clicks the sign in button, so that user is shown all his accounts everytime.
Problem
I've added a Google log-in to my Android App (steps can be found at edit 3 of this post). Everything works as it should, but I'd like to make some small adjustments. With the current code found at edit 3, you are automatically logged in every time you start the app, without the Log-in Screen. I want to disable this, since the app will be used on a tablet at work, where a different employee should Login to the app every day. I started by removing the `mGoogleApiClient.connect();` from the `onStart()` method, and now I have the Google Login button again. When I add `mGoogleApiClient.connect();` to the `signInWithGoogle()` I'm able to sign in with the remembered user. What I want right now is the default Google Login screen where you can fill in your Google-Email and Password, every time you click on the Login Button, instead of just logging-in the remembered user. (PS: Keep in mind that on my Android Device I currently only have one user at `Settings -> Google Accounts`, maybe that's why it automatically logs in instead of giving the option to choose which account should be connecting.) I will test if it makes a different when I have multiple Google Accounts on my Android Device. Ok, I've added a second Google Account to my Android Device, but my app still automatically logs in the remembered user when I click on Sign-In.. EDIT 2: I haven't been able to find a solution for my problem yet. I did find some more tutorials with different ways to log-in to Google, like using the `AccountManager` so a user can select one of the existing Google Accounts on the device. (I've only read about this method today, so don't have any code examples for this yet. But this also isn't what I'm looking for anyway.) I probably already made it clear in the post above, but this is what I want explained in pictures: - A User starts the app on his/her Android Device. - A User puts in his Google-Account username (used e-mail) and password - After a user is successfully logged in, we can do other things with the app PS: Just to make sure, this Log-in screen is the one of Google itself. So it's not a Login screen created by myself. This would in theory make me able to save the entered password, which is against protocol of Google OAuth. EDIT 3 (The Code): Steps I did to make Google Services work so far are below. Now I just need to figure out how to either force the log-in screen or completely log-out which results in the log-in screen every time. I've followed the following tutorial: http://www.androidhive.info/2014/02/android-login-with-google-plus-account-1/ With extra info used from the following tutorials/sites: - https://developers.google.com/+/mobile/android/getting-started - https://developers.google.com/+/mobile/android/sign-in - http://developer.android.com/google/play-services/setup.html#Setup This generated the following code: AndroidManifest.xml: ``` <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.testproject_gmaillogin" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.USE_CREDENTIALS" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> <activity android:name="com.example.testproject_gmaillogin.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> ``` strings.xml: ``` <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">TestProject_GmailLogin</string> <string name="action_settings">Settings</string> <string name="profile_pic_description">Google Profile Picture</string> <string name="btn_logout_from_google">Logout from Google</string> <string name="btn_revoke_access">Revoke Access</string> </resources> ``` activity_main.xml: ``` <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp" tools:context=".MainActivity" > <LinearLayout android:id="@+id/profile_layout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:orientation="horizontal" android:weightSum="3" android:visibility="gone"> <ImageView android:id="@+id/img_profile_pic" android:contentDescription="@string/profile_pic_description" android:layout_width="80dp" android:layout_height="wrap_content" android:layout_weight="1"/> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:orientation="vertical" android:layout_weight="2" > <TextView android:id="@+id/txt_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="5dp" android:textSize="20sp" /> <TextView android:id="@+id/txt_email" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="5dp" android:textSize="18sp" /> </LinearLayout> </LinearLayout> <com.google.android.gms.common.SignInButton android:id="@+id/btn_sign_in" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="20dp"/> <Button android:id="@+id/btn_sign_out" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/btn_logout_from_google" android:visibility="gone" android:layout_marginBottom="10dp"/> <Button android:id="@+id/btn_revoke_access" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/btn_revoke_access" android:visibility="gone" /> </LinearLayout> ``` MainActivity.java: ``` package com.example.testproject_gmaillogin; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.common.SignInButton; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import com.google.android.gms.plus.Plus; import com.google.android.gms.plus.model.people.Person; import android.support.v7.app.ActionBarActivity; import android.content.Intent; import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends ActionBarActivity implements ConnectionCallbacks, OnConnectionFailedListener, OnClickListener { // Logcat tag private static final String TAG = "MainActivity"; // Profile pix image size in pixels private static final int PROFILE_PIC_SIZE = 400; // Request code used to invoke sign in user interactions private static final int RC_SIGN_IN = 0; // Client used to interact with Google APIs private GoogleApiClient mGoogleApiClient; // A flag indicating that a PendingIntent is in progress and prevents // us from starting further intents private boolean mIntentInProgress; // Track whether the sign-in button has been clicked so that we know to resolve // all issues preventing sign-in without waiting private boolean mSignInClicked; // Store the connection result from onConnectionFailed callbacks so that we can // resolve them when the user clicks sign-in private ConnectionResult mConnectionResult; // The used UI-elements private SignInButton btnSignIn; private Button btnSignOut, btnRevokeAccess; private ImageView imgProfilePic; private TextView txtName, txtEmail; private LinearLayout profileLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Get the UI-elements btnSignIn = (SignInButton) findViewById(R.id.btn_sign_in); btnSignOut = (Button) findViewById(R.id.btn_sign_out); btnRevokeAccess = (Button) findViewById(R.id.btn_revoke_access); imgProfilePic = (ImageView) findViewById(R.id.img_profile_pic); txtName = (TextView) findViewById(R.id.txt_name); txtEmail = (TextView) findViewById(R.id.txt_email); profileLayout = (LinearLayout) findViewById(R.id.profile_layout); // Set the Button onClick-listeners btnSignIn.setOnClickListener(this); btnSignOut.setOnClickListener(this); btnRevokeAccess.setOnClickListener(this); mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(Plus.API, null) .addScope(Plus.SCOPE_PLUS_LOGIN) .build(); } @Override protected void onStart(){ super.onStart(); mGoogleApiClient.connect(); // <- REMOVED (EDIT 4: Added again) } @Override protected void onStop(){ super.onStop(); if(mGoogleApiClient.isConnected()) mGoogleApiClient.disconnect(); } @Override public void onClick(View view){ switch(view.getId()){ case R.id.btn_sign_in: signInWithGPlus(); break; case R.id.btn_sign_out: signOutFromGPlus(); break; case R.id.btn_revoke_access: revokeGPlusAccess(); break; } } @Override public void onConnectionFailed(ConnectionResult result) { if(!result.hasResolution()){ GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), this, 0).show(); return; } if(!mIntentInProgress){ // Store the ConnectionResult so that we can use it later when the user clicks 'sign-in' mConnectionResult = result; if(mSignInClicked) // The user has already clicked 'sign-in' so we attempt to resolve all // errors until the user is signed in, or they cancel resolveSignInErrors(); } } @Override protected void onActivityResult(int requestCode, int responseCode, Intent intent){ if(requestCode == RC_SIGN_IN && responseCode == RESULT_OK) SignInClicked = true; mIntentInProgress = false; if(!mGoogleApiClient.isConnecting()) mGoogleApiClient.connect(); } } @Override public void onConnected(Bundle connectionHint) { mSignInClicked = false; Toast.makeText(this, "User is connected!", Toast.LENGTH_LONG).show(); // Get all the user's information getProfileInformation(); // Update the UI after sign-in updateUI(true); } @Override public void onConnectionSuspended(int cause){ mGoogleApiClient.connect(); updateUI(false); } // Updating the UI, showing/hiding buttons and profile layout private void updateUI(boolean isSignedIn){ if(isSignedIn){ btnSignIn.setVisibility(View.GONE); btnSignOut.setVisibility(View.VISIBLE); btnRevokeAccess.setVisibility(View.VISIBLE); profileLayout.setVisibility(View.VISIBLE); } else{ btnSignIn.setVisibility(View.VISIBLE); btnSignOut.setVisibility(View.GONE); btnRevokeAccess.setVisibility(View.GONE); profileLayout.setVisibility(View.GONE); } } // Sign-in into Google private void signInWithGPlus(){ //if(!mGoogleApiClient.isConnecting()) // <- ADDED (EDIT 4: Removed again) //mGoogleApiClient.connect(); // <- ADDED (EDIT 4: Removed again) if(!mGoogleApiClient.isConnecting()){ mSignInClicked = true; resolveSignInErrors(); } } // Method to resolve any sign-in errors private void resolveSignInErrors(){ if(mConnectionResult.hasResolution()){ try{ mIntentInProgress = true; //Toast.makeText(this, "Resolving Sign-in Errors", Toast.LENGTH_SHORT).show(); mConnectionResult.startResolutionForResult(this, RC_SIGN_IN); } catch(SendIntentException e){ // The intent was cancelled before it was sent. Return to the default // state and attempt to connect to get an updated ConnectionResult mIntentInProgress = false; mGoogleApiClient.connect(); } } } // Fetching the user's infromation name, email, profile pic private void getProfileInformation(){ try{ if(Plus.PeopleApi.getCurrentPerson(mGoogleApiClient) != null){ Person currentPerson = Plus.PeopleApi.getCurrentPerson(mGoogleApiClient); String personName = currentPerson.getDisplayName(); String personPhotoUrl = currentPerson.getImage().getUrl(); String personGooglePlusProfile = currentPerson.getUrl(); String personEmail = Plus.AccountApi.getAccountName(mGoogleApiClient); Log.e(TAG, "Name: " + personName + ", " + "plusProfile: " + personGooglePlusProfile + ", " + "email: " + personEmail + ", " + "image: " + personPhotoUrl); txtName.setText(personName); txtEmail.setText(personEmail); // by default the profile url gives 50x50 px image, // but we can replace the value with whatever dimension we // want by replacing sz=X personPhotoUrl = personPhotoUrl.substring(0, personPhotoUrl.length() - 2) + PROFILE_PIC_SIZE; new LoadProfileImage(imgProfilePic).execute(personPhotoUrl); } else{ Toast.makeText(getApplicationContext(), "Person information is null", Toast.LENGTH_LONG).show(); } } catch(Exception ex){ ex.printStackTrace(); } } // Sign-out from Google private void signOutFromGPlus(){ if(mGoogleApiClient.isConnected()){ Plus.AccountApi.clearDefaultAccount(mGoogleApiClient); mGoogleApiClient.disconnect(); mGoogleApiClient.connect(); updateUI(false); } } // Revoking access from Google private void revokeGPlusAccess(){ if(mGoogleApiClient.isConnected()){ Plus.AccountApi.clearDefaultAccount(mGoogleApiClient); Plus.AccountApi.revokeAccessAndDisconnect(mGoogleApiClient) .setResultCallback(new ResultCallback<Status>(){ @Override public void onResult(Status s){ Log.e(TAG, "User access revoked!"); mGoogleApiClient.connect(); updateUI(false); } }); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) return true; return super.onOptionsItemSelected(item); } } ``` LoadProfileImage.java: ``` package com.example.testproject_gmaillogin; import java.io.InputStream; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.util.Log; import android.widget.ImageView; /** * Background async task to load user profile picture from url **/ public class LoadProfileImage extends AsyncTask<String, Void, Bitmap> { private ImageView bmImage; public LoadProfileImage(ImageView bmImage){ this.bmImage = bmImage; } @Override protected Bitmap doInBackground(String... urls){ String urlDisplay = urls[0]; Bitmap mIcon11 = null; try{ InputStream in = new java.net.URL(urlDisplay).openStream(); mIcon11 = BitmapFactory.decodeStream(in); } catch(Exception ex){ Log.e("Error", ex.getMessage()); ex.printStackTrace(); } return mIcon11; } @Override protected void onPostExecute(Bitmap result){ bmImage.setImageBitmap(result); } } ``` The other steps I did were: At https://console.developers.google.com/project I've created a project with: Google+ API on: And a Client ID created with the correct SHA1 and exact same namespace as the project: At Eclipse: I've installed the google-play-services library: And added it to the project: I've also created an Emulator with version Google 4.4.2 (so not Android 4.4.2) and also changed the Project to Google 4.4.2 instead of Android 4.4.2: EDIT 4: Ok, I have a temporary solution for my own case. In my case the app I'm making should run on a Tablet which is used explicitly for my App. Because this is the case, I can remove all the Google Accounts from the Device Settings the moment someone revokes access (as log out function). I started by removing the previous changes (re-added the `.connect();` to the `onStart()` and removed it from the `signInWithGPlus()`) Then I added one line to the `revokeGPlusAccess`-method: ``` // Revoking access from Google private void revokeGPlusAccess(){ if(mGoogleApiClient.isConnected()){ Plus.AccountApi.clearDefaultAccount(mGoogleApiClient); Plus.AccountApi.revokeAccessAndDisconnect(mGoogleApiClient) .setResultCallback(new ResultCallback<Status>(){ @Override public void onResult(Status s){ Log.e(TAG, "User access revoked!"); removeAllGoogleAccountsFromDevice(); // <- Added mGoogleApiClient.connect(); updateUI(false); } }); } } ``` With the following method: ``` // Method to remove ALL Google Accounts from the Android Device private void removeAllGoogleAccountsFromDevice(){ // Ask if this really is what you want new AlertDialog.Builder(MainActivity.mActivity) .setMessage("Are you sure you want to delete all Google Accounts from this Android Device?\r\n\r\n" + "WARNING: If you run this app on the Work Tablet, click YES. If you run this on your own device, it's recommended to click NO.") .setCancelable(false) .setPositiveButton("Yes, continue", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // AccountManager is final because we use it in the separate Thread below final AccountManager accountManager = AccountManager.get(MainActivity.this); Account[] googleAccounts = accountManager.getAccountsByType(GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE); // Account is final because we use it in the separate Thread below for(final Account a : googleAccounts){ // Separate Thread because AccountManager #removeAccount is an async operation Thread worker = new Thread(new Runnable() { @Override public void run() { accountManager.removeAccount(a, null, null); } }); worker.start(); } } }) .setNegativeButton("No", null) .show(); } ``` Still, this only works in my case. This won't work on a personal device where you obviously don't want to delete all the Device's Google Accounts. I would still like to know a solution when you don't want to delete any Device's Google Accounts.