איך מטמיעים את התכונה 'כניסה באמצעות חשבון Google' באפליקציית Android

1. לפני שמתחילים

ב-codelab הזה תלמדו איך להטמיע את התכונה 'כניסה באמצעות חשבון Google' ב-Android באמצעות Credential Manager.

דרישות מוקדמות

  • הבנה בסיסית של השימוש ב-Kotlin לפיתוח ל-Android
  • הבנה בסיסית של Jetpack Compose (מידע נוסף זמין כאן)

מה תלמדו

  • איך יוצרים פרויקט ב-Google Cloud
  • איך יוצרים לקוחות OAuth ב-Google Cloud Console
  • איך מטמיעים את התכונה 'כניסה באמצעות חשבון Google' באמצעות תהליך Bottom Sheet
  • איך מטמיעים את התכונה 'כניסה באמצעות חשבון Google' באמצעות תהליך הכפתור

מה צריך

2. יצירת פרויקט ב-Android Studio

משך הזמן: 3:00 עד 5:00

כדי להתחיל, צריך ליצור פרויקט חדש ב-Android Studio:

  1. פתיחת Android Studio
  2. לוחצים על פרויקט חדשהמסך 'ברוכים הבאים' ב-Android Studio.
  3. בוחרים באפשרות טלפון וטאבלט ואז באפשרות פעילות ריקהפרויקט Android Studio
  4. לוחצים על הבא.
  5. עכשיו הגיע הזמן להגדיר כמה חלקים בפרויקט:
    • שם: השם של הפרויקט
    • שם החבילה: השדה הזה יאוכלס אוטומטית על סמך שם הפרויקט
    • מיקום השמירה: ברירת המחדל היא התיקייה שבה Android Studio שומר את הפרויקטים. אפשר לשנות את המיקום הזה בכל שלב.
    • גרסת ה-SDK המינימלית: זו הגרסה הכי נמוכה של Android SDK שהאפליקציה שלכם מיועדת לפעול בה. ב-CodeLab הזה נשתמש ב-API 36 ‏ (Baklava)
    הגדרת פרויקט ב-Android Studio
  6. לוחצים על סיום.
  7. ‫Android Studio ייצור את הפרויקט ויוריד את כל יחסי התלות שנדרשים לאפליקציית הבסיס. התהליך הזה יכול להימשך כמה דקות. כדי לראות את זה קורה, פשוט לוחצים על סמל הבנייה:Android Studio Project Building
  8. אחרי שההורדה מסתיימת, Android Studio אמור להיראות כך:פרויקט Android Studio נוצר

3. הגדרת הפרויקט ב-Google Cloud

יצירת פרויקט של Google Cloud

  1. עוברים אל מסוף Google Cloud.
  2. פותחים פרויקט או יוצרים פרויקט חדשGCP create new projectGCP create new project 2GCP create new project 3
  3. לוחצים על APIs & Services (ממשקי API ושירותים)ממשקי API ושירותים של GCP.
  4. עוברים אל מסך ההסכמה ל-OAuthמסך הסכמה ל-OAuth ב-GCP
  5. כדי להמשיך, צריך למלא את השדות בסקירה כללית. לוחצים על שנתחיל? כדי להתחיל למלא את הפרטים הבאים:כפתור 'התחלה' ב-GCP
    • שם האפליקציה: השם של האפליקציה הזו, שצריך להיות זהה לשם שהשתמשתם בו כשיצרתם את הפרויקט ב-Android Studio
    • כתובת אימייל לתמיכה במשתמשים: כאן יוצג חשבון Google שאיתו נכנסתם לחשבון וכל קבוצות Google שאתם מנהלים.
    פרטי אפליקציית GCP
    • קהל:
      • פנימיות לאפליקציה שמשמשת רק בתוך הארגון. אם אין לכם ארגון שמשויך לפרויקט ב-Google Cloud, לא תוכלו לבחור באפשרות הזו.
      • אנחנו נשתמש באפשרות External.
    קהל ב-GCP
    • פרטים ליצירת קשר: כאן אפשר להזין כל כתובת אימייל שרוצים שתשמש כנקודת הקשר לגבי הבקשה
    פרטים ליצירת קשר ב-GCP
    • חשוב לעיין במדיניות של Google בנושא נתוני משתמשים בשירותי API.
  6. אחרי שקוראים את המדיניות בנושא נתוני משתמשים ומסכימים לה, לוחצים על יצירהGCP Create

