Dowiedz się, jak wdrożyć funkcję Zaloguj się przez Google w aplikacji na Androida

1. Zanim zaczniesz

Z tego laboratorium dowiesz się, jak zaimplementować na Androidzie funkcję Zaloguj się przez Google za pomocą Menedżera danych logowania.

Wymagania wstępne

  • Podstawowa wiedza na temat korzystania z Kotlin do tworzenia aplikacji na Androida.
  • Podstawowa znajomość Jetpack Compose (więcej informacji znajdziesz tutaj).

Czego się nauczysz

  • Jak utworzyć projekt Google Cloud
  • Jak tworzyć klienty OAuth w konsoli Google Cloud
  • Jak wdrożyć funkcję Zaloguj się przez Google za pomocą przepływu arkusza u dołu ekranu
  • Jak wdrożyć funkcję Zaloguj się przez Google za pomocą procedury przycisku

Wymagania

2. Tworzenie projektu Android Studio

Czas trwania: 3:00–5:00

Na początek musimy utworzyć nowy projekt w Android Studio:

  1. Otwórz Android Studio
  2. Kliknij Nowy projektPowitanie w Android Studio.
  3. Wybierz Telefon i tablet oraz Puste działanieProjekt Android Studio
  4. Kliknij Dalej.
  5. Teraz czas skonfigurować kilka elementów projektu:
    • Nazwa: nazwa projektu.
    • Nazwa pakietu: zostanie wypełniona automatycznie na podstawie nazwy projektu.
    • Lokalizacja zapisu: domyślnie powinien to być folder, w którym Android Studio zapisuje projekty. Możesz to zmienić w dowolnym momencie.
    • Minimalny pakiet SDK: jest to najstarsza wersja pakietu Android SDK, na której Twoja aplikacja może działać. W tym module będziemy używać interfejsu API 36 (Baklava).
    Projekt konfiguracji Android Studio
  6. Kliknij Zakończ.
  7. Android Studio utworzy projekt i pobierze wszystkie niezbędne zależności dla podstawowej aplikacji. Może to potrwać kilka minut. Aby to zobaczyć, kliknij ikonę kompilacji:Tworzenie projektu Android Studio
  8. Po zakończeniu tego procesu środowisko Android Studio powinno wyglądać podobnie do tego:Projekt Android Studio został skompilowany

3. Konfigurowanie projektu Google Cloud

Tworzenie projektu Google Cloud

  1. Otwórz konsolę Google Cloud.
  2. Otwórz projekt lub utwórz nowyGCP create new projectGCP – tworzenie nowego projektu 2GCP tworzenie nowego projektu 3
  3. Kliknij Interfejsy API i usługiInterfejsy API i usługi GCP
  4. Otwórz ekran zgody OAuthEkran zgody OAuth w GCP.
  5. Aby kontynuować, musisz wypełnić pola w sekcji Przegląd. Kliknij Rozpocznij, aby zacząć wypełniać te informacje:Przycisk Rozpocznij w GCP
    • Nazwa aplikacji: nazwa aplikacji, która powinna być taka sama jak nazwa użyta podczas tworzenia projektu w Android Studio.
    • Adres e-mail pomocy dla użytkowników: wyświetli konto Google, na które się logujesz, oraz wszystkie Grupy dyskusyjne Google, którymi zarządzasz.
    Informacje o aplikacji GCP
    • Odbiorcy:
      • Wewnętrzna – w przypadku aplikacji używanej tylko w organizacji. Jeśli z projektem Google Cloud nie jest powiązana żadna organizacja, nie możesz go wybrać.
      • Będziemy korzystać z opcji Zewnętrzny.
    Odbiorcy GCP
    • Dane kontaktowe: może to być dowolny adres e-mail, który ma być punktem kontaktowym dla aplikacji.
    Informacje kontaktowe GCP
    • Zapoznaj się z zasadami dotyczącymi danych użytkownika w usługach interfejsów API Google.
  6. Po przeczytaniu i zaakceptowaniu zasad dotyczących danych użytkownika kliknij UtwórzGCP Create

