Android アプリに「Google でログイン」を実装する方法

1. 始める前に

この Codelab では、Credential Manager を使用して Android で Google でログインを実装する方法を学びます。

前提条件

  • Android 開発で Kotlin を使用するための基礎知識
  • Jetpack Compose に関する基礎知識(詳細については、こちらをご覧ください)

学習内容

  • Google Cloud プロジェクトを作成する方法
  • Google Cloud コンソールで OAuth クライアントを作成する方法
  • ボトムシート フローを使用して「Google でログイン」を実装する方法
  • ボタンフローを使用して「Google でログイン」を実装する方法

必要なもの

2. Android Studio プロジェクトを作成する

所要時間: 3:00 ~ 5:00

まず、Android Studio で新しいプロジェクトを作成する必要があります。

  1. Android Studio を開く
  2. [新しいプロジェクト] をクリックします。Android Studio のウェルカム画面
  3. [Phone and Tablet] と [Empty Activity] を選択します。Android Studio Project
  4. [次へ] をクリックします。
  5. 次に、プロジェクトのいくつかの部分を設定します。
    • 名前: プロジェクトの名前
    • パッケージ名: プロジェクト名に基づいて自動的に入力されます。
    • 保存場所: デフォルトでは、Android Studio がプロジェクトを保存するフォルダに設定されています。この設定はいつでも変更できます。
    • Minimum SDK: アプリが実行されるようにビルドされた Android SDK の最小バージョンです。この Codelab では、API 36(Baklava)を使用します。
    Android Studio セットアップ プロジェクト
  6. [Finish] をクリックします。
  7. Android Studio がプロジェクトを作成し、ベース アプリケーションに必要な依存関係をダウンロードします。これには数分かかることがあります。この処理を確認するには、ビルドアイコン Android Studio プロジェクトのビルド をクリックします。
  8. 完了すると、Android Studio は次のようになります。Android Studio プロジェクトのビルド

3. Google Cloud プロジェクトを設定する

Google Cloud プロジェクトを作成する

  1. Google Cloud コンソールに移動します。
  2. プロジェクトを開くか、新しいプロジェクトを作成するGCP で新しいプロジェクトを作成するGCP で新しいプロジェクトを作成する 2GCP で新しいプロジェクトを作成する 3
  3. [API とサービス] GCP API とサービス をクリックします。
  4. OAuth 同意画面に移動します。GCP OAuth 同意画面
  5. 続行するには、[概要] のフィールドに入力する必要があります。[開始] をクリックして、以下の情報の入力を開始します。GCP の [始める] ボタン
    • アプリ名: このアプリの名前。Android Studio でプロジェクトを作成したときに使用した名前と同じにする必要があります。
    • ユーザー サポートのメールアドレス: ログインに使用している Google アカウントと、管理している Google グループが表示されます。
    GCP アプリ情報
    • 対象者:
      • 組織内でのみ使用されるアプリの場合は「内部」。Google Cloud プロジェクトに関連付けられている組織がない場合は、これを選択できません。
      • External を使用します。
    GCP オーディエンス
    • 連絡先情報: アプリケーションの連絡先として使用する任意のメールアドレスを指定できます。
    GCP の連絡先情報
    • Google API サービス: ユーザーデータ ポリシーを確認します。
  6. ユーザーデータに関するポリシーを確認して同意したら、[作成] をクリックします。GCP Create

OAuth クライアントを設定する

Google Cloud プロジェクトが設定されたので、ウェブ クライアントと Android クライアントを追加して、クライアント ID を使用して OAuth バックエンド サーバーに API 呼び出しを行えるようにする必要があります。