הגדרת לקוחות OAuth

אחרי שמגדירים פרויקט ב-Google Cloud, צריך להוסיף לקוח אינטרנט ולקוח Android כדי שנוכל לבצע קריאות ל-API לשרת הקצה העורפי של OAuth עם מזהי הלקוח שלהם.

כדי להשתמש בלקוח האינטרנט של Android, צריך:

  • שם החבילה של האפליקציה (למשל, com.example.example)
  • חתימת ה-SHA-1 של האפליקציה
    • מהי חתימת SHA-1?
      • טביעת האצבע של SHA-1 היא גיבוב קריפטוגרפי שנוצר מחתימת האפליקציה. הוא משמש כמזהה ייחודי של אישור החתימה של האפליקציה הספציפית שלכם. אפשר לחשוב על זה כמו על 'חתימה' דיגיטלית לאפליקציה.
    • למה אנחנו צריכים את חתימת SHA-1?
      • טביעת האצבע של SHA-1 מוודאת שרק האפליקציה שלכם, שנחתמה באמצעות מפתח החתימה הספציפי שלכם, יכולה לבקש טוקנים לגישה באמצעות מזהה הלקוח של OAuth 2.0. כך נמנעת גישה של אפליקציות אחרות (גם כאלה עם אותו שם חבילה) למשאבים של הפרויקט ולנתוני המשתמשים.
      • אפשר לחשוב על זה כך:
        • חתימת האפליקציה היא כמו המפתח הפיזי ל'דלת' של האפליקציה. היא מאפשרת גישה לפעולות הפנימיות של האפליקציה.
        • טביעת האצבע של SHA-1 היא כמו מזהה ייחודי של כרטיס מפתח שמקושר למפתח הפיזי שלכם. זהו קוד ספציפי שמזהה את המפתח המסוים הזה.
        • מזהה לקוח OAuth 2.0 הוא כמו קוד כניסה למשאב או לשירות ספציפיים של Google (למשל, כניסה באמצעות חשבון Google).
        • כשמספקים את טביעת האצבע של SHA-1 במהלך הגדרת לקוח OAuth, בעצם אומרים ל-Google: "רק כרטיס המפתח עם המזהה הספציפי הזה (SHA-1) יכול לפתוח את קוד הגישה הזה (מזהה הלקוח)". כך מוודאים שרק האפליקציה שלכם יכולה לגשת לשירותי Google שמקושרים לקוד הכניסה הזה".

במקרה של לקוח אינטרנט, כל מה שצריך הוא השם שבו רוצים להשתמש כדי לזהות את הלקוח במסוף.

יצירת לקוח OAuth 2.0 ל-Android

  1. עוברים לדף לקוחותלקוחות GCP
  2. לוחצים על יצירת לקוחGCP Create Clients.
  3. בקטע Application type (סוג האפליקציה), בוחרים באפשרות Android.
  4. תצטרכו לציין את שם החבילה של האפליקציה
  5. מ-Android Studio, צריך לקבל את חתימת ה-SHA-1 של האפליקציה ולהעתיק אותה לכאן:
    1. עוברים אל Android Studio ופותחים את הטרמינל.
    2. מריצים את הפקודה הבאה:
      keytool -list -v -keystore ~/.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. המסך שלכם אמור להיראות עכשיו כמו בתמונה הבאה, ואפשר ללחוץ על יצירה:פרטי לקוח AndroidAndroid Client