Konfigurowanie klientów OAuth

Po skonfigurowaniu projektu Google Cloud musimy dodać klienta internetowego i klienta Androida, aby móc wywoływać interfejsy API na serwerze backendu OAuth za pomocą ich identyfikatorów klienta.

W przypadku klienta internetowego na Androida musisz mieć:

  • Nazwa pakietu aplikacji (np. com.example.example)
  • Podpis SHA-1 aplikacji
    • Co to jest podpis SHA-1?
      • Odcisk cyfrowy SHA-1 to skrót kryptograficzny wygenerowany na podstawie klucza podpisywania aplikacji. Jest to unikalny identyfikator certyfikatu podpisywania konkretnej aplikacji. Można go porównać do cyfrowego „podpisu” aplikacji.
    • Dlaczego potrzebujemy podpisu SHA-1?
      • Odcisk cyfrowy SHA-1 zapewnia, że tylko Twoja aplikacja podpisana określonym kluczem podpisywania może prosić o tokeny dostępu przy użyciu identyfikatora klienta OAuth 2.0. Zapobiega to uzyskiwaniu dostępu do zasobów projektu i danych użytkowników przez inne aplikacje (nawet te o tej samej nazwie pakietu).
      • Wyobraź sobie, że:
        • Klucz podpisywania aplikacji jest jak fizyczny klucz do „drzwi” aplikacji. Umożliwia on dostęp do wewnętrznych mechanizmów aplikacji.
        • Odcisk cyfrowy SHA-1 jest jak unikalny identyfikator karty dostępu, który jest powiązany z fizycznym kluczem. Jest to konkretny kod, który identyfikuje dany klucz.
        • Identyfikator klienta OAuth 2.0 jest jak kod dostępu do konkretnego zasobu lub usługi Google (np. Logowanie przez Google).
        • Podając odcisk SHA-1 podczas konfigurowania klienta OAuth, mówisz Google: „Tylko karta z tym konkretnym identyfikatorem (SHA-1) może otworzyć ten kod dostępu (identyfikator klienta)”. Dzięki temu tylko Twoja aplikacja będzie mieć dostęp do usług Google połączonych z tym kodem."

W przypadku klienta internetowego wystarczy podać nazwę, której chcesz używać do identyfikacji klienta w konsoli.

Tworzenie klienta OAuth 2.0 na Androida

  1. Otwórz stronę Klienci.Klienci GCP
  2. Kliknij Utwórz klientaGCP Create Clients
  3. Jako typ aplikacji wybierz Android.
  4. Musisz podać nazwę pakietu aplikacji
  5. W Androidzie Studio musimy uzyskać podpis SHA-1 naszej aplikacji i skopiować go tutaj:
    1. Otwórz Android Studio i terminal.
    2. Uruchom to polecenie:
      keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
      
      To polecenie służy do wyświetlania szczegółów konkretnego wpisu (aliasu) w magazynie kluczy.
      • -list: Ta opcja nakazuje narzędziu keytool wyświetlenie zawartości magazynu kluczy.
      • -v: ta opcja włącza szczegółowe dane wyjściowe, które zawierają więcej informacji o wpisie.
      • -keystore ~/.android/debug.keystore: Określa ścieżkę do pliku magazynu kluczy.
      • -alias androiddebugkey: określa alias (nazwę wpisu) klucza, który chcesz sprawdzić.
      • -storepass android: podaje hasło do pliku magazynu kluczy.
      • -keypass android: podaje hasło do klucza prywatnego określonego aliasu.
    3. Skopiuj wartość podpisu SHA-1:
    Podpis SHA
    1. Wróć do okna Google Cloud i wklej wartość podpisu SHA-1:
  6. Ekran powinien teraz wyglądać podobnie do tego, a Ty możesz kliknąć Utwórz:Szczegóły klienta AndroidaKlient Androida

