Узнайте, как реализовать функцию «Войти через Google» в приложении для Android.

1. Прежде чем начать

В этой лабораторной работе вы узнаете, как реализовать вход с помощью Google на Android с помощью диспетчера учетных данных.

Предпосылки

  • Базовое понимание использования 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, на которой работает ваше приложение. В этом практическом занятии мы будем использовать API 36 (Baklava).
    Проект настройки Android Studio
  6. Нажмите «Готово».
  7. Android Studio создаст проект и загрузит все необходимые зависимости для базового приложения. Это может занять несколько минут. Чтобы увидеть процесс, просто нажмите на значок сборки: Создание проекта Android Studio
  8. После завершения Android Studio должна выглядеть примерно так:Проект Android Studio создан

3. Настройте свой проект Google Cloud

Создайте проект Google Cloud

  1. Перейдите в Google Cloud Console.
  2. Откройте свой проект или создайте новый проект GCP создать новый проектGCP создать новый проект 2GCP создать новый проект 3
  3. API и сервисы ClickAPI и сервисы GCP
  4. Перейти к экрану согласия OAuth Экран согласия GCP OAuth
  5. Чтобы продолжить, вам потребуется заполнить поля в разделе «Обзор» . Нажмите «Начать» , чтобы начать заполнение этой информации: Кнопка «Начать работу с GCP»
    • Имя приложения : имя этого приложения, которое должно совпадать с тем, которое вы использовали при создании проекта в Android Studio.
    • Электронная почта службы поддержки пользователей : здесь будет указана учетная запись Google, с которой вы вошли в систему, и все группы Google, которыми вы управляете.
    Информация о приложении GCP
    • Аудитория :
      • Внутреннее — для приложения, используемого только внутри вашей организации. Если у вас нет организации, связанной с проектом Google Cloud, вы не сможете выбрать этот вариант.
      • Мы будем использовать External.
    Аудитория GCP
    • Контактная информация : это может быть любой адрес электронной почты, который вы хотите указать в качестве контактного лица для подачи заявки.
    Контактная информация GCP
    • Ознакомьтесь со статьей «Сервисы API Google: Политика в отношении пользовательских данных».
  6. После ознакомления и согласия с Политикой обработки данных пользователей нажмите «Создать».GCP Создать

Настройка клиентов OAuth

Теперь, когда у нас настроен проект Google Cloud, нам нужно добавить веб-клиент и клиент Android, чтобы мы могли выполнять вызовы API на внутренний сервер OAuth, используя их идентификаторы клиентов.

Для Android Web Client вам понадобится:

  • Имя пакета вашего приложения (например, 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, связанным с этим кодом входа.

Для веб-клиента нам понадобится только имя, которое вы хотите использовать для идентификации клиента в консоли.

Создать клиент Android OAuth 2.0

  1. Перейти на страницу «Клиенты»Клиенты GCP
  2. Нажмите «Создать клиента».GCP Создать клиентов
  3. Выберите 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-клиент

Создать веб-клиент OAuth 2.0

  1. Чтобы создать идентификатор клиента веб-приложения, повторите шаги 1–2 из раздела «Создание клиента Android» и выберите «Веб-приложение» в качестве типа приложения.
  2. Дайте клиенту имя (это будет клиент OAuth):Подробности веб-клиента
  3. Нажмите «Создать».Веб-клиент
  4. Скопируйте идентификатор клиента из всплывающего окна. Он понадобится вам позже. Копировать идентификатор клиента

Теперь, когда наши клиенты OAuth полностью настроены, мы можем вернуться в Android Studio и создать приложение «Войти через Google» для Android!

4. Настройте виртуальное устройство Android

Для быстрого тестирования приложения без физического устройства Android вам потребуется создать виртуальное устройство Android, на котором вы сможете собрать и сразу запустить приложение из Android Studio. Если вы хотите протестировать приложение на физическом устройстве Android, следуйте инструкциям из документации Android Devloper.

Создайте виртуальное устройство Android

  1. В Android Studio откройте диспетчер устройств.Диспетчер устройств
  2. Нажмите кнопку + > Создать виртуальное устройство. Создать виртуальное устройство
  3. Здесь вы можете добавить любое устройство, необходимое для вашего проекта. Для целей этой практической работы выберите Medium Phone и нажмите «Далее».Средний телефон
  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

Чтобы выполнять вызовы API OAuth, нам сначала нужно интегрировать необходимые библиотеки, которые позволят нам выполнять запросы аутентификации и использовать идентификаторы Google для выполнения этих запросов:

  • libs.googleid
  • libs.play.services.auth
  1. Перейдите в Файл > Структура проекта:Структура проекта
  2. Затем перейдите в раздел Зависимости > приложение > «+» > Зависимость библиотеки.Зависимости
  3. Теперь нам нужно добавить наши библиотеки:
    1. В диалоговом окне поиска введите googleid и нажмите «Поиск».
    2. Должна быть только одна запись, выберите ее и самую высокую доступную версию (на момент выполнения этой лабораторной работы это 1.1.1).
    3. Нажмите «ОК».Пакет Google ID
    4. Повторите шаги 1–3, но вместо этого выполните поиск по запросу «play-services-auth» и выберите строку с «com.google.android.gms» в качестве идентификатора группы и «play-services-auth» в качестве имени артефакта. Авторизация Play Services
  4. Нажмите «ОК».Готовые зависимости

6. Нижний поток листа

Нижний поток листа

В нижнем листе используется API Credential Manager для упрощения входа пользователей в ваше приложение с помощью учётных записей 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.
   //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() : шаблон конструктора используется для настройки параметров.
      • .setFilterByAuthorizedAccounts(true) : определяет, разрешить ли пользователю выбирать из всех учётных записей Google или только из тех, которые уже авторизовали приложение. В этом случае значение равно true, что означает, что запрос будет выполнен с использованием учётных данных, которые пользователь ранее авторизовал для использования с этим приложением, если таковые имеются.
      • .setServerClientId(webClientId) : устанавливает идентификатор клиента сервера, который является уникальным идентификатором бэкенда вашего приложения. Это необходимо для получения токена идентификатора.
      • .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 = ... : создается новый GetCredentialRequest с googleIdOptionFalse .
      • signIn(requestFalse, context) : пытается авторизовать пользователя с новым запросом, который позволяет использовать любую учетную запись.

По сути, этот код подготавливает запрос к API диспетчера учётных данных для получения токена Google ID для пользователя с использованием предоставленных конфигураций. Затем GetCredentialRequest можно использовать для запуска пользовательского интерфейса диспетчера учётных данных, где пользователь может выбрать свою учётную запись Google и предоставить необходимые разрешения.

fun generateSecureRandomNonce(byteLength: Int = 32): String : определяет функцию generateSecureRandomNonce . Она принимает целочисленный аргумент byteLength (со значением по умолчанию 32), который задаёт желаемую длину одноразового значения в байтах. Функция возвращает строку, которая будет представлять собой случайные байты в кодировке 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. Это часто требуется, чтобы сделать одноразовый код немного короче и компактнее.
    • .encodeToString(randomBytes) : кодирует случайные байты в строку Base64 и возвращает ее.

Подводя итог, эта функция генерирует криптографически стойкий случайный одноразовый код заданной длины, кодирует его с помощью URL-безопасного Base64 и возвращает результирующую строку. Это стандартная практика генерации одноразовых кодов, безопасных для использования в контекстах, требующих повышенной безопасности.

Сделайте запрос на вход

Теперь, когда мы можем создать наш запрос на вход, мы можем использовать Credential Manager, чтобы использовать его для входа. Для этого нам нужно создать функцию, которая обрабатывает проходящие запросы на вход с помощью 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 ID).
  • context : Контекст Android, необходимый для взаимодействия с системой.