יצירת לקוח OAuth 2.0 לאינטרנט

  1. כדי ליצור מזהה לקוח של אפליקציית אינטרנט, חוזרים על שלבים 1-2 מהקטע יצירת לקוח Android ובוחרים באפשרות אפליקציית אינטרנט בשדה 'סוג האפליקציה'.
  2. נותנים ללקוח שם (זה יהיה לקוח OAuth): פרטי לקוח האינטרנט
  3. לוחצים על יצירהלקוח אינטרנט.
  4. מעתיקים את מזהה הלקוח מהחלון הקופץ, יהיה צורך בו בהמשךהעתקת ה-Client ID

עכשיו, אחרי שהגדרנו את כל לקוחות ה-OAuth, אפשר לחזור ל-Android Studio וליצור את אפליקציית Android 'כניסה באמצעות חשבון Google'.

4. הגדרה של מכשיר אנדרואיד וירטואלי

כדי לבדוק את האפליקציה במהירות בלי מכשיר Android פיזי, כדאי ליצור מכשיר Android וירטואלי שבו תוכלו לבנות את האפליקציה ולהריץ אותה באופן מיידי מ-Android Studio. אם רוצים לבדוק באמצעות מכשיר Android פיזי, אפשר לפעול לפי ההוראות שמפורטות במסמכים למפתחי Android.

יצירת מכשיר Android וירטואלי

  1. ב-Android Studio, פותחים את Device Managerניהול מכשיר
  2. לוחצים על הלחצן + > יצירת מכשיר וירטואלייצירת מכשיר וירטואלי
  3. מכאן אפשר להוסיף כל מכשיר שצריך לפרויקט. למטרות של Codelab הזה, בוחרים באפשרות Medium Phone (טלפון בינוני) ולוחצים על Next (הבא)טלפון בינוני.
  4. עכשיו אפשר להגדיר את המכשיר לפרויקט על ידי מתן שם ייחודי, בחירת גרסת Android שתופעל במכשיר ועוד. מוודאים שממשק ה-API מוגדר ל-API 36 ‏Baklava‏; Android 16 ואז לוחצים על סיוםהגדרת מכשיר וירטואלי
  5. המכשיר החדש אמור להופיע במרכז ניהול המכשירים. כדי לוודא שהמכשיר פועל, לוחצים על הפעלת המכשיר לצד המכשיר שיצרתםהפעלת מכשיר 2.
  6. המכשיר אמור לפעול עכשיו.מכשיר שבו פועלת התוכנה

כניסה למכשיר הווירטואלי של Android

המכשיר שיצרתם עובד, ועכשיו כדי למנוע שגיאות כשבודקים את התכונה 'כניסה באמצעות חשבון Google', צריך להיכנס למכשיר באמצעות חשבון Google.

  1. עוברים להגדרות:
    1. לוחצים במרכז המסך במכשיר הווירטואלי ומחליקים למעלה.
    לחיצה והחלקה
    1. מחפשים את אפליקציית ההגדרות ולוחצים עליה.
    אפליקציית ההגדרות
  2. לוחצים על Google בהגדרותשירותי Google והעדפות.
  3. לוחצים על כניסה ופועלים לפי ההוראות כדי להיכנס לחשבון Googleכניסה לחשבון במכשיר.
  1. עכשיו אתם אמורים להיות מחוברים למכשירהמכשיר מחובר לחשבון

המכשיר הווירטואלי שלך עם Android מוכן עכשיו לבדיקה.

5. הוספת יחסי תלות

משך 5:00