Tworzenie klienta internetowego OAuth 2.0

  1. Aby utworzyć identyfikator klienta aplikacji internetowej, powtórz kroki 1–2 z sekcji Tworzenie klienta Androida i jako typ aplikacji wybierz Aplikacja internetowa.
  2. Nadaj klientowi nazwę (będzie to klient OAuth): Szczegóły klienta internetowego
  3. Kliknij Utwórz.Klient internetowy
  4. Skopiuj identyfikator klienta z wyskakującego okienka. Będzie on potrzebny później.Kopiuj identyfikator klienta

Po skonfigurowaniu klientów OAuth możemy wrócić do Android Studio i utworzyć aplikację na Androida do logowania się za pomocą Google.

4. Konfigurowanie urządzenia wirtualnego z Androidem

Aby szybko przetestować aplikację bez fizycznego urządzenia z Androidem, utwórz urządzenie wirtualne z Androidem, na którym możesz skompilować i natychmiast uruchomić aplikację z Android Studio. Jeśli chcesz przeprowadzić test na fizycznym urządzeniu z Androidem, postępuj zgodnie z instrukcjami w dokumentacji dla deweloperów aplikacji na Androida.

Tworzenie wirtualnego urządzenia z Androidem

  1. W Android Studio otwórz Menedżera urządzeńMenedżer urządzeń.
  2. Kliknij przycisk + > Utwórz urządzenie wirtualneTworzenie urządzenia wirtualnego
  3. W tym miejscu możesz dodać dowolne urządzenie potrzebne do projektu. Na potrzeby tego Codelabu wybierz Medium Phone (Średni telefon) i kliknij DalejTelefon średni.
  4. Teraz możesz skonfigurować urządzenie dla swojego projektu, nadając mu unikalną nazwę, wybierając wersję Androida, na której będzie działać, i wykonując inne czynności. Sprawdź, czy interfejs API jest ustawiony na API 36 „Baklava”; Android 16, a potem kliknij ZakończKonfigurowanie urządzenia wirtualnego.
  5. Nowe urządzenie powinno pojawić się w Menedżerze urządzeń. Aby sprawdzić, czy urządzenie działa, kliknij Uruchom urządzenie obok utworzonego urządzeniaUruchom urządzenie 2.
  6. Urządzenie powinno się teraz włączyć.Urządzenie do biegania

Logowanie się na wirtualnym urządzeniu z Androidem

Utworzone urządzenie działa. Aby uniknąć błędów podczas testowania logowania się przez Google, musimy zalogować się na urządzeniu za pomocą konta Google.

  1. Otwórz Ustawienia:
    1. Kliknij środek ekranu urządzenia wirtualnego i przesuń palcem w górę.
    Kliknięcie i przesunięcie
    1. Znajdź aplikację Ustawienia i kliknij ją.
    Aplikacja Ustawienia
  2. W Ustawieniach kliknij GoogleUsługi Google i ustawienia
  3. Kliknij Zaloguj się i postępuj zgodnie z instrukcjami, aby zalogować się na konto GoogleLogowanie na urządzeniu.
  1. Powinno nastąpić zalogowanie na urządzeniu.Urządzenie zalogowane

Wirtualne urządzenie z Androidem jest gotowe do testowania.

5. Dodawanie zależności

Czas trwania: 5:00