Android ウェブ クライアントの場合、次のものが必要です。

  • アプリのパッケージ名(例: com.example.example)
  • アプリの SHA-1 署名
    • SHA-1 署名とは何ですか?
      • SHA-1 フィンガープリントは、アプリの署名鍵から生成される暗号ハッシュです。これは、特定のアプリの署名証明書の一意の識別子として機能します。アプリのデジタル「署名」のようなものとお考えください。
    • SHA-1 署名が必要な理由
      • SHA-1 フィンガープリントにより、特定の署名鍵で署名されたアプリのみが OAuth 2.0 クライアント ID を使用してアクセス トークンをリクエストできるようになり、他のアプリ(同じパッケージ名を持つアプリを含む)がプロジェクトのリソースとユーザーデータにアクセスすることを防ぎます。
      • 次のように考えてください。
        • アプリの署名鍵は、アプリの「ドア」の物理的な鍵のようなものです。アプリの内部動作へのアクセスを許可するものです。
        • SHA-1 フィンガープリントは、物理キーにリンクされた一意のキーカード ID のようなものです。これは、特定のキーを識別する特定のコードです。
        • OAuth 2.0 クライアント ID は、特定の Google リソースまたはサービス(Google ログインなど)へのエントリ コードのようなものです。
        • OAuth クライアントの設定時に SHA-1 フィンガープリントを指定すると、Google に対して「この特定の ID(SHA-1)のキーカードのみがこのアクセスコード(クライアント ID)を開くことができる」と伝えることになります。これにより、そのエントリ コードにリンクされている Google サービスにアクセスできるのはアプリのみになります。」

ウェブ クライアントの場合、コンソールでクライアントを識別するために使用する名前のみが必要です。

Android OAuth 2.0 クライアントを作成する

  1. [クライアント] ページに移動します。GCP クライアント
  2. [クライアントを作成] をクリックします。GCP クライアントの作成
  3. [アプリケーションの種類] で [Android] を選択します。
  4. アプリのパッケージ名を指定する必要があります。
  5. Android Studio からアプリの SHA-1 署名を取得し、ここにコピー&ペーストする必要があります。
    1. Android Studio に移動してターミナルを開きます。
    2. 次のコマンドを実行します。Mac/Linux:
      keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
      
      Windows:
        keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
      
      このコマンドは、キーストア内の特定のエントリ(エイリアス)の詳細を一覧表示するように設計されています。
      • -list: このオプションは、キーストアの内容を一覧表示するよう keytool に指示します。
      • -v: このオプションは詳細出力を有効にし、エントリに関する詳細情報を提供します。
      • -keystore ~/.android/debug.keystore: キーストア ファイルのパスを指定します。
      • -alias androiddebugkey: 検査する鍵のエイリアス(エントリ名)を指定します。
      • -storepass android: キーストア ファイルのパスワードを指定します。
      • -keypass android: 指定されたエイリアスの秘密鍵のパスワードを提供します。
    3. SHA-1 署名の値をコピーします。
    SHA 署名
    1. Google Cloud ウィンドウに戻り、SHA-1 署名値を貼り付けます。
  6. 画面は次のようになります。作成をクリックします。Android クライアントの詳細Android クライアント

ウェブ OAuth 2.0 クライアントを作成する

  1. ウェブ アプリケーションのクライアント ID を作成するには、Android クライアントの作成セクションの手順 1 ~ 2 を繰り返し、アプリケーションの種類として [ウェブ アプリケーション] を選択します。
  2. クライアントに名前を付けます(これが OAuth クライアントになります)。 ウェブ クライアントの詳細
  3. [作成] をクリックします。ウェブ クライアント
  4. ポップアップ ウィンドウからクライアント ID をコピーします。これは後で必要になります。クライアント ID のコピー

OAuth クライアントの設定が完了したので、Android Studio に戻って「Google でログイン」Android アプリを作成しましょう。

4. Android Virtual Device をセットアップする

実機なしでアプリケーションを迅速にテストするには、Android Studio からアプリをビルドしてすぐに実行できる Android 仮想デバイスを作成します。Android 実機でテストする場合は、Android デベロッパー向けドキュメントの手順に沿って操作してください。