כדי לבצע קריאות ל-OAuth API, קודם צריך לשלב את הספריות הנדרשות שמאפשרות לנו לבצע בקשות אימות ולהשתמש במזהי Google כדי לבצע את הבקשות האלה:

  • libs.googleid
  • libs.play.services.auth
  1. עוברים אל File (קובץ) > Project Structure (מבנה הפרויקט):מבנה הפרויקט
  2. אחר כך עוברים אל Dependencies (תלויות) > app (אפליקציה) > '+' > Library Dependency (תלות בספרייה)תלויות
  3. עכשיו צריך להוסיף את הספריות:
    1. בתיבת הדו-שיח של החיפוש, מקלידים googleid ולוחצים על חיפוש.
    2. צריכה להיות רק רשומה אחת. בוחרים אותה ואת הגרסה הכי גבוהה שזמינה (בזמן כתיבת ה-Codelab הזה, הגרסה היא 1.1.1).
    3. לוחצים על אישורחבילת מזהים של Google
    4. חוזרים על שלבים 1-3, אבל הפעם מחפשים את play-services-auth ובוחרים בשורה עם com.google.android.gms בתור Group ID ו-play-services-auth בתור Artifact Nameאימות ב-Play Services
  4. לוחצים על אישוריחסי תלות שהסתיימו

6. תהליך הגיליון התחתון

תהליך הגיליון התחתון

תהליך ההרשמה באמצעות דף התחתון מבוסס על Credential Manager API, ומאפשר למשתמשים להיכנס לאפליקציה באמצעות חשבונות Google שלהם ב-Android בצורה פשוטה ויעילה. הוא נועד להיות מהיר ונוח, במיוחד למשתמשים חוזרים. התהליך הזה צריך להיות מופעל כשפותחים את האפליקציה.

יצירת בקשת הכניסה

  1. כדי להתחיל, צריך להסיר את הפונקציות Greeting() ו-GreetingPreview() מה-MainActivity.kt, כי לא נשתמש בהן.
  2. עכשיו צריך לוודא שהחבילות שאנחנו צריכים מיובאות לפרויקט הזה. אפשר להוסיף את משפטי import הבאים אחרי המשפטים הקיימים, החל משורה 3::
    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 Class
   //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

  • val context = LocalContext.current: מאחזר את ההקשר הנוכחי של Android. ההרשאה הזו נדרשת לפעולות שונות, כולל הפעלת רכיבי ממשק משתמש.
  • LaunchedEffect(Unit) { ... }: ‏LaunchedEffect היא פונקציה הניתנת להגדרה ב-Jetpack Compose שמאפשרת להפעיל פונקציית השהיה (פונקציה שיכולה להשהות ולחדש את ההפעלה) במהלך מחזור החיים של הפונקציה הניתנת להגדרה. הערך Unit כמפתח אומר שהאפקט הזה יפעל רק פעם אחת כשהקומפוזיציה תופעל בפעם הראשונה.
    • val googleIdOption: GetGoogleIdOption = ...: יצירת אובייקט GetGoogleIdOption. האובייקט הזה מגדיר את סוג פרטי הכניסה שמבוקשים מ-Google.
      • .Builder(): נעשה שימוש בתבנית builder כדי להגדיר את האפשרויות.
      • .setFilterByAuthorizedAccounts(true): מציין אם לאפשר למשתמש לבחור מתוך כל חשבונות Google, או רק מתוך החשבונות שכבר אישרו את האפליקציה. במקרה הזה, הערך מוגדר כ-true, כלומר הבקשה תתבצע באמצעות פרטי הכניסה שהמשתמש אישר בעבר לשימוש באפליקציה הזו, אם יש כאלה.
      • .setServerClientId(webClientId): הגדרת מזהה הלקוח של השרת, שהוא מזהה ייחודי עבור הקצה העורפי של האפליקציה. הפעולה הזו נדרשת כדי לקבל אסימון מזהה.
      • .setNonce(generateSecureRandomNonce()): הגדרת ערך אקראי (nonce) כדי למנוע התקפות שליחה מחדש ולוודא שאסימון המזהה משויך לבקשה הספציפית.
      • .build(): יוצר את אובייקט GetGoogleIdOption עם ההגדרות שצוינו.
    • val request: GetCredentialRequest = ...: יצירת אובייקט GetCredentialRequest. האובייקט הזה מכיל את כל בקשת פרטי הכניסה.
      • .Builder(): מתחיל את תבנית ה-builder להגדרת הבקשה.
      • .addCredentialOption(googleIdOption): מוסיף את googleIdOption לבקשה, ומציין שאנחנו רוצים לבקש אסימון מזהה של Google.
      • .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 = ...: נוצר GetCredentialRequest חדש עם googleIdOptionFalse.
      • signIn(requestFalse, context): ניסיון להיכנס לחשבון המשתמש באמצעות הבקשה החדשה שמאפשרת שימוש בכל חשבון.