Aby wywoływać interfejs API OAuth, musimy najpierw zintegrować potrzebne biblioteki, które umożliwiają wysyłanie żądań uwierzytelniania i używanie identyfikatorów Google do wysyłania tych żądań:

  • libs.googleid
  • libs.play.services.auth
  1. Kliknij kolejno File > Project Structure (Plik > Struktura projektu):Struktura projektu
  2. Następnie otwórz Dependencies (Zależności) > app (aplikacja) > „+” > Library Dependency (Zależność biblioteki)Zależności.
  3. Teraz musimy dodać nasze biblioteki:
    1. W oknie wyszukiwania wpisz googleid i kliknij Szukaj.
    2. Powinien być tylko jeden wpis. Wybierz go i najnowszą dostępną wersję (w momencie tworzenia tego kursu jest to wersja 1.1.1).
    3. Kliknij OKPakiet identyfikatorów Google
    4. Powtórz kroki 1–3, ale zamiast tego wyszukaj „play-services-auth” i wybierz wiersz z wartością „com.google.android.gms” w kolumnie Identyfikator grupy oraz „play-services-auth” w kolumnie Nazwa artefaktu.Uwierzytelnianie w Usługach Play
  4. Kliknij OKZakończone zależności

6. Przepływ planszy dolnej

Przepływ planszy dolnej

Przepływ arkusza u dołu ekranu korzysta z interfejsu Credential Manager API, aby umożliwić użytkownikom logowanie się w aplikacji na Androidzie za pomocą kont Google. Został on zaprojektowany tak, aby był szybki i wygodny, zwłaszcza dla powracających użytkowników. Ten proces powinien być uruchamiany przy uruchomieniu aplikacji.

Tworzenie żądania logowania

  1. Na początek usuń funkcje Greeting()GreetingPreview() z funkcji MainActivity.kt – nie będą nam potrzebne.
  2. Teraz musimy się upewnić, że pakiety, których potrzebujemy, są zaimportowane do tego projektu. Dodaj te instrukcje import po dotychczasowych, zaczynając od wiersza 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. Następnie musimy utworzyć funkcję do tworzenia żądania Bottom Sheet. Wklej ten kod pod klasą 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)
   }

Przyjrzyjmy się, co robi ten kod:

fun BottomSheet(webClientId: String) {...}: tworzy funkcję o nazwie BottomSheet, która przyjmuje 1 argument tekstowy o nazwie webClientid.

  • val context = LocalContext.current: pobiera bieżący kontekst Androida. Jest to wymagane w przypadku różnych operacji, w tym uruchamiania komponentów interfejsu.
  • LaunchedEffect(Unit) { ... }: LaunchedEffect to funkcja kompozycyjna Jetpack Compose, która umożliwia uruchamianie funkcji zawieszającej (funkcji, która może wstrzymywać i wznawiać wykonanie) w cyklu życia funkcji kompozycyjnej. Jednostka jako klucz oznacza, że ten efekt zostanie uruchomiony tylko raz, gdy funkcja kompozycyjna zostanie uruchomiona po raz pierwszy.
    • val googleIdOption: GetGoogleIdOption = ...: tworzy obiekt GetGoogleIdOption. Ten obiekt konfiguruje typ danych logowania, o które użytkownik prosi Google.
      • .Builder(): do konfigurowania opcji używany jest wzorzec konstruktora.
      • .setFilterByAuthorizedAccounts(true): określa, czy użytkownik może wybierać spośród wszystkich kont Google, czy tylko tych, które już autoryzowały aplikację. W tym przypadku wartość to „true”, co oznacza, że żądanie zostanie wysłane przy użyciu danych logowania, które użytkownik wcześniej autoryzował do użycia w tej aplikacji (jeśli takie dane są dostępne).
      • .setServerClientId(webClientId): ustawia identyfikator klienta serwera, który jest unikalnym identyfikatorem backendu aplikacji. Jest to wymagane, aby uzyskać token identyfikatora.
      • .setNonce(generateSecureRandomNonce()): ustawia wartość jednorazową, czyli losową wartość, aby zapobiec atakom typu replay i zapewnić, że token identyfikatora jest powiązany z określonym żądaniem.
      • .build(): tworzy obiekt GetGoogleIdOption z określoną konfiguracją.
    • val request: GetCredentialRequest = ...: tworzy obiekt GetCredentialRequest. Ten obiekt zawiera całe żądanie danych logowania.
      • .Builder(): uruchamia wzorzec konstruktora, aby skonfigurować żądanie.
      • .addCredentialOption(googleIdOption): dodaje do żądania parametr googleIdOption, który określa, że chcemy poprosić o token identyfikatora Google.
      • .build(): tworzy obiekt GetCredentialRequest.
    • val e = signIn(request, context): próbuje zalogować użytkownika za pomocą utworzonego żądania i bieżącego kontekstu. Wynik funkcji signIn jest przechowywany w e. Ta zmienna będzie zawierać wynik lub wyjątek.
    • if (e is NoCredentialException) { ... }: jest to sprawdzanie warunkowe. Jeśli funkcja signIn zakończy się niepowodzeniem z wyjątkiem NoCredentialException, oznacza to, że nie ma dostępnych wcześniej autoryzowanych kont.
      • val googleIdOptionFalse: GetGoogleIdOption = ...: jeśli poprzedni warunek signIn nie został spełniony, ta część tworzy nowy warunek GetGoogleIdOption.
      • .setFilterByAuthorizedAccounts(false): to jest kluczowa różnica w porównaniu z pierwszą opcją. Wyłącza filtrowanie autoryzowanych kont, co oznacza, że do logowania się można używać dowolnego konta Google na urządzeniu.
      • val requestFalse: GetCredentialRequest = ...: Tworzony jest nowy GetCredentialRequest z wartością googleIdOptionFalse.
      • signIn(requestFalse, context): próba zalogowania użytkownika za pomocą nowego żądania, które umożliwia użycie dowolnego konta.