Android Virtual Device を作成する

  1. Android Studio でデバイス マネージャーを開きます。デバイス マネージャー
  2. [+] ボタン > [Create Virtual Device] をクリックします。仮想デバイスを作成する
  3. ここから、プロジェクトに必要なデバイスを追加できます。この Codelab では、[Medium Phone] を選択し、[次へ] をクリックします。Medium Phone
  4. デバイスに一意の名前を付けたり、デバイスで実行する Android のバージョンを選択したりして、プロジェクト用にデバイスを構成できるようになりました。API が API 36「Baklava」; Android 16 に設定されていることを確認し、[完了] をクリックします。仮想デバイスを構成する
  5. デバイス マネージャーに新しいデバイスが表示されます。デバイスが実行されていることを確認するには、作成したデバイスの横にある デバイスの実行 をクリックします。デバイス 2 を実行
  6. デバイスが実行されているはずです。実行中のデバイス

Android Virtual Device にログインする

作成したデバイスが動作することを確認したら、Google でログインをテストする際にエラーが発生しないように、Google アカウントでデバイスにログインする必要があります。

  1. [設定] に移動します。
    1. 仮想デバイスの画面の中央をクリックして上にスワイプします。
    クリックとスワイプ
    1. [設定] アプリを探してクリックします。
    設定アプリ
  2. [設定] Google サービスと設定 で [Google] をクリックします。
  3. [ログイン] をクリックし、画面の指示に沿って Google アカウントにログインします。デバイスへのログイン
  1. デバイスにログインしているはずです。デバイスにログインしました

これで、仮想 Android デバイスのテストの準備が整いました。

5. 依存関係を追加する

所要時間 5:00

OAuth API 呼び出しを行うには、まず認証リクエストを行い、Google ID を使用してリクエストを行うために必要なライブラリを統合する必要があります。

  • libs.googleid
  • libs.play.services.auth
  1. [File] > [Project Structure] に移動します。プロジェクト構造
  2. [Dependencies] > [app] > ['+'] > [Library Dependency] に移動します。依存関係
  3. 次に、ライブラリを追加する必要があります。
    1. 検索ダイアログに「googleid」と入力し、[検索] をクリックします。
    2. エントリは 1 つだけです。それを選択し、利用可能な最新バージョン(この Codelab の時点では 1.1.1)を選択します。
    3. [OK] をクリックします。Google ID Package
    4. 手順 1 ~ 3 を繰り返しますが、代わりに 「play-services-auth」 を検索し、グループ ID が「com.google.android.gms」、アーティファクト名が「play-services-auth」の行を選択します。Play 開発者サービス認証
  4. [OK] をクリックします。完了した依存関係

6. ボトムシートのフロー

ボトムシートのフロー

ボトムシート フローは、認証情報マネージャー API を活用して、ユーザーが Android で Google アカウントを使用してアプリにログインするための効率的な方法を提供します。特にリピーターにとって、すばやく簡単に利用できるように設計されています。このフローはアプリの起動時にトリガーされる必要があります。