Для тела функции:

  • val credentialManager = CredentialManager.create(context) : Создаёт экземпляр CredentialManager, который является основным интерфейсом для взаимодействия с API Credential Manager. Именно так приложение начинает процесс входа.
  • 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) : Здесь происходит фактический вызов 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 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) : записывает исключение в logcat Android с помощью Log.e, который используется для ошибок. В него будет включена трассировка стека исключения для облегчения отладки. Также для развлечения добавлен злой смайлик.
  • return e : Функция возвращает исключение, если таковое было перехвачено, или null, если вход был успешным.

Подводя итог, можно сказать, что этот код обеспечивает способ обработки входа пользователя с использованием 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. Поток кнопок

Кнопка Flow GIF

Кнопка «Войти через Google» упрощает для пользователей регистрацию или вход в ваше Android-приложение с использованием существующей учётной записи Google. Они нажмут на неё, если закроют нижнюю панель или просто предпочтут явно использовать учётную запись Google для входа или регистрации. Для разработчиков это означает более плавный процесс адаптации и меньше проблем при регистрации.

Хотя это можно сделать с помощью стандартной кнопки «Создать» Jetpack, мы будем использовать предварительно одобренный значок бренда со страницы «Руководство по брендингу для входа с помощью Google» .

Добавить значок бренда в проект

  1. Загрузите ZIP-архив предварительно одобренных значков брендов здесь.
  2. Распакуйте файл signin-assest.zip из архива (это зависит от операционной системы вашего компьютера). Теперь вы можете открыть папку signin-assets и просмотреть доступные значки. Для этой лабораторной работы мы будем использовать signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png .
  3. Скопировать файл
  4. Вставьте в проект в Android Studio в res > drawable , щелкнув правой кнопкой мыши по папке drawable и нажав «Вставить» (вам может потребоваться развернуть папку res , чтобы увидеть ее).Рисуемый
  5. Появится диалоговое окно с предложением переименовать файл и подтвердить каталог, в который он будет добавлен. Переименуйте ресурс в siwg_button.png и нажмите «ОК». Кнопка добавления

Код потока кнопок

Этот код будет использовать ту же функцию signIn() , что и для BottomSheet() , но вместо 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) : объявляет функцию с именем ButtonUI , которая принимает webClientId (идентификатор клиента вашего проекта Google Cloud) в качестве аргумента.

val context = LocalContext.current : Возвращает текущий контекст Android. Это необходимо для различных операций, включая запуск компонентов пользовательского интерфейса.

val coroutineScope = rememberCoroutineScope() : Создаёт область действия сопрограммы. Она используется для управления асинхронными задачами, позволяя коду выполняться без блокировки основного потока. rememberCoroutineScope () — это компонуемая функция из Jetpack Compose, которая предоставляет область действия, привязанную к жизненному циклу компонуемого объекта.

val onClick: () -> Unit = { ... } : Это создаёт лямбда-функцию, которая будет выполнена при нажатии кнопки. Лямбда-функция:

  • val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build() : Эта часть создаёт объект GetSignInWithGoogleOption . Этот объект используется для указания параметров процесса «Войти через Google». Для него требуются webClientId и одноразовое число (случайная строка, используемая для обеспечения безопасности).
  • 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) : делает изображение кликабельным, и при щелчке выполняется ранее определенная лямбда-функция 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. Заключение

Вы завершили эту практику! Дополнительную информацию или помощь по функции «Войти через 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
}