Ten kod przygotowuje żądanie do interfejsu Credential Manager API, aby pobrać token identyfikatora Google dla użytkownika przy użyciu podanych konfiguracji. Żądanie GetCredentialRequest można następnie wykorzystać do uruchomienia interfejsu menedżera danych logowania, w którym użytkownik może wybrać swoje konto Google i przyznać niezbędne uprawnienia.

fun generateSecureRandomNonce(byteLength: Int = 32): String: Definiuje to funkcję o nazwie generateSecureRandomNonce. Przyjmuje argument całkowity byteLength (z wartością domyślną 32), który określa żądaną długość nonce w bajtach. Zwraca ciąg tekstowy, który jest reprezentacją losowych bajtów zakodowaną w standardzie Base64.

  • val randomBytes = ByteArray(byteLength): tworzy tablicę bajtów o określonej długości, która będzie zawierać losowe bajty.
  • SecureRandom.getInstanceStrong().nextBytes(randomBytes):
    • SecureRandom.getInstanceStrong(): zapewnia to generator liczb losowych o właściwościach kryptograficznych. Jest to kluczowe dla bezpieczeństwa, ponieważ zapewnia, że wygenerowane liczby są rzeczywiście losowe i nieprzewidywalne. Wykorzystuje najsilniejsze dostępne źródło entropii w systemie.
    • .nextBytes(randomBytes): wypełnia tablicę randomBytes losowymi bajtami wygenerowanymi przez instancję SecureRandom.
  • return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes):
    • Base64.getUrlEncoder(): pobiera koder Base64, który używa alfabetu bezpiecznego dla adresów URL (zamiast znaków + i / używa znaków - i _). Jest to ważne, ponieważ dzięki temu wynikowy ciąg znaków może być bezpiecznie używany w adresach URL bez konieczności dalszego kodowania.
    • .withoutPadding(): usuwa znaki dopełnienia z ciągu zakodowanego w Base64. Często jest to pożądane, aby skrócić i zmniejszyć rozmiar nonce.
    • .encodeToString(randomBytes): koduje losowe bajty w ciąg Base64 i zwraca go.

Podsumowując, ta funkcja generuje kryptograficznie silny losowy nonce o określonej długości, koduje go za pomocą bezpiecznego dla adresu URL kodowania Base64 i zwraca wynikowy ciąg znaków. Jest to standardowa praktyka generowania wartości nonce, które są bezpieczne w kontekstach wymagających szczególnej ochrony.

