1. 始める前に
この Codelab では、Credential Manager を使用して Android で Google でログインを実装する方法を学びます。
前提条件
学習内容
- Google Cloud プロジェクトを作成する方法
- Google Cloud コンソールで OAuth クライアントを作成する方法
- ボトムシート フローを使用して「Google でログイン」を実装する方法
- ボタンフローを使用して「Google でログイン」を実装する方法
必要なもの
- Android Studio(こちらからダウンロード)
- Android Studio のシステム要件を満たしているパソコン
- Android Emulator のシステム要件を満たすパソコン
- Java と Java Development Kit(JDK)のインストール
2. Android Studio プロジェクトを作成する
所要時間: 3:00 ~ 5:00
まず、Android Studio で新しいプロジェクトを作成する必要があります。
- Android Studio を開く
- [新しいプロジェクト] をクリックします。
- [Phone and Tablet] と [Empty Activity] を選択します。
- [次へ] をクリックします。
- 次に、プロジェクトのいくつかの部分を設定します。
- 名前: プロジェクトの名前
- パッケージ名: プロジェクト名に基づいて自動的に入力されます。
- 保存場所: デフォルトでは、Android Studio がプロジェクトを保存するフォルダに設定されています。この設定はいつでも変更できます。
- Minimum SDK: アプリが実行されるようにビルドされた Android SDK の最小バージョンです。この Codelab では、API 36(Baklava)を使用します。
- [Finish] をクリックします。
- Android Studio がプロジェクトを作成し、ベース アプリケーションに必要な依存関係をダウンロードします。これには数分かかることがあります。この処理を確認するには、ビルドアイコン
をクリックします。
- 完了すると、Android Studio は次のようになります。
3. Google Cloud プロジェクトを設定する
Google Cloud プロジェクトを作成する
- Google Cloud コンソールに移動します。
- プロジェクトを開くか、新しいプロジェクトを作成する
- [API とサービス]
をクリックします。
- OAuth 同意画面に移動します。
- 続行するには、[概要] のフィールドに入力する必要があります。[開始] をクリックして、以下の情報の入力を開始します。
- アプリ名: このアプリの名前。Android Studio でプロジェクトを作成したときに使用した名前と同じにする必要があります。
- ユーザー サポートのメールアドレス: ログインに使用している Google アカウントと、管理している Google グループが表示されます。
- 対象者:
- 組織内でのみ使用されるアプリの場合は「内部」。Google Cloud プロジェクトに関連付けられている組織がない場合は、これを選択できません。
- External を使用します。
- 連絡先情報: アプリケーションの連絡先として使用する任意のメールアドレスを指定できます。
- Google API サービス: ユーザーデータ ポリシーを確認します。
- ユーザーデータに関するポリシーを確認して同意したら、[作成] をクリックします。
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 サービスにアクセスできるのはアプリのみになります。」
- SHA-1 署名とは何ですか?
ウェブ クライアントの場合、コンソールでクライアントを識別するために使用する名前のみが必要です。
Android OAuth 2.0 クライアントを作成する
- [クライアント] ページに移動します。
- [クライアントを作成] をクリックします。
- [アプリケーションの種類] で [Android] を選択します。
- アプリのパッケージ名を指定する必要があります。
- Android Studio からアプリの SHA-1 署名を取得し、ここにコピー&ペーストする必要があります。
- Android Studio に移動してターミナルを開きます。
- 次のコマンドを実行します。Mac/Linux:
Windows:keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
このコマンドは、キーストア内の特定のエントリ(エイリアス)の詳細を一覧表示するように設計されています。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
: 指定されたエイリアスの秘密鍵のパスワードを提供します。
- SHA-1 署名の値をコピーします。
- Google Cloud ウィンドウに戻り、SHA-1 署名値を貼り付けます。
- 画面は次のようになります。作成をクリックします。
ウェブ OAuth 2.0 クライアントを作成する
- ウェブ アプリケーションのクライアント ID を作成するには、Android クライアントの作成セクションの手順 1 ~ 2 を繰り返し、アプリケーションの種類として [ウェブ アプリケーション] を選択します。
- クライアントに名前を付けます(これが OAuth クライアントになります)。
- [作成] をクリックします。
- ポップアップ ウィンドウからクライアント ID をコピーします。これは後で必要になります。
OAuth クライアントの設定が完了したので、Android Studio に戻って「Google でログイン」Android アプリを作成しましょう。
4. Android Virtual Device をセットアップする
実機なしでアプリケーションを迅速にテストするには、Android Studio からアプリをビルドしてすぐに実行できる Android 仮想デバイスを作成します。Android 実機でテストする場合は、Android デベロッパー向けドキュメントの手順に沿って操作してください。
Android Virtual Device を作成する
- Android Studio でデバイス マネージャーを開きます。
- [+] ボタン > [Create Virtual Device] をクリックします。
- ここから、プロジェクトに必要なデバイスを追加できます。この Codelab では、[Medium Phone] を選択し、[次へ] をクリックします。
- デバイスに一意の名前を付けたり、デバイスで実行する Android のバージョンを選択したりして、プロジェクト用にデバイスを構成できるようになりました。API が API 36「Baklava」; Android 16 に設定されていることを確認し、[完了] をクリックします。
- デバイス マネージャーに新しいデバイスが表示されます。デバイスが実行されていることを確認するには、作成したデバイスの横にある
をクリックします。
- デバイスが実行されているはずです。
Android Virtual Device にログインする
作成したデバイスが動作することを確認したら、Google でログインをテストする際にエラーが発生しないように、Google アカウントでデバイスにログインする必要があります。
- [設定] に移動します。
- 仮想デバイスの画面の中央をクリックして上にスワイプします。
- [設定] アプリを探してクリックします。
- [設定]
で [Google] をクリックします。
- [ログイン] をクリックし、画面の指示に沿って Google アカウントにログインします。
- デバイスにログインしているはずです。
これで、仮想 Android デバイスのテストの準備が整いました。
5. 依存関係を追加する
所要時間 5:00
OAuth API 呼び出しを行うには、まず認証リクエストを行い、Google ID を使用してリクエストを行うために必要なライブラリを統合する必要があります。
- libs.googleid
- libs.play.services.auth
- [File] > [Project Structure] に移動します。
- [Dependencies] > [app] > ['+'] > [Library Dependency] に移動します。
- 次に、ライブラリを追加する必要があります。
- 検索ダイアログに「googleid」と入力し、[検索] をクリックします。
- エントリは 1 つだけです。それを選択し、利用可能な最新バージョン(この Codelab の時点では 1.1.1)を選択します。
- [OK] をクリックします。
- 手順 1 ~ 3 を繰り返しますが、代わりに 「play-services-auth」 を検索し、グループ ID が「com.google.android.gms」、アーティファクト名が「play-services-auth」の行を選択します。
- [OK] をクリックします。
6. ボトムシートのフロー
ボトムシート フローは、認証情報マネージャー API を活用して、ユーザーが Android で Google アカウントを使用してアプリにログインするための効率的な方法を提供します。特にリピーターにとって、すばやく簡単に利用できるように設計されています。このフローはアプリの起動時にトリガーされる必要があります。
ログイン リクエストを作成する
- まず、
MainActivity.kt
からGreeting()
関数とGreetingPreview()
関数を削除します。これらは不要になります。 - 次に、このプロジェクトに必要なパッケージがインポートされていることを確認する必要があります。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
- 次に、ボトムシート リクエストを作成する関数を作成する必要があります。このコードを 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
: 取得する認証情報のタイプ(GetCredentialRequest
Google 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 アカウントを持っていないなど)。- 重要なのは、この関数が
e
、NoCredentialException
に保存されている例外を返すことです。これにより、呼び出し元は認証情報が利用できない場合に特定のケースを処理できます。
- 重要なのは、この関数が
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])して実行できます。
- 実行ボタン
を押します。
- エミュレータでアプリを実行すると、ログイン BottomSheet がポップアップ表示されます。[続行] をクリックしてログインをテストします。
- ログインが成功したことを示すトースト メッセージが表示されます。
7. ボタンフロー
[Google でログイン] のボタンフローを使用すると、ユーザーは既存の Google アカウントを使用して Android アプリに簡単に登録またはログインできます。このボタンは、ボトムシートを閉じた場合や、ログインまたは登録に Google アカウントを明示的に使用したい場合に表示されます。開発者にとっては、オンボーディングがスムーズになり、登録時の負担が軽減されます。
これは Jetpack Compose の標準のボタンでも可能ですが、ここでは Google でログインのブランド ガイドライン ページで事前承認済みのブランド アイコンを使用します。
プロジェクトにブランド アイコンを追加する
- 事前承認済みのブランド アイコンの ZIP ファイルをこちらからダウンロードしてください。
- ダウンロードした signin-assest.zip を解凍します(この手順はパソコンのオペレーティング システムによって異なります)。これで、signin-assets フォルダを開いて、利用可能なアイコンを確認できます。この Codelab では、
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png
を使用します。 - ファイルをコピーする
- Android Studio の [res] > [drawable] に貼り付けます。これを行うには、[drawable] フォルダを右クリックして [Paste] をクリックします(表示するには [res] フォルダを開く必要がある場合があります)。
- ダイアログが表示され、ファイルの名前を変更して、追加先のディレクトリを確認するよう求められます。アセットの名前を 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])して実行できます。
- 実行ボタン
を押します。
- エミュレータでアプリが実行されると、BottomSheet が表示されます。ダイアログの外側をクリックして閉じます。
- 作成したボタンがアプリに表示されているはずです。クリックしてログイン ダイアログを表示します。
- アカウントをクリックしてログインします。
8. まとめ
これでこの Codelab は終了です。Android での「Google でログイン」に関する詳細やヘルプについては、以下の「よくある質問」セクションをご覧ください。
よくある質問
- Stackoverflow
- Android 認証情報マネージャーのトラブルシューティング ガイド
- Android 認証情報マネージャーに関するよくある質問
- OAuth アプリの検証に関するヘルプセンター
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
}