ログイン リクエストを作成する

  1. まず、MainActivity.kt から Greeting() 関数と GreetingPreview() 関数を削除します。これらは不要になります。
  2. 次に、このプロジェクトに必要なパッケージがインポートされていることを確認する必要があります。3 行目から始まる既存のステートメントの後に、次の import ステートメントを追加します。
    import android.content.ContentValues.TAG
    import android.content.Context
    import android.credentials.GetCredentialException
    import android.os.Build
    import android.util.Log
    import android.widget.Toast
    import androidx.annotation.RequiresApi
    import androidx.compose.foundation.clickable
    import androidx.compose.foundation.Image
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Surface
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.res.painterResource
    import androidx.credentials.CredentialManager
    import androidx.credentials.exceptions.GetCredentialCancellationException
    import androidx.credentials.exceptions.GetCredentialCustomException
    import androidx.credentials.exceptions.NoCredentialException
    import androidx.credentials.GetCredentialRequest
    import com.google.android.libraries.identity.googleid.GetGoogleIdOption
    import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
    import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
    import java.security.SecureRandom
    import java.util.Base64
    import kotlinx.coroutines.CoroutineScope
    import androidx.compose.runtime.LaunchedEffect
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    
  3. 次に、ボトムシート リクエストを作成する関数を作成する必要があります。このコードを MainActivity クラスの下に貼り付けます。
   //This line is not needed for the project to build, but you will see errors if it is not present.
   //This code will not work on Android versions < UpsideDownCake
   @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
   @Composable
    fun BottomSheet(webClientId: String) {
        val context = LocalContext.current

        // LaunchedEffect is used to run a suspend function when the composable is first launched.
        LaunchedEffect(Unit) {
            // Create a Google ID option with filtering by authorized accounts enabled.
            val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
                .setFilterByAuthorizedAccounts(true)
                .setServerClientId(webClientId)
                .setNonce(generateSecureRandomNonce())
                .build()

            // Create a credential request with the Google ID option.
            val request: GetCredentialRequest = GetCredentialRequest.Builder()
                .addCredentialOption(googleIdOption)
                .build()

            // Attempt to sign in with the created request using an authorized account
            val e = signIn(request, context)
            // If the sign-in fails with NoCredentialException,  there are no authorized accounts.
            // In this case, we attempt to sign in again with filtering disabled.
            if (e is NoCredentialException) {
                val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
                    .setFilterByAuthorizedAccounts(false)
                    .setServerClientId(webClientId)
                    .setNonce(generateSecureRandomNonce())
                    .build()

                val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
                    .addCredentialOption(googleIdOptionFalse)
                    .build()

                //We will build out this function in a moment
                signIn(requestFalse, context)
            }
        }
    }

   //This function is used to generate a secure nonce to pass in with our request
   fun generateSecureRandomNonce(byteLength: Int = 32): String {
      val randomBytes = ByteArray(byteLength)
      SecureRandom.getInstanceStrong().nextBytes(randomBytes)
      return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
   }

このコードの処理内容を詳しく見ていきましょう。

fun BottomSheet(webClientId: String) {...}: BottomSheet という関数を作成します。この関数は webClientid という文字列引数を 1 つ取ります。

  • val context = LocalContext.current: 現在の Android コンテキストを取得します。これは、UI コンポーネントの起動など、さまざまなオペレーションで必要になります。
  • LaunchedEffect(Unit) { ... }: LaunchedEffect は、コンポーザブルのライフサイクル内で suspend 関数(実行を一時停止して再開できる関数)を実行できる Jetpack Compose コンポーザブルです。キーとして Unit を使用すると、このエフェクトはコンポーザブルが最初に起動されたときに 1 回だけ実行されます。
    • val googleIdOption: GetGoogleIdOption = ...: GetGoogleIdOption オブジェクトを作成します。このオブジェクトは、Google にリクエストする認証情報のタイプを設定します。
      • .Builder(): ビルダー パターンを使用してオプションを構成します。
      • .setFilterByAuthorizedAccounts(true): ユーザーがすべての Google アカウントから選択できるようにするか、アプリをすでに承認しているアカウントのみから選択できるようにするかを指定します。この場合は true に設定されているため、ユーザーがこのアプリでの使用を以前に承認した認証情報が利用可能な場合は、その認証情報を使用してリクエストが行われます。
      • .setServerClientId(webClientId): サーバー クライアント ID を設定します。これは、アプリのバックエンドの一意の識別子です。これは ID トークンを取得するために必要です。
      • .setNonce(generateSecureRandomNonce()): リプレイ攻撃を防ぎ、ID トークンが特定のリクエストに関連付けられるように、ノンス(ランダムな値)を設定します。
      • .build(): 指定された構成で GetGoogleIdOption オブジェクトを作成します。
    • val request: GetCredentialRequest = ...: GetCredentialRequest オブジェクトを作成します。このオブジェクトは、認証情報リクエスト全体をカプセル化します。
      • .Builder(): リクエストを構成するビルダー パターンを開始します。
      • .addCredentialOption(googleIdOption): リクエストに googleIdOption を追加し、Google ID トークンをリクエストすることを指定します。
      • .build(): GetCredentialRequest オブジェクトを作成します。
    • val e = signIn(request, context): 作成されたリクエストと現在のコンテキストを使用して、ユーザーのログインを試みます。signIn 関数の結果は e に保存されます。この変数には、成功した結果または例外のいずれかが含まれます。
    • if (e is NoCredentialException) { ... }: これは条件付きチェックです。signIn 関数が NoCredentialException で失敗した場合、以前に承認されたアカウントが利用できないことを意味します。
      • val googleIdOptionFalse: GetGoogleIdOption = ...: 前の signIn が失敗した場合、この部分で新しい GetGoogleIdOption が作成されます。
      • .setFilterByAuthorizedAccounts(false): これは最初のオプションとの重要な違いです。これにより、承認済みアカウントのフィルタリングが無効になり、デバイス上の任意の Google アカウントを使用してログインできるようになります。
      • val requestFalse: GetCredentialRequest = ...: googleIdOptionFalse を使用して新しい GetCredentialRequest が作成されます。
      • signIn(requestFalse, context): 任意のアカウントを使用できる新しいリクエストでユーザーのログインを試みます。