Wysyłanie prośby o zalogowanie

Teraz, gdy możemy utworzyć żądanie logowania, możemy użyć Menedżera danych logowania, aby się zalogować. Aby to zrobić, musimy utworzyć funkcję, która będzie obsługiwać przekazywanie żądań logowania za pomocą Menedżera danych logowania, a także typowe wyjątki, które mogą wystąpić.

Aby to zrobić, możesz wkleić tę funkcję pod funkcją 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
}

Teraz omówmy, co robi ten kod:

suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?: definiuje funkcję zawieszania o nazwie signIn. Oznacza to, że można go wstrzymać i wznowić bez blokowania głównego wątku.Zwraca on Exception?, który będzie miał wartość null, jeśli logowanie się powiedzie, lub konkretny wyjątek, jeśli logowanie się nie powiedzie.

Przyjmuje 2 parametry:

  • request: obiekt GetCredentialRequest, który zawiera konfigurację typu danych logowania do pobrania (np. Identyfikator Google).
  • context: kontekst Androida potrzebny do interakcji z systemem.

W przypadku treści funkcji:

  • val credentialManager = CredentialManager.create(context): tworzy instancję CredentialManager, która jest głównym interfejsem do interakcji z interfejsem Credential Manager API. W ten sposób aplikacja rozpocznie proces logowania.
  • val failureMessage = "Sign in failed!": definiuje ciąg znaków (failureMessage), który ma być wyświetlany w powiadomieniu, gdy logowanie się nie powiedzie.
  • var e: Exception? = null: ten wiersz inicjuje zmienną e, która będzie przechowywać wszelkie wyjątki, jakie mogą wystąpić podczas procesu, zaczynając od wartości null.
  • delay(250): wprowadza opóźnienie 250 milisekund. Jest to obejście potencjalnego problemu, w którym wyjątek NoCredentialException może być zgłaszany natychmiast po uruchomieniu aplikacji, zwłaszcza podczas korzystania z przepływu BottomSheet. Daje to systemowi czas na zainicjowanie menedżera danych logowania.
  • try { ... } catch (e: Exception) { ... }:Blok try-catch służy do niezawodnej obsługi błędów. Dzięki temu w przypadku wystąpienia błędu podczas procesu logowania aplikacja nie ulegnie awarii i będzie mogła prawidłowo obsłużyć wyjątek.
    • val result = credentialManager.getCredential(request = request, context = context): w tym miejscu następuje rzeczywiste wywołanie interfejsu Credential Manager API, które inicjuje proces pobierania danych logowania. Przyjmuje żądanie i kontekst jako dane wejściowe i wyświetla użytkownikowi interfejs, w którym może on wybrać dane logowania. Jeśli operacja się uda, zwróci wynik zawierający wybrane dane logowania. Wynik tej operacji, GetCredentialResponse, jest przechowywany w zmiennej result.
    • Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show():wyświetla krótki komunikat informujący o udanym logowaniu.
    • Log.i(TAG, "Sign in Successful!"): zapisuje w logcatcie zabawną, udaną wiadomość.
    • catch (e: GetCredentialException): obsługuje wyjątki typu GetCredentialException. Jest to klasa nadrzędna dla kilku konkretnych wyjątków, które mogą wystąpić podczas pobierania danych logowania.
    • catch (e: GoogleIdTokenParsingException): obsługuje wyjątki, które występują, gdy podczas analizowania tokena identyfikatora Google wystąpi błąd.
    • catch (e: NoCredentialException): obsługuje wyjątek NoCredentialException, który jest zgłaszany, gdy użytkownik nie ma dostępnych danych logowania (np. nie zapisał żadnych danych lub nie ma konta Google).
      • Co ważne, ta funkcja zwraca wyjątek przechowywany w e, NoCredentialException, co umożliwia wywołującemu obsługę konkretnego przypadku, gdy nie są dostępne żadne dane logowania.
    • catch (e: GetCredentialCustomException): obsługuje niestandardowe wyjątki, które mogą być zgłaszane przez dostawcę danych logowania.
    • catch (e: GetCredentialCancellationException): obsługuje wyjątek GetCredentialCancellationException, który jest zgłaszany, gdy użytkownik anuluje proces logowania.
    • Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show(): wyświetla komunikat w formie wyskakującego okienka informujący o tym, że logowanie nie powiodło się z powodu failureMessage.
    • Log.e(TAG, "", e): rejestruje wyjątek w logcat Androida za pomocą Log.e, który jest używany w przypadku błędów. Będzie on zawierać ślad stosu wyjątku, co ułatwi debugowanie. Zawiera też emotikon z wyrazem złości dla zabawy.
  • return e: funkcja zwraca wyjątek, jeśli został przechwycony, lub wartość null, jeśli logowanie się powiodło.