במהות, הקוד הזה מכין בקשה ל-Credential Manager API כדי לאחזר אסימון מזהה של Google עבור המשתמש, באמצעות ההגדרות שסופקו. אחר כך אפשר להשתמש ב-GetCredentialRequest כדי להפעיל את ממשק המשתמש של מנהל פרטי הכניסה, שבו המשתמש יכול לבחור את חשבון Google שלו ולהעניק את ההרשאות הנדרשות.

fun generateSecureRandomNonce(byteLength: Int = 32): String: הפונקציה generateSecureRandomNonce מוגדרת. היא מקבלת ארגומנט של מספר שלם byteLength (עם ערך ברירת מחדל של 32) שמציין את האורך הרצוי של ה-nonce בבייטים. הפונקציה מחזירה מחרוזת, שהיא הייצוג בקידוד Base64 של הבייטים האקראיים.

  • val randomBytes = ByteArray(byteLength): יוצר מערך בייטים עם byteLength שצוין כדי להכיל את הבייטים האקראיים.
  • SecureRandom.getInstanceStrong().nextBytes(randomBytes):
    • SecureRandom.getInstanceStrong(): הפונקציה הזו מחזירה מחולל מספרים אקראיים חזק מבחינה קריפטוגרפית. הדבר חשוב מאוד לאבטחה, כי הוא מבטיח שהמספרים שנוצרו הם אקראיים באמת ולא ניתנים לחיזוי. היא משתמשת במקור האנטרופיה החזק ביותר שזמין במערכת.
    • .nextBytes(randomBytes): הפקודה הזו מאכלסת את המערך randomBytes בבייטים אקראיים שנוצרו על ידי המופע SecureRandom.
  • return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes):
    • Base64.getUrlEncoder(): הפונקציה הזו מקבלת מקודד Base64 שמשתמש באלפבית בטוח לשימוש בכתובות URL (משתמש ב-‎-‎ וב-‎_‎ במקום ב-‎+‎ וב-‎/‎). זה חשוב כי זה מבטיח שאפשר להשתמש בבטחה במחרוזת שמתקבלת בכתובות URL בלי צורך בקידוד נוסף.
    • .withoutPadding(): הפונקציה הזו מסירה את כל תווי הריפוד מהמחרוזת בקידוד Base64. לפעמים כדאי לקצר קצת את ה-nonce כדי שהוא יהיה קומפקטי יותר.
    • .encodeToString(randomBytes): הפונקציה הזו מקודדת את ה-randomBytes למחרוזת Base64 ומחזירה אותה.

לסיכום, הפונקציה הזו יוצרת ערך חד-פעמי אקראי חזק מבחינה קריפטוגרפית באורך שצוין, מקודדת אותו באמצעות Base64 בטוח לשימוש בכתובות URL ומחזירה את המחרוזת שמתקבלת. זוהי שיטה סטנדרטית ליצירת ערכי nonce שניתן להשתמש בהם בבטחה בהקשרים שבהם האבטחה חשובה.

שליחת בקשת הכניסה