このコードは、提供された構成を使用して、ユーザーの Google ID トークンを取得するための Credential Manager API へのリクエストを準備します。GetCredentialRequest を使用して認証情報マネージャー UI を起動し、ユーザーが Google アカウントを選択して必要な権限を付与できるようにします。

fun generateSecureRandomNonce(byteLength: Int = 32): String: generateSecureRandomNonce という名前の関数を定義します。このメソッドは、ノンスの長さ(バイト単位)を指定する整数引数 byteLength(デフォルト値は 32)を受け取ります。ランダム バイトの Base64 エンコード表現である String を返します。

  • val randomBytes = ByteArray(byteLength): 指定された byteLength のバイト配列を作成して、ランダム バイトを保持します。
  • SecureRandom.getInstanceStrong().nextBytes(randomBytes):
    • SecureRandom.getInstanceStrong(): 暗号学的に強力な乱数生成ツールを取得します。これは、生成された数値が予測不可能な真の乱数であることを保証するため、セキュリティ上重要です。システムで利用可能な最も強力なエントロピー ソースを使用します。
    • .nextBytes(randomBytes): SecureRandom インスタンスによって生成されたランダムバイトで randomBytes 配列を設定します。
  • return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes):
    • Base64.getUrlEncoder(): URL セーフなアルファベット(+ と / の代わりに - と _ を使用)を使用する Base64 エンコーダを取得します。これは、結果の文字列をさらにエンコードすることなく URL で安全に使用できるようにするために重要です。
    • .withoutPadding(): Base64 でエンコードされた文字列からパディング文字を削除します。多くの場合、ノンスを少し短くしてコンパクトにするために望ましいことです。
    • .encodeToString(randomBytes): これにより、randomBytes が Base64 文字列にエンコードされて返されます。

要約すると、この関数は指定された長さの暗号的に強力なランダム ノンスを生成し、URL セーフな Base64 を使用してエンコードし、結果の文字列を返します。これは、セキュリティが重要なコンテキストで使用しても安全なノンスを生成するための標準的な方法です。

ログイン リクエストを行う

ログイン リクエストを作成できるようになったので、認証情報マネージャーを使用してログインに使用できます。そのためには、Credential Manager を使用してログイン リクエストを渡す処理を行い、発生する可能性のある一般的な例外を処理する関数を作成する必要があります。

この関数を BottomSheet() 関数の下に貼り付けると、この処理を実現できます。

//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
    val credentialManager = CredentialManager.create(context)
    val failureMessage = "Sign in failed!"
    var e: Exception? = null
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    try {
        // The getCredential is called to request a credential from Credential Manager.
        val result = credentialManager.getCredential(
            request = request,
            context = context,
        )
        Log.i(TAG, result.toString())

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")

    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)

    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)

    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        return e

    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)

    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
    }
    return e
}

コードの動作を詳しく見てみましょう。

suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?: signIn という名前の suspend 関数を定義します。つまり、メインスレッドをブロックせずに一時停止と再開が可能です。ログインに成功した場合は Exception? を返し、ログインに失敗した場合は特定の例外を返します。

