アンドロイドのGoogleスマートロック
このチュートリアルでは、Smart Lock機能について説明し、Androidアプリケーションに実装します。
Google スマートロック
スマートロックは、認証情報を一度保存することで、アプリケーションに自動的にサインインするために使用されます。つまり、しばらくしてアプリケーションを再インストールした場合でも、以前に保存された認証情報をChromeのパスワードから削除しなかった限り、自動的にサインインすることができます。
Google スマートロックは、たった一つのタップでサインインすることができます。
Smart Lockをアプリケーションに統合するためには、Credentials APIを使用する必要があります。Credentials APIを使用すると、ユーザーが以下のことが可能になります。
- Request credentials when opening the app.
- Save credentials from the login form.
- Sync credentials between the app and website.
- Display email hints in case we want to help the user in the sign-in/sign-up process.
あなたのアプリケーションでGoogle Smart Lockを使用するためには、以下の依存関係を追加する必要があります。
dependencies {
implementation 'com.google.android.gms:play-services-auth:16.0.0'
}
SmartLockは、AndroidアプリケーションにおいてGoogleApiClientの設定が必要です。SmartLockは、1つの資格情報の場合に自動でサインインを許可します。複数の資格情報が存在する場合は、ダイアログに表示されます。
以前はSharedPreferencesに依存して、自動的に署名を行い、ローカルに資格情報を保存していました。しかし、Google Smart Lockでは全てGoogleサーバーが管理しています。
以下は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)
Googleアカウントの保存されたすべての認証情報を表示するには、passwords.google.comにアクセスすることができます。Smart Lockを使用したアプリケーションのフローはどのようになりますか?ログイン画面のコードを以下の方法で構築する必要があります。
- Check for Credentials. If a single credential exists, auto-sign or auto-fill the login form.
- If there are more than one credentials, show them in a dialog and let the user choose.
- If there are no saved credentials you can either let the user fill up the form OR make it easier for them by AutoFill or showing a Hint dialog with available accounts to sign in with.
始める
Androidアプリケーションで、スマートロック機能の実装を始めましょう。GoogleApiClientを設定します。
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.enableAutoManage(this, this)
.build();
GoogleApiClientのインターフェースを実装し、メソッドを実装します。Credentials Clientを初期化します。
CredentialsOptions options = new CredentialsOptions.Builder()
.forceEnableSaveDialog()
.build();
CredentialsClient mCredentialsApiClient = Credentials.getClient(this, options);
Android Oreo以降では、forceEnableSaveDialog()が必要です。CredentialRequestを作成してください。
CredentialRequest mCredentialRequest = new CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.setAccountTypes(IdentityProviders.GOOGLE)
.build();
資格情報を取得する
Auth.CredentialsApi.request(mGoogleApiClient, mCredentialRequest).setResultCallback(this);
setResultCallBackは、インターフェースResultCallbackのメソッドonResultをオーバーライドする必要があります。
@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();
}
}
}
以下に、3つのケースがあります。
単一の認証 – 成功 – 複数の認証情報 – 解決し、利用可能なすべての認証情報をダイアログに表示する。
- No Credentials – Show a Hint Dialog with all the available sign in accounts
「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.scdev.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.scdev.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();
}
}
上記のアプリケーションの実行結果は以下の通りです。最初のアカウントを作成し、アプリを開くたびに自動的にサインインすることがわかります。ただし、自動サインインを無効にしない限り、サインインする前に許可が求められます。別のアカウントを作成しました。今度はそれを削除します。そして、削除後にアプリケーションが最初のアカウントに自動的にサインインします。これでチュートリアルは終了です。プロジェクトは以下のリンクからダウンロードできます。
アンドロイドのGoogleスマートロック
以下のGithubプロジェクトリンク