アンドロイドの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プロジェクトリンク

コメントを残す 0

Your email address will not be published. Required fields are marked *