この関数は 2 つのパラメータを取ります。

  • request: 取得する認証情報のタイプ(GetCredentialRequestGoogle ID)。
  • context: システムとやり取りするために必要な Android コンテキスト。

関数の本文の場合:

  • val credentialManager = CredentialManager.create(context): CredentialManager のインスタンスを作成します。これは、Credential Manager API とやり取りするためのメイン インターフェースです。アプリは、この方法でログインフローを開始します。
  • val failureMessage = "Sign in failed!": ログインに失敗したときにトーストに表示される文字列(failureMessage)を定義します。
  • var e: Exception? = null: この行は、プロセス中に発生する可能性のある例外を格納する変数 e を null で初期化します。
  • delay(250): 250 ミリ秒の遅延を発生させます。これは、アプリの起動時に NoCredentialException がすぐにスローされる可能性がある問題(特に BottomSheet フローを使用している場合)の回避策です。これにより、システムが認証情報マネージャーを初期化する時間を確保できます。
  • try { ... } catch (e: Exception) { ... }:堅牢なエラー処理のために try-catch ブロックが使用されます。これにより、ログイン プロセス中にエラーが発生してもアプリがクラッシュせず、例外を適切に処理できます。
    • val result = credentialManager.getCredential(request = request, context = context): ここで Credential Manager API への実際の呼び出しが行われ、認証情報の取得プロセスが開始されます。リクエストとコンテキストを入力として受け取り、認証情報を選択するための UI をユーザーに表示します。成功すると、選択した認証情報を含む結果が返されます。このオペレーションの結果である GetCredentialResponse は、result 変数に格納されます。
    • Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show():ログインが成功したことを示す短いトースト メッセージを表示します。
    • Log.i(TAG, "Sign in Successful!"): 楽しい成功メッセージを logcat に記録します。
    • catch (e: GetCredentialException): GetCredentialException 型の例外を処理します。これは、認証情報の取得プロセス中に発生する可能性のあるいくつかの特定例外の親クラスです。
    • catch (e: GoogleIdTokenParsingException): Google ID トークンの解析中にエラーが発生した場合に例外を処理します。
    • catch (e: NoCredentialException): ユーザーの認証情報がない場合にスローされる NoCredentialException を処理します(認証情報を保存していない、Google アカウントを持っていないなど)。
      • 重要なのは、この関数が eNoCredentialException に保存されている例外を返すことです。これにより、呼び出し元は認証情報が利用できない場合に特定のケースを処理できます。
    • catch (e: GetCredentialCustomException): 認証情報プロバイダによってスローされる可能性のあるカスタム例外を処理します。
    • catch (e: GetCredentialCancellationException): ユーザーがログイン プロセスをキャンセルしたときにスローされる GetCredentialCancellationException を処理します。
    • Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show(): failureMessage を使用して、ログインに失敗したことを示すトースト メッセージを表示します。
    • Log.e(TAG, "", e): エラーに使用される Log.e を使用して、例外を Android logcat に記録します。これには、デバッグに役立つ例外のスタック トレースが含まれます。また、楽しい怒りの絵文字も含まれています。
  • return e: 例外がキャッチされた場合は例外を返し、ログインが成功した場合は null を返します。

要約すると、このコードは、認証情報マネージャー API を使用してユーザーのログインを処理し、非同期オペレーションを管理し、発生する可能性のあるエラーを処理し、エラー処理にユーモアを加えながらトーストとログを通じてユーザーにフィードバックを提供する方法を提供します。

アプリにボトムシート フローを実装する

次のコードと、Google Cloud コンソールからコピーしたウェブ アプリケーション クライアント ID を使用して、MainActivity クラスで BottomSheet フローをトリガーする呼び出しを設定できるようになりました。

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    //This will trigger on launch
                    BottomSheet(webClientId)
                }
            }
        }
    }
}