Podsumowując, ten kod umożliwia obsługę logowania użytkownika za pomocą interfejsu Credential Manager API, zarządza operacją asynchroniczną, obsługuje potencjalne błędy i przekazuje użytkownikowi informacje zwrotne za pomocą powiadomień i logów, a przy tym dodaje odrobinę humoru do obsługi błędów.

Wdrażanie przepływu planszy dolnej w aplikacji

Teraz możemy skonfigurować wywołanie, które uruchomi przepływ BottomSheet w naszej klasie MainActivity, używając tego kodu i identyfikatora klienta aplikacji internetowej skopiowanego wcześniej z konsoli Google Cloud:

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

Teraz możemy zapisać projekt (Plik > Zapisz) i go uruchomić:

  1. Naciśnij przycisk uruchamiania:Uruchamianie projektu
  2. Gdy aplikacja zostanie uruchomiona na emulatorze, powinien pojawić się arkusz BottomSheet logowania. Kliknij Dalej, aby przetestować logowanie.Plansza dolna
  3. Powinien zostać wyświetlony komunikat informujący o zalogowaniu się.Plansza dolna – powodzenie

7. Proces tworzenia przycisków

GIF z przepływem przycisku

Proces logowania przez Google za pomocą przycisku ułatwia użytkownikom rejestrację i logowanie się w aplikacji na Androida przy użyciu istniejącego konta Google. Użytkownicy mogą z niej skorzystać, jeśli zamkną arkusz u dołu ekranu lub wolą wyraźnie użyć konta Google do zalogowania się lub zarejestrowania. Dla deweloperów oznacza to łatwiejsze wprowadzanie i mniej problemów podczas rejestracji.

Można to zrobić za pomocą gotowego przycisku Jetpack Compose, ale my użyjemy wstępnie zatwierdzonej ikony marki ze strony Wytyczne dotyczące marki Zaloguj się przez Google.

Dodawanie ikony marki do projektu

  1. Pobierz plik ZIP z wstępnie zatwierdzonymi ikonami marek tutaj
  2. Rozpakuj pobrany plik signin-assest.zip (sposób rozpakowywania zależy od systemu operacyjnego komputera). Teraz możesz otworzyć folder signin-assets i przejrzeć dostępne ikony. W tym ćwiczeniu użyjemy signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png.
  3. Kopiowanie pliku
  4. Wklej do projektu w Android Studio w sekcji res > drawable, klikając prawym przyciskiem myszy folder drawable i wybierając Wklej (może być konieczne rozwinięcie folderu res, aby go zobaczyć).Drawable
  5. Pojawi się okno z prośbą o zmianę nazwy pliku i potwierdzenie katalogu, do którego zostanie on dodany. Zmień nazwę zasobu na siwg_button.png, a następnie kliknij OK.Dodawanie przycisku

Kod przepływu przycisku