עכשיו, אחרי שיצרנו את בקשת הכניסה, אנחנו יכולים להשתמש במנהל פרטי הכניסה כדי להיכנס. לשם כך, אנחנו צריכים ליצור פונקציה שמטפלת בהעברת בקשות כניסה באמצעות 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. כלומר, אפשר להשהות את הפעולה ולחדש אותה בלי לחסום את השרשור הראשי.הפונקציה מחזירה Exception?, שיכול להיות null אם הכניסה לחשבון הצליחה, או החריגה הספציפית אם הכניסה נכשלה.

הוא מקבל שני פרמטרים:

  • request: אובייקט GetCredentialRequest שמכיל את ההגדרות של סוג פרטי הכניסה לאחזור (למשל, מזהה Google).
  • 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 ומתחיל תהליך אחזור פרטי הכניסה. הוא מקבל את הבקשה ואת ההקשר כקלט, ומציג למשתמש ממשק משתמש לבחירת אמצעי זיהוי. אם הפעולה תצליח, יוחזר תוצאה שתכיל את פרטי הכניסה שנבחרו. התוצאה של הפעולה הזו, 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.
    • 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): מתעד את החריגה ב-Logcat של Android באמצעות Log.e, שמשמש לשגיאות. הדיווח יכלול את ה-stacktrace של החריג כדי לעזור בניפוי הבאגים. הוא כולל גם את האמוטיקון הכועס בשביל הכיף.
  • return e: הפונקציה מחזירה את החריגה אם נתפסה חריגה כלשהי, או null אם הכניסה הצליחה.

לסיכום, הקוד הזה מספק דרך לטפל בכניסה של משתמשים באמצעות Credential Manager API, מנהל את הפעולה האסינכרונית, מטפל בשגיאות פוטנציאליות ומספק למשתמש משוב באמצעות הודעות קצרות ויומנים, תוך הוספת הומור לטיפול בשגיאות.

הטמעה של תהליך הגיליון התחתון באפליקציה

עכשיו אפשר להגדיר שיחה להפעלת התהליך של BottomSheet במחלקה MainActivity באמצעות הקוד הבא ומזהה הלקוח של אפליקציית האינטרנט שהעתקנו קודם מ-Google Cloud Console:

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)
                }
            }
        }
    }
}

עכשיו אפשר לשמור את הפרויקט (קובץ > שמירה) ולהפעיל אותו:

  1. לוחצים על לחצן ההפעלה:הפעלת הפרויקט
  2. אחרי שהאפליקציה תפעל באמולטור, אמור להופיע חלון קופץ של BottomSheet לכניסה לחשבון. לוחצים על המשך כדי לבדוק את הכניסהגיליון תחתון
  3. תוצג הודעה קופצת שמאשרת שהכניסה לחשבון בוצעה בהצלחה.גיליון תחתון של הצלחה

7. תהליך היצירה של כפתורים

קובץ GIF של זרימת לחצנים

התהליך של הכפתור 'כניסה באמצעות חשבון Google' מקל על המשתמשים להירשם לאפליקציית Android שלכם או להתחבר אליה באמצעות חשבון Google הקיים שלהם. הם יראו את האפשרות הזו אם הם יסגרו את התחתית או אם הם פשוט יעדיפו להשתמש בחשבון 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 ואז על הדבקה (יכול להיות שתצטרכו להרחיב את התיקייה res כדי לראות אותה)ניתן לציור
  5. תופיע תיבת דו-שיח עם בקשה לשנות את שם הקובץ ולאשר את הספרייה שאליה הוא יתווסף. משנים את השם של הנכס ל-siwg_button.png ולוחצים על OKהוספת כפתור

קוד של זרימת כפתור

הקוד הזה ישתמש באותה פונקציה signIn() שמשמשת ל-BottomSheet(), אבל ישתמש ב-GetSignInWithGoogleOption במקום ב-GetGoogleIdOption כי התהליך הזה לא מסתמך על אישורי הכניסה ומפתחות הגישה ששמורים במכשיר כדי להציג אפשרויות כניסה. זה הקוד שאפשר להדביק מתחת לפונקציה 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): הפונקציה ButtonUI מקבלת את הארגומנט webClientId (מזהה הלקוח של הפרויקט שלכם ב-Google Cloud).