これで、プロジェクトを保存([File] > [Save])して実行できます。

  1. 実行ボタン プロジェクトを実行する を押します。
  2. エミュレータでアプリを実行すると、ログイン BottomSheet がポップアップ表示されます。[続行] をクリックしてログインをテストします。ボトムシート
  3. ログインが成功したことを示すトースト メッセージが表示されます。ボトムシートの成功

7. ボタンフロー

ボタンフローの GIF

[Google でログイン] のボタンフローを使用すると、ユーザーは既存の Google アカウントを使用して Android アプリに簡単に登録またはログインできます。このボタンは、ボトムシートを閉じた場合や、ログインまたは登録に Google アカウントを明示的に使用したい場合に表示されます。開発者にとっては、オンボーディングがスムーズになり、登録時の負担が軽減されます。

これは Jetpack Compose の標準のボタンでも可能ですが、ここでは Google でログインのブランド ガイドライン ページで事前承認済みのブランド アイコンを使用します。

プロジェクトにブランド アイコンを追加する

  1. 事前承認済みのブランド アイコンの ZIP ファイルをこちらからダウンロードしてください。
  2. ダウンロードした signin-assest.zip を解凍します(この手順はパソコンのオペレーティング システムによって異なります)。これで、signin-assets フォルダを開いて、利用可能なアイコンを確認できます。この Codelab では、signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png を使用します。
  3. ファイルをコピーする
  4. Android Studio の [res] > [drawable] に貼り付けます。これを行うには、[drawable] フォルダを右クリックして [Paste] をクリックします(表示するには [res] フォルダを開く必要がある場合があります)。ドローアブル
  5. ダイアログが表示され、ファイルの名前を変更して、追加先のディレクトリを確認するよう求められます。アセットの名前を siwg_button.png に変更し、[OK] をクリックします。ボタンを追加する

ボタンフローのコード

このコードは BottomSheet() に使用されるのと同じ signIn() 関数を使用しますが、このフローではログイン オプションを表示するためにデバイスに保存された認証情報とパスキーを利用しないため、GetGoogleIdOption の代わりに GetSignInWithGoogleOption を使用します。BottomSheet() 関数の下に貼り付けることができるコードは次のとおりです。

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()

    val onClick: () -> Unit = {
        val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
            .Builder(serverClientId = webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(signInWithGoogleOption)
            .build()

        coroutineScope.launch {
            signIn(request, context)
        }
    }
    Image(
        painter = painterResource(id = R.drawable.siwg_button),
        contentDescription = "",
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = true, onClick = onClick)
    )
}

コードの動作を分解すると、次のようになります。

fun ButtonUI(webClientId: String): webClientId(Google Cloud プロジェクトのクライアント ID)を引数として受け取る ButtonUI という名前の関数を宣言します。

val context = LocalContext.current: 現在の Android コンテキストを取得します。これは、UI コンポーネントの起動など、さまざまなオペレーションで必要になります。

val coroutineScope = rememberCoroutineScope(): コルーチン スコープを作成します。これは非同期タスクの管理に使用され、メインスレッドをブロックせずにコードを実行できます。rememberCoroutineScope() は、コンポーザブルのライフサイクルに結び付けられたスコープを提供する Jetpack Compose のコンポーザブル関数です。

val onClick: () -> Unit = { ... }: ボタンがクリックされたときに実行されるラムダ関数を作成します。このラムダ関数は次の処理を行います。

  • val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build(): この部分は GetSignInWithGoogleOption オブジェクトを作成します。このオブジェクトは、「Google でログイン」プロセスのパラメータを指定するために使用されます。webClientId と nonce(セキュリティに使用されるランダムな文字列)が必要です。
  • val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build(): GetCredentialRequest オブジェクトをビルドします。このリクエストは、Credential Manager を使用してユーザーの認証情報を取得するために使用されます。GetCredentialRequest は、以前に作成した GetSignInWithGoogleOption をオプションとして追加し、「Google でログイン」認証情報をリクエストします。
  • coroutineScope.launch { ... }: 非同期オペレーション(コルーチンを使用)を管理する CoroutineScope
    • signIn(request, context): 前に定義した signIn() 関数を呼び出します。