Ten kod będzie używać tej samej funkcji signIn(), która jest używana w przypadku BottomSheet(), ale zamiast GetGoogleIdOption będzie używać GetSignInWithGoogleOption, ponieważ ten proces nie wykorzystuje przechowywanych na urządzeniu danych logowania ani kluczy dostępu do wyświetlania opcji logowania. Oto kod, który możesz wkleić pod funkcją 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)
    )
}

Aby wyjaśnić, co robi kod:

fun ButtonUI(webClientId: String): deklaruje funkcję o nazwie ButtonUI, która przyjmuje argument webClientId (identyfikator klienta projektu Google Cloud).

val context = LocalContext.current: pobiera bieżący kontekst Androida. Jest to wymagane w przypadku różnych operacji, w tym uruchamiania komponentów interfejsu.

val coroutineScope = rememberCoroutineScope(): tworzy zakres współprogramu. Służy do zarządzania zadaniami asynchronicznymi, dzięki czemu kod może działać bez blokowania wątku głównego. rememberCoroutineScope() to funkcja typu „composable” z Jetpack Compose, która zapewnia zakres powiązany z cyklem życia funkcji typu „composable”.

val onClick: () -> Unit = { ... }: tworzy funkcję lambda, która zostanie wykonana po kliknięciu przycisku. Funkcja Lambda:

  • val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build(): Ta część tworzy obiekt GetSignInWithGoogleOption. Ten obiekt służy do określania parametrów procesu „Zaloguj się przez Google”. Wymaga on webClientId i wartości nonce (losowego ciągu znaków używanego do celów związanych z bezpieczeństwem).
  • val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build(): tworzy to obiekt GetCredentialRequest. Ta prośba zostanie użyta do uzyskania poświadczeń użytkownika za pomocą Menedżera poświadczeń. GetCredentialRequest dodaje utworzony wcześniej GetSignInWithGoogleOption jako opcję, aby poprosić o dane logowania „Zaloguj się przez Google”.
  • coroutineScope.launch { ... }: CoroutineScope do zarządzania operacjami asynchronicznymi (za pomocą korutyn).
    • signIn(request, context): wywołuje zdefiniowaną wcześniej funkcję signIn()

Image(...): renderuje obraz przy użyciu elementu painterResource, który wczytuje obraz R.drawable.siwg_button.

  • Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick):
    • fillMaxSize(): powoduje wypełnienie dostępnego miejsca przez obraz.
    • clickable(enabled = true, onClick = onClick): sprawia, że obraz można kliknąć, a po kliknięciu wykonuje on wcześniej zdefiniowaną funkcję lambda onClick.

Podsumowując, ten kod konfiguruje przycisk „Zaloguj się przez Google” w interfejsie Jetpack Compose. Gdy użytkownik kliknie ten przycisk, przygotowywana jest prośba o dane logowania, która uruchamia Menedżera danych logowania i umożliwia użytkownikowi zalogowanie się za pomocą konta Google.

Teraz musimy zaktualizować klasę MainActivity, aby uruchomić funkcję 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)
                    }
                }
            }
        }
    }
}

Teraz możemy zapisać projekt (Plik > Zapisz) i go uruchomić:

  1. Naciśnij przycisk uruchamiania:Uruchamianie projektu
  2. Gdy aplikacja zostanie uruchomiona na emulatorze, powinien pojawić się BottomSheet. Kliknij poza nią, aby ją zamknąć.Kliknij tutaj
  3. W aplikacji powinien być teraz widoczny utworzony przez nas przycisk. Kliknij go, aby wyświetlić okno logowania.Okno logowania
  4. Kliknij swoje konto, aby się zalogować.

8. Podsumowanie

To ćwiczenie zostało ukończone. Więcej informacji lub pomocy dotyczących logowania się przez Google na Androidzie znajdziesz w sekcji Najczęstsze pytania poniżej:

Najczęstsze pytania

Pełny kod MainActivity.kt

Oto pełny kod pliku 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
}