在本教程中,我们将讨论智能锁功能并在 Android 应用程序中实现它。
Smart Lock 用于通过一劳永逸地保存凭据来自动登录您的应用程序。这意味着,如果您在一段时间后重新安装应用程序,您可以使用之前保存的凭据自动登录,前提是您没有从 chrome 密码中删除它们。
Google Smart Lock 让您只需轻轻一按即可登录。
为了将 Smart Lock 集成到您的应用程序中,您需要使用 Credentials API。凭证 API 允许用户:
- 打开应用程序时请求凭据。
- 保存登录表单中的凭据。
- 在应用程序和网站之间同步凭据。
- 如果我们想在登录/注册过程中帮助用户,则显示电子邮件提示。
要在您的应用程序中使用 Google Smart Lock,您需要添加以下依赖项:
dependencies {
implementation 'com.google.android.gms:play-services-auth:16.0.0'
}
SmartLock 需要在您的 Android 应用程序中设置 GoogleApiClient。当只有一个凭据时,SmartLock 允许自动登录。当有多个凭据时,它们会显示在对话框中。
早些时候,我们曾经依赖 SharedPreferences 在本地自动签名和保存凭据。现在,有了 Google Smart Lock,一切都由 Google 服务器处理。
以下是 Credentials API 中存在的主要方法:
save(GoogleApiClient client, Credential credential)
-
request(GoogleApiClient client, CredentialRequestrequest)
- 请求为应用程序保存的所有凭据。
-
getHintPickerIntent(GoogleApiClient client, HintRequest request)
- 显示您拥有的登录帐户列表,以便快速填写登录表单。
disableAutoSignIn(GoogleApiClient client)
delete(GoogleApiClient client, Credential credential)
您可以通过访问查看为 Google 帐户保存的所有凭据密码.google.com. 带有智能锁的应用程序的流程是什么?您需要按以下方式构建登录屏幕代码:
- 检查凭证。如果存在单个凭据,请自动签名或自动填写登录表单。
- 如果有多个凭据,请在对话框中显示它们并让用户选择。
- 如果没有保存的凭据,您可以让用户填写表单,或者通过自动填充或显示带有可用帐户登录的提示对话框来让他们更轻松。
让我们开始在 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);
强制启用保存对话框()Android Oreo 及更高版本需要。创建凭证请求
CredentialRequest mCredentialRequest = new CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.setAccountTypes(IdentityProviders.GOOGLE)
.build();
检索凭证
Auth.CredentialsApi.request(mGoogleApiClient, mCredentialRequest).setResultCallback(this);
The setResultCallBack
需要我们重写该方法onResult
从界面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();
}
}
}
共有三种情况——
单个凭证 - 成功 - 多个凭证 - 解决它们并在对话框中显示所有可用的凭证
状态码为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();
}
}
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:
Android谷歌SmartLock
Github 项目链接