Image(...): 画像 R.drawable.siwg_button を読み込む painterResource を使用して画像をレンダリングします。

  • Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick):
    • fillMaxSize(): 画像が利用可能なスペースを埋めるようにします。
    • clickable(enabled = true, onClick = onClick): 画像をクリック可能にします。クリックすると、以前に定義した onClick ラムダ関数が実行されます。

まとめると、このコードは Jetpack Compose UI に [Google でログイン] ボタンを設定します。ボタンがクリックされると、認証情報リクエストが準備され、認証情報マネージャーが起動して、ユーザーが Google アカウントでログインできるようになります。

次に、ButtonUI() 関数を実行するように MainActivity クラスを更新する必要があります。

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally

                    ) {
                        //This will trigger on launch
                        BottomSheet(webClientId)

                        //This requires the user to press the button
                        ButtonUI(webClientId)
                    }
                }
            }
        }
    }
}

これで、プロジェクトを保存([File] > [Save])して実行できます。

  1. 実行ボタン プロジェクトを実行する を押します。
  2. エミュレータでアプリが実行されると、BottomSheet が表示されます。ダイアログの外側をクリックして閉じます。ここをタップ
  3. 作成したボタンがアプリに表示されているはずです。クリックしてログイン ダイアログを表示します。ログイン ダイアログ
  4. アカウントをクリックしてログインします。

8. まとめ

これでこの Codelab は終了です。Android での「Google でログイン」に関する詳細やヘルプについては、以下の「よくある質問」セクションをご覧ください。

よくある質問

MainActivity.kt の完全なコード

参考までに、MainActivity.kt のコード全体を次に示します。

package com.example.example

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.credentials.GetCredentialException
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally

                    ) {
                        //This will trigger on launch
                        BottomSheet(webClientId)

                        //This requires the user to press the button
                        ButtonUI(webClientId)
                    }
                }
            }
        }
    }
}

//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
    fun BottomSheet(webClientId: String) {
        val context = LocalContext.current

        // LaunchedEffect is used to run a suspend function when the composable is first launched.
        LaunchedEffect(Unit) {
            // Create a Google ID option with filtering by authorized accounts enabled.
            val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
                .setFilterByAuthorizedAccounts(true)
                .setServerClientId(webClientId)
                .setNonce(generateSecureRandomNonce())
                .build()

            // Create a credential request with the Google ID option.
            val request: GetCredentialRequest = GetCredentialRequest.Builder()
                .addCredentialOption(googleIdOption)
                .build()

            // Attempt to sign in with the created request using an authorized account
            val e = signIn(request, context)
            // If the sign-in fails with NoCredentialException,  there are no authorized accounts.
            // In this case, we attempt to sign in again with filtering disabled.
            if (e is NoCredentialException) {
                val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
                    .setFilterByAuthorizedAccounts(false)
                    .setServerClientId(webClientId)
                    .setNonce(generateSecureRandomNonce())
                    .build()

                val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
                    .addCredentialOption(googleIdOptionFalse)
                    .build()

                signIn(requestFalse, context)
            }
        }
    }

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()

    val onClick: () -> Unit = {
        val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
            .Builder(serverClientId = webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(signInWithGoogleOption)
            .build()

        signIn(coroutineScope, request, context)
    }
    Image(
        painter = painterResource(id = R.drawable.siwg_button),
        contentDescription = "",
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = true, onClick = onClick)
    )
}

fun generateSecureRandomNonce(byteLength: Int = 32): String {
    val randomBytes = ByteArray(byteLength)
    SecureRandom.getInstanceStrong().nextBytes(randomBytes)
    return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}

//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
    val credentialManager = CredentialManager.create(context)
    val failureMessage = "Sign in failed!"
    var e: Exception? = null
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    try {
        // The getCredential is called to request a credential from Credential Manager.
        val result = credentialManager.getCredential(
            request = request,
            context = context,
        )
        Log.i(TAG, result.toString())

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")

    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)

    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)

    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        return e

    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)

    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
    }
    return e
}