val context = LocalContext.current: מאחזר את ההקשר הנוכחי של Android. ההרשאה הזו נדרשת לפעולות שונות, כולל הפעלת רכיבי ממשק משתמש.

val coroutineScope = rememberCoroutineScope(): יוצרת היקף של שגרת משנה. הוא משמש לניהול משימות אסינכרוניות, ומאפשר לקוד לפעול בלי לחסום את ה-thread הראשי. ‫rememberCoroutineScope() היא פונקציה הניתנת להגדרה מ-Jetpack Compose, שמספקת היקף שקשור למחזור החיים של הפונקציה הניתנת להגדרה.

val onClick: () -> Unit = { ... }: הפקודה הזו יוצרת פונקציית lambda שתופעל כשלוחצים על הלחצן. פונקציית הלמדה תבצע את הפעולות הבאות:

  • val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build(): בחלק הזה נוצר אובייקט GetSignInWithGoogleOption. האובייקט הזה משמש לציון הפרמטרים של התהליך 'כניסה באמצעות חשבון Google'. הוא דורש את הפרמטר webClientId וערך nonce (מחרוזת אקראית שמשמשת לאבטחה).
  • val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build(): הפקודה הזו יוצרת אובייקט GetCredentialRequest. הבקשה הזו תשמש לקבלת פרטי הכניסה של המשתמש באמצעות מנהל האישורים. ה-GetCredentialRequest מוסיף את GetSignInWithGoogleOption שנוצר קודם כאפשרות, כדי לבקש אישור כניסה באמצעות חשבון Google.
  • coroutineScope.launch { ... }: CoroutineScope לניהול פעולות אסינכרוניות (באמצעות קורוטינות).
    • signIn(request, context): קורא לפונקציה signIn() שהגדרנו קודם

Image(...): התג הזה מעבד תמונה באמצעות painterResource שמעלה את התמונה R.drawable.siwg_button

  • Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick):
    • fillMaxSize(): התמונה תמלא את השטח הזמין.
    • clickable(enabled = true, onClick = onClick): הופך את התמונה לקליקבילית, וכשלוקחים את העכבר מעל התמונה ולוחצים עליה, מופעלת פונקציית ה-lambda של onClick שהוגדרה קודם.

לסיכום, הקוד הזה מגדיר לחצן 'כניסה באמצעות חשבון Google' בממשק משתמש של Jetpack Compose. כשלוחצים על הלחצן, המערכת מכינה בקשה לפרטי כניסה כדי להפעיל את מנהל פרטי הכניסה ולאפשר למשתמש להיכנס באמצעות חשבון Google שלו.

עכשיו צריך לעדכן את המחלקה MainActivity כדי להפעיל את הפונקציה ButtonUI():

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)
                    }
                }
            }
        }
    }
}

עכשיו אפשר לשמור את הפרויקט (קובץ > שמירה) ולהפעיל אותו:

  1. לוחצים על לחצן ההפעלה:הפעלת הפרויקט
  2. אחרי שהאפליקציה תפעל באמולטור, ה-BottomSheet אמור להופיע. כדי לסגור אותו, לוחצים מחוץ לו.יש ללחוץ כאן
  3. עכשיו אמור להופיע באפליקציה הלחצן שיצרנו. לוחצים עליו כדי לראות את תיבת הדו-שיח לכניסהתיבת דו-שיח לכניסה
  4. לוחצים על החשבון כדי להיכנס.

8. סיכום

סיימתם את ה-Codelab הזה! מידע נוסף או עזרה בנושא כניסה באמצעות חשבון Google ב-Android זמינים בקטע השאלות הנפוצות שבהמשך:

שאלות נפוצות

הקוד המלא של 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
}