In this tutorial, we’ll be discussing the Smart Lock feature and implement it in our Android Application.
Smart Lock is used to automatically sign into your application by saving the credentials once and for all. That means that if you reinstall your application after a while, you can automatically sign in with the previously saved credentials provided you didn’t delete them from the chrome passwords.
Google Smart Lock lets you sign in with just one tap.
In order to integrate Smart Lock in your application, you need to use Credentials API. Credentials API allow the user to:
To use Google Smart Lock in your application you need to add the following dependency:
dependencies {
implementation 'com.google.android.gms:play-services-auth:16.0.0'
}
SmartLock requires the GoogleApiClient to be setup in your android application. SmartLock allows auto-sign in when there is only one credential. When there are more than one credentials, they are shown up in a dialog.
Earlier we used to depend on SharedPreferences to auto-sign and save credentials locally. Now with Google Smart Lock everything is taken care of by the Google Servers.
Following are the major methods present in the Credentials API:
save(GoogleApiClient client, Credential credential)
request(GoogleApiClient client, CredentialRequestrequest)
- Requests all the credentials saved for the app.getHintPickerIntent(GoogleApiClient client, HintRequest request)
- Shows a list of signed-in accounts you have in order to quickly fill up login forms.disableAutoSignIn(GoogleApiClient client)
delete(GoogleApiClient client, Credential credential)
You can view all the credentials saved for a Google account by going to passwords.google.com. What’s the Flow for an application with Smart Lock? You need to structure your login screen code in the following way:
Let’s begin our implementation of Smart Lock feature in an Android application. Set up GoogleApiClient
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.enableAutoManage(this, this)
.build();
Implement the GoogleApiClient interfaces and implement the methods. Initialise Credentials Client
CredentialsOptions options = new CredentialsOptions.Builder()
.forceEnableSaveDialog()
.build();
CredentialsClient mCredentialsApiClient = Credentials.getClient(this, options);
forceEnableSaveDialog() is required for Android Oreo and above. Create CredentialRequest
CredentialRequest mCredentialRequest = new CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.setAccountTypes(IdentityProviders.GOOGLE)
.build();
Retrieve Credentials
Auth.CredentialsApi.request(mGoogleApiClient, mCredentialRequest).setResultCallback(this);
The setResultCallBack
requires us to override the method onResult
from the interface ResultCallback<CredentialRequestResult>
@Override
public void onResult(@NonNull CredentialRequestResult credentialRequestResult) {
Status status = credentialRequestResult.getStatus();
if (status.isSuccess()) {
onCredentialRetrieved(credentialRequestResult.getCredential());
} else {
if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) {
try {
isResolving = true;
status.startResolutionForResult(this, RC_READ);
} catch (IntentSender.SendIntentException e) {
Log.d(TAG, e.toString());
}
} else {
showHintDialog();
}
}
}
There are three cases -
Single Credential - Success- Multiple Credentials - Resolve them and display all the available credentials in a dialog
A status code of RESOLUTION_REQUIRED
implies that there are more than one credentials that need to be resolved. For this, we call startResolutionForResult
which returns the result in the onActivityResult method. We use a boolean flag in order to prevent multiple resolutions to occur. That would cause multiple dialogs to be built. Now that we’ve seen the gist of the SmartLock feature, lets implement it fully with saving and deleting credentials features.
The code for the activity_main.xml layout is given below:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants">
<Button
android:id="@+id/btnLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="24dp"
android:text="Login"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/inPassword" />
<EditText
android:id="@+id/inEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="32dp"
android:ems="10"
android:hint="email"
android:inputType="textEmailAddress"
app:layout_constraintHorizontal_bias="0.503"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<EditText
android:id="@+id/inPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:ems="10"
android:hint="password"
android:inputType="textPassword"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/inEmail" />
</android.support.constraint.ConstraintLayout>
android:importantForAutofill="noExcludeDescendants">
is used to disable Autofill on the EditText Fields. We’ll discuss the Autofill API in a separate tutorial. The code for the MainActivity.java class is given below:
package com.journaldev.androidgooglesmartlock;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentSender;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Patterns;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.auth.api.credentials.Credential;
import com.google.android.gms.auth.api.credentials.CredentialPickerConfig;
import com.google.android.gms.auth.api.credentials.CredentialRequest;
import com.google.android.gms.auth.api.credentials.CredentialRequestResponse;
import com.google.android.gms.auth.api.credentials.CredentialRequestResult;
import com.google.android.gms.auth.api.credentials.Credentials;
import com.google.android.gms.auth.api.credentials.CredentialsClient;
import com.google.android.gms.auth.api.credentials.CredentialsOptions;
import com.google.android.gms.auth.api.credentials.HintRequest;
import com.google.android.gms.auth.api.credentials.IdentityProviders;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResolvableApiException;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import java.util.regex.Pattern;
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, ResultCallback<CredentialRequestResult> {
private GoogleApiClient mGoogleApiClient;
CredentialsClient mCredentialsApiClient;
CredentialRequest mCredentialRequest;
public static final String TAG = "API123";
private static final int RC_READ = 3;
private static final int RC_SAVE = 1;
private static final int RC_HINT = 2;
boolean isResolving;
Button btnLogin;
EditText inEmail, inPassword;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setUpGoogleApiClient();
//needed for Android Oreo.
CredentialsOptions options = new CredentialsOptions.Builder()
.forceEnableSaveDialog()
.build();
mCredentialsApiClient = Credentials.getClient(this, options);
createCredentialRequest();
btnLogin = findViewById(R.id.btnLogin);
inEmail = findViewById(R.id.inEmail);
inPassword = findViewById(R.id.inPassword);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String email = inEmail.getText().toString();
String password = inPassword.getText().toString();
if (TextUtils.isEmpty(email) || TextUtils.isEmpty(password) || !Patterns.EMAIL_ADDRESS.matcher(email).matches())
showToast("Please enter valid email and password");
else {
Credential credential = new Credential.Builder(email)
.setPassword(password)
.build();
saveCredentials(credential);
}
}
});
}
public void setUpGoogleApiClient() {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.enableAutoManage(this, this)
.build();
}
public void createCredentialRequest() {
mCredentialRequest = new CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.setAccountTypes(IdentityProviders.GOOGLE)
.build();
}
public void requestCredentials() {
Auth.CredentialsApi.request(mGoogleApiClient, mCredentialRequest).setResultCallback(this);
}
private void onCredentialRetrieved(Credential credential) {
String accountType = credential.getAccountType();
if (accountType == null) {
// Sign the user in with information from the Credential.
gotoNext();
} else if (accountType.equals(IdentityProviders.GOOGLE)) {
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.build();
GoogleSignInClient signInClient = GoogleSignIn.getClient(this, gso);
Task<GoogleSignInAccount> task = signInClient.silentSignIn();
task.addOnCompleteListener(new OnCompleteListener<GoogleSignInAccount>() {
@Override
public void onComplete(@NonNull Task<GoogleSignInAccount> task) {
if (task.isSuccessful()) {
// See "Handle successful credential requests"
populateLoginFields(task.getResult().getEmail(), null);
} else {
showToast("Unable to do a google sign in");
}
}
});
}
}
public void gotoNext() {
startActivity(new Intent(this, SecondActivity.class));
finish();
}
public void showToast(String s) {
Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT).show();
}
@Override
public void onConnected(@Nullable Bundle bundle) {
Log.d("API123", "onConnected");
requestCredentials();
}
@Override
public void onConnectionSuspended(int i) {
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
}
@Override
protected void onDestroy() {
mGoogleApiClient.disconnect();
super.onDestroy();
}
@Override
public void onResult(@NonNull CredentialRequestResult credentialRequestResult) {
Status status = credentialRequestResult.getStatus();
if (status.isSuccess()) {
onCredentialRetrieved(credentialRequestResult.getCredential());
} else {
if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) {
try {
isResolving = true;
status.startResolutionForResult(this, RC_READ);
} catch (IntentSender.SendIntentException e) {
Log.d(TAG, e.toString());
}
} else {
showHintDialog();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d(TAG, "onActivityResult");
if (requestCode == RC_READ) {
if (resultCode == RESULT_OK) {
Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY);
onCredentialRetrieved(credential);
} else {
Log.d(TAG, "Request failed");
}
isResolving = false;
}
if (requestCode == RC_HINT) {
if (resultCode == RESULT_OK) {
Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY);
populateLoginFields(credential.getId(), "");
} else {
showToast("Hint dialog closed");
}
}
if (requestCode == RC_SAVE) {
if (resultCode == RESULT_OK) {
Log.d(TAG, "SAVE: OK");
gotoNext();
showToast("Credentials saved");
}
}
}
public void populateLoginFields(String email, String password) {
if (!TextUtils.isEmpty(email))
inEmail.setText(email);
if (!TextUtils.isEmpty(password))
inPassword.setText(password);
}
public void showHintDialog() {
HintRequest hintRequest = new HintRequest.Builder()
.setHintPickerConfig(new CredentialPickerConfig.Builder()
.setShowCancelButton(true)
.build())
.setEmailAddressIdentifierSupported(true)
.setAccountTypes(IdentityProviders.GOOGLE)
.build();
PendingIntent intent = mCredentialsApiClient.getHintPickerIntent(hintRequest);
try {
startIntentSenderForResult(intent.getIntentSender(), RC_HINT, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG, "Could not start hint picker Intent", e);
}
}
public void saveCredentials(Credential credential) {
mCredentialsApiClient.save(credential).addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
Log.d(TAG, "SAVE: OK");
showToast("Credentials saved");
return;
}
Exception e = task.getException();
if (e instanceof ResolvableApiException) {
// Try to resolve the save request. This will prompt the user if
// the credential is new.
ResolvableApiException rae = (ResolvableApiException) e;
try {
rae.startResolutionForResult(MainActivity.this, RC_SAVE);
} catch (IntentSender.SendIntentException f) {
// Could not resolve the request
Log.e(TAG, "Failed to send resolution.", f);
showToast("Saved failed");
}
} else {
// Request has no resolution
showToast("Saved failed");
}
}
});
}
}
In the onConnected method we request for available credentials. That means as soon as the activity is started the credentials if any are retrieved. If a single credential is there, then it would auto sign and goto the next activity. The code for the activity_second.xml layout is given below:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondActivity">
<Button
android:id="@+id/btnDeleteAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Delete account"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnSignOut" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="You are logged in."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnSignOut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="SIGN OUT"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<Button
android:id="@+id/btnSignOutDisableAutoSign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SIGN OUT AND DISABLE AUTO SIGN IN"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnDeleteAccount" />
</android.support.constraint.ConstraintLayout>
Inside the SecondActivity, we’ll do three different actions - Sign out, Sign out and disable auto-sign in the next time, delete credential. The code for the SecondActivity.java class is given below:
package com.journaldev.androidgooglesmartlock;
import android.content.Intent;
import android.content.IntentSender;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.auth.api.credentials.Credential;
import com.google.android.gms.auth.api.credentials.CredentialRequest;
import com.google.android.gms.auth.api.credentials.CredentialRequestResult;
import com.google.android.gms.auth.api.credentials.Credentials;
import com.google.android.gms.auth.api.credentials.CredentialsClient;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
public class SecondActivity extends AppCompatActivity implements View.OnClickListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, ResultCallback<CredentialRequestResult> {
Button btnSignOut, btnSignOutDisableAuto, btnDelete;
private GoogleApiClient mGoogleApiClient;
CredentialsClient mCredentialsApiClient;
CredentialRequest mCredentialRequest;
public static final String TAG = "API123";
private static final int RC_REQUEST = 4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
setUpGoogleApiClient();
mCredentialsApiClient = Credentials.getClient(this);
btnSignOut = findViewById(R.id.btnSignOut);
btnSignOutDisableAuto = findViewById(R.id.btnSignOutDisableAutoSign);
btnDelete = findViewById(R.id.btnDeleteAccount);
btnSignOut.setOnClickListener(this);
btnSignOutDisableAuto.setOnClickListener(this);
btnDelete.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btnSignOut:
signOut(false);
break;
case R.id.btnSignOutDisableAutoSign:
signOut(true);
break;
case R.id.btnDeleteAccount:
requestCredentials();
break;
}
}
@Override
public void onConnected(@Nullable Bundle bundle) {
}
@Override
public void onConnectionSuspended(int i) {
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
}
@Override
public void onResult(@NonNull CredentialRequestResult credentialRequestResult) {
Status status = credentialRequestResult.getStatus();
if (status.isSuccess()) {
onCredentialSuccess(credentialRequestResult.getCredential());
} else {
if (status.hasResolution()) {
try {
status.startResolutionForResult(this, RC_REQUEST);
} catch (IntentSender.SendIntentException e) {
Log.d(TAG, e.toString());
}
} else {
showToast("Request Failed");
}
}
}
public void setUpGoogleApiClient() {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.enableAutoManage(this, this)
.build();
}
private void requestCredentials() {
mCredentialRequest = new CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.build();
Auth.CredentialsApi.request(mGoogleApiClient, mCredentialRequest).setResultCallback(this);
}
@Override
protected void onDestroy() {
mGoogleApiClient.disconnect();
super.onDestroy();
}
private void onCredentialSuccess(Credential credential) {
Auth.CredentialsApi.delete(mGoogleApiClient, credential).setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(@NonNull Status status) {
if (status.isSuccess()) {
signOut(false);
} else {
showToast("Account Deletion Failed");
}
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_REQUEST) {
if (resultCode == RESULT_OK) {
showToast("Deleted");
Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY);
onCredentialSuccess(credential);
} else {
Log.d(TAG, "Request failed");
}
}
}
public void showToast(String s) {
Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT).show();
}
private void signOut(boolean disableAutoSignIn) {
if (disableAutoSignIn)
Auth.CredentialsApi.disableAutoSignIn(mGoogleApiClient);
startActivity(new Intent(this, MainActivity.class));
finish();
}
}
The output of the above application in action is given below: We created the first account and see that everytime you open the app, it automatically signs in. Unless we disable auto-sign. Then it will ask for permission before signing in. We created another account. This time when we delete it. And the application auto-signs into the first account after deletion. That brings an end to this tutorial. You can download the project from the link below:
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
not work on 7.0 and below, work good for 8.0 and above… can you guide me to work on 7.0 and below? thanks.
- Indra