Умный замок Google для Android
В этом уроке мы обсудим функцию Smart Lock и реализуем ее в нашем приложении для Android.
Умный замок Google
Smart Lock используется для автоматического входа в ваше приложение путем раз и навсегдаго сохранения учетных данных. Это означает, что если вы переустановите свое приложение через некоторое время, вы сможете автоматически войти в систему с ранее сохраненными учетными данными, если вы не удалили их из паролей Chrome.
Google Smart Lock позволяет войти в систему одним касанием.
Чтобы интегрировать Smart Lock в ваше приложение, вам необходимо использовать Credentials API. Credentials API позволяет пользователю:
- Запрашивать учетные данные при открытии приложения.
- Сохранить учетные данные из формы входа.
- Синхронизируйте учетные данные между приложением и веб-сайтом.
- Отображать подсказки по электронной почте, если мы хотим помочь пользователю в процессе входа/регистрации.
Чтобы использовать Google Smart Lock в вашем приложении, вам нужно добавить следующую зависимость:
dependencies {
implementation 'com.google.android.gms:play-services-auth:16.0.0'
}
SmartLock требует, чтобы GoogleApiClient был настроен в вашем приложении для Android. SmartLock позволяет выполнять автоматический вход при наличии только одного удостоверения. Если учетных данных несколько, они отображаются в диалоговом окне.
Раньше мы зависели от SharedPreferences для автоматической подписи и локального сохранения учетных данных. Теперь с Google Smart Lock обо всем позаботятся серверы Google.
Ниже приведены основные методы, представленные в Credentials API:
сохранить(клиент GoogleApiClient, учетные данные)
request(клиент GoogleApiClient, CredentialRequestrequest)
— запрашивает все учетные данные, сохраненные для приложения.getHintPickerIntent (клиент GoogleApiClient, запрос HintRequest)
– показывает список учетных записей, в которые вы вошли, чтобы быстро заполнять формы входа.отключитьAutoSignIn(клиент GoogleApiClient)
удалить(клиент GoogleApiClient, учетные данные)
Вы можете просмотреть все учетные данные, сохраненные для учетной записи Google, перейдя на passwords.google.com. Какой поток для приложения с Smart Lock? Вам необходимо структурировать код экрана входа в систему следующим образом:
- Проверьте учетные данные. Если существуют одни учетные данные, автоматически подпишите или заполните форму входа.
- Если учетных данных несколько, покажите их в диалоговом окне и предоставьте пользователю возможность выбора.
- Если сохраненных учетных данных нет, вы можете позволить пользователю заполнить форму ИЛИ упростить его с помощью автозаполнения или отображения диалогового окна подсказки с доступными учетными записями для входа.
Начиная
Давайте приступим к реализации функции Smart Lock в приложении для Android. Настроить GoogleApiClient
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.enableAutoManage(this, this)
.build();
Реализуйте интерфейсы GoogleApiClient и реализуйте методы. Инициализировать клиент учетных данных
CredentialsOptions options = new CredentialsOptions.Builder()
.forceEnableSaveDialog()
.build();
CredentialsClient mCredentialsApiClient = Credentials.getClient(this, options);
forceEnableSaveDialog() требуется для Android Oreo и выше. Создать учетные данные
CredentialRequest mCredentialRequest = new CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.setAccountTypes(IdentityProviders.GOOGLE)
.build();
Получить учетные данные
Auth.CredentialsApi.request(mGoogleApiClient, mCredentialRequest).setResultCallback(this);
setResultCallBack
требует, чтобы мы переопределили метод onResult
из интерфейса ResultCallback
@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();
}
}
}
Есть три случая -
Одиночные учетные данные - Успех - Несколько учетных данных - Разрешите их и отобразите все доступные учетные данные в диалоговом окне.
- Нет учетных данных – показать диалоговое окно подсказок со всеми доступными учетными записями для входа
Код состояния RESOLUTION_REQUIRED
означает, что необходимо разрешить несколько учетных данных. Для этого мы вызываем startResolutionForResult
, который возвращает результат в методе onActivityResult. Мы используем логический флаг, чтобы предотвратить множественные разрешения. Это приведет к созданию нескольких диалогов. Теперь, когда мы увидели суть функции SmartLock, давайте полностью реализуем ее с функциями сохранения и удаления учетных данных.
Структура проекта
Код
Код макета activity_main.xml приведен ниже:
<?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>
используется для отключения автозаполнения в полях EditText. Мы обсудим API автозаполнения в отдельном руководстве. Код для класса MainActivity.java приведен ниже:
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");
}
}
});
}
}
В методе onConnected мы запрашиваем доступные учетные данные. Это означает, что как только действие запускается, учетные данные, если таковые имеются, извлекаются. Если есть одно удостоверение, оно автоматически подпишется и перейдет к следующему действию. Код макета activity_second.xml приведен ниже:
<?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>
Внутри SecondActivity мы выполним три разных действия — выход из системы, выход из системы и отключение автоматического входа в следующий раз, удаление учетных данных. Код для класса SecondActivity.java приведен ниже:
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();
}
}
AndroidGoogleSmartLock
Ссылка на проект на гитхабе