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

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

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

Предварительные требования

  • Базовое понимание использования Kotlin для разработки под Android.
  • Базовое понимание Jetpack Compose (более подробную информацию можно найти здесь ).

Что вы узнаете

  • Как создать проект в Google Cloud
  • Как создать OAuth-клиенты в консоли Google Cloud
  • Как реализовать вход через Google с помощью всплывающего окна внизу страницы
  • Как реализовать вход через 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
  8. После завершения этого процесса Android Studio должен выглядеть примерно так:Проект Android Studio создан

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

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

  1. Перейдите в консоль Google Cloud.
  2. Откройте свой проект или создайте новый проект. GCP создает новый проектGCP создает новый проект 2GCP создает новый проект 3
  3. API и сервисы ClickAPI и сервисы GCP
  4. Перейдите на экран подтверждения OAuth. Экран согласия GCP OAuth
  5. Для продолжения вам необходимо заполнить поля в разделе «Обзор» . Нажмите «Начать» , чтобы приступить к заполнению этой информации: Кнопка «Начать работу» в GCP
    • Название приложения : Название этого приложения, которое должно совпадать с тем, что вы использовали при создании проекта в Android Studio.
    • Адрес электронной почты службы поддержки пользователей : здесь будет отображаться учетная запись Google, под которой вы вошли в систему, а также все группы Google, которыми вы управляете.
    Информация о приложении GCP
    • Аудитория :
      • Это внутренний вариант приложения, используемого только внутри вашей организации. Если ваша организация не связана с проектом Google Cloud, вы не сможете выбрать этот вариант.
      • Мы будем использовать внешнее приложение.
    Аудитория GCP
    • Контактная информация : Здесь можно указать любой адрес электронной почты, который вы хотите использовать в качестве контактного лица для подачи заявки.
    Контактная информация GCP
    • Ознакомьтесь с политикой Google API Services: Политика в отношении пользовательских данных.
  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, связанным с этим кодом доступа.

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

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

  1. Перейдите на страницу «Клиенты» .Клиенты GCP
  2. Нажмите «Создать клиента».Создание клиентов в GCP
  3. В качестве типа приложения выберите Android.
  4. Вам потребуется указать имя пакета вашего приложения.
  5. В Android Studio нам потребуется получить SHA-1-подпись нашего приложения и скопировать/вставить её сюда:
    1. Перейдите в Android Studio и откройте терминал.
    2. Выполните эту команду: Mac/Linux:
      keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
      
      Окна:
        keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
      
      Эта команда предназначена для вывода подробной информации о конкретной записи (псевдониме) в хранилище ключей.
      • -list : Эта опция указывает keytool вывести список содержимого хранилища ключей.
      • -v : Эта опция включает подробный вывод, предоставляя более детальную информацию о записи.
      • -keystore ~/.android/debug.keystore : Этот параметр указывает путь к файлу хранилища ключей.
      • -alias androiddebugkey : Эта опция указывает псевдоним (имя записи) ключа, который вы хотите проверить.
      • -storepass android : Эта команда предоставляет пароль для файла хранилища ключей.
      • -keypass android : Эта команда предоставляет пароль для закрытого ключа указанного псевдонима.
    3. Скопируйте значение подписи SHA-1:
    Подпись SHA
    1. Вернитесь в окно Google Cloud и вставьте значение подписи SHA-1:
  6. Теперь ваш экран должен выглядеть примерно так, и вы можете нажать кнопку «Создать» : Сведения о клиенте AndroidКлиент для Android

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

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

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

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

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

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

  1. В Android Studio откройте Диспетчер устройств.Диспетчер устройств
  2. Нажмите кнопку «+» > Создать виртуальное устройство Создать виртуальное устройство
  3. Здесь вы можете добавить любое устройство, необходимое для вашего проекта. Для целей этого практического занятия выберите «Средний телефон» , затем нажмите «Далее».Средний телефон
  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
  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() : Для настройки параметров используется шаблон проектирования Builder.
      • .setFilterByAuthorizedAccounts(true) : Указывает, разрешать ли пользователю выбирать из всех учетных записей Google или только из тех, которые уже авторизовали приложение. В данном случае значение установлено на true, что означает, что запрос будет выполнен с использованием учетных данных, которые пользователь ранее авторизовал для использования с этим приложением, если таковые имеются.
      • .setServerClientId(webClientId) : Устанавливает идентификатор клиента сервера, который является уникальным идентификатором для бэкэнда вашего приложения. Это необходимо для получения токена ID.
      • .setNonce(generateSecureRandomNonce()) : Устанавливает nonce — случайное значение — для предотвращения атак повторного воспроизведения и обеспечения связи токена 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), указывающий желаемую длину nonce в байтах. Она возвращает строку, которая будет представлять собой закодированное в Base64 представление случайных байтов.

  • val randomBytes = ByteArray(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) : Эта функция кодирует случайные байты в строку Base64 и возвращает её.

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

Отправьте запрос на вход в систему.

Теперь, когда мы можем сформировать запрос на вход в систему, мы можем использовать диспетчер учетных данных для его авторизации. Для этого нам нужно создать функцию, которая обрабатывает передачу запросов на вход в систему с помощью диспетчера учетных данных, а также обрабатывает распространенные исключения, с которыми мы можем столкнуться.

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

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

Реализуйте поток данных из нижней панели приложения.

Теперь мы можем настроить вызов для запуска потока BottomSheet в нашем классе MainActivity , используя следующий код и идентификатор клиента веб-приложения, который мы скопировали ранее из консоли 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)
                }
            }
        }
    }
}

Теперь мы можем сохранить наш проект ( Файл > Сохранить ) и запустить его:

  1. Нажмите кнопку запуска:Запуск проекта
  2. После запуска приложения на эмуляторе должно появиться всплывающее окно входа в систему. Нажмите « Продолжить» , чтобы проверить вход в систему.Нижний лист
  3. Вы должны увидеть всплывающее сообщение, подтверждающее успешный вход в систему! Успех нижнего листа

7. Последовательность нажатия кнопок

Button Flow GIF

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

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

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

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

Код обработки нажатия кнопки

В этом коде будет использоваться та же функция 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 и 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) : Делает изображение кликабельным, и при нажатии на него выполняется ранее определенная лямбда-функция 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
}