1. Прежде чем начать
В этом практическом занятии вы узнаете, как реализовать вход через Google на Android с помощью Credential Manager.
Предварительные требования
- Базовое понимание использования Kotlin для разработки под Android.
- Базовое понимание Jetpack Compose (более подробную информацию можно найти здесь ).
Что вы узнаете
- Как создать проект в Google Cloud
- Как создать OAuth-клиенты в консоли Google Cloud
- Как реализовать вход через Google с помощью всплывающего окна внизу страницы
- Как реализовать вход через Google с помощью кнопки
Что вам нужно
- Android Studio (скачать здесь )
- Компьютер, соответствующий системным требованиям Android Studio.
- Компьютер, соответствующий системным требованиям эмулятора Android.
- Установка Java и комплекта разработки Java (JDK)
2. Создайте проект в Android Studio.
Продолжительность: 3:00 - 5:00
Для начала нам нужно создать новый проект в Android Studio:
- Откройте Android Studio
- Нажмите «Новый проект».

- Выберите «Телефон» и «Планшет» , затем «Пустое действие».

- Нажмите Далее
- Теперь пришло время настроить несколько компонентов проекта:
- Название : это название вашего проекта.
- Название пакета : это поле будет заполнено автоматически на основе названия вашего проекта.
- Место сохранения : по умолчанию это должна быть папка, в которую Android Studio сохраняет ваши проекты. Вы можете изменить это место на любое другое по своему усмотрению.
- Минимальный SDK : это самая низкая версия Android SDK, для которой предназначено ваше приложение. В этом CodeLab мы будем использовать API 36 (Baklava).

- Нажмите «Завершить»
- Android Studio создаст проект и загрузит все необходимые зависимости для базового приложения, это может занять несколько минут. Чтобы увидеть это в действии, просто нажмите на значок сборки:

- После завершения этого процесса Android Studio должен выглядеть примерно так:

3. Настройте свой проект в Google Cloud.
Создайте проект в Google Cloud.
- Перейдите в консоль Google Cloud.
- Откройте свой проект или создайте новый проект.



- API и сервисы Click

- Перейдите на экран подтверждения OAuth.

- Для продолжения вам необходимо заполнить поля в разделе «Обзор» . Нажмите «Начать» , чтобы приступить к заполнению этой информации:

- Название приложения : Название этого приложения, которое должно совпадать с тем, что вы использовали при создании проекта в Android Studio.
- Адрес электронной почты службы поддержки пользователей : здесь будет отображаться учетная запись Google, под которой вы вошли в систему, а также все группы Google, которыми вы управляете.

- Аудитория :
- Это внутренний вариант приложения, используемого только внутри вашей организации. Если ваша организация не связана с проектом Google Cloud, вы не сможете выбрать этот вариант.
- Мы будем использовать внешнее приложение.

- Контактная информация : Здесь можно указать любой адрес электронной почты, который вы хотите использовать в качестве контактного лица для подачи заявки.

- Ознакомьтесь с политикой Google API Services: Политика в отношении пользовательских данных.
- После ознакомления с Политикой обработки пользовательских данных и принятия её условий, нажмите «Создать».

Настройка клиентов 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, связанным с этим кодом доступа.
- Что такое подпись SHA-1?
Для веб-клиента нам нужно лишь имя, которое вы хотите использовать для идентификации клиента в консоли.
Создание Android OAuth 2.0 клиента
- Перейдите на страницу «Клиенты» .

- Нажмите «Создать клиента».

- В качестве типа приложения выберите Android.
- Вам потребуется указать имя пакета вашего приложения.
- В Android Studio нам потребуется получить SHA-1-подпись нашего приложения и скопировать/вставить её сюда:
- Перейдите в Android Studio и откройте терминал.
- Выполните эту команду: 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: Эта команда предоставляет пароль для закрытого ключа указанного псевдонима.
-
- Скопируйте значение подписи SHA-1:

- Вернитесь в окно Google Cloud и вставьте значение подписи SHA-1:
- Теперь ваш экран должен выглядеть примерно так, и вы можете нажать кнопку «Создать» :


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

- Нажмите «Создать».

- Скопируйте идентификатор клиента из всплывающего окна, он понадобится вам позже.

Теперь, когда у нас настроены все OAuth-клиенты, мы можем вернуться в Android Studio и создать наше приложение для Android "Вход через Google"!
4. Настройка виртуального устройства Android
Для быстрого тестирования вашего приложения без физического устройства Android вам потребуется создать виртуальное устройство Android, на котором вы сможете собрать и сразу же запустить приложение из Android Studio. Если вы хотите тестировать приложение с помощью физического устройства Android, вы можете следовать инструкциям из документации разработчика Android.
Создайте виртуальное устройство Android.
- В Android Studio откройте Диспетчер устройств.

- Нажмите кнопку «+» > Создать виртуальное устройство

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

- Теперь вы можете настроить устройство для своего проекта, присвоив ему уникальное имя, выбрав версию Android, на которой будет работать устройство, и многое другое. Убедитесь, что для API установлено значение API 36 "Baklava"; Android 16 , затем нажмите "Готово".

- Новое устройство должно отобразиться в Диспетчере устройств. Чтобы убедиться, что устройство работает, нажмите на него.
рядом с только что созданным вами устройством
- Устройство должно работать!

Войдите в систему виртуального устройства Android.
Созданное вами устройство работает, теперь, чтобы избежать ошибок при тестировании входа через Google, нам потребуется войти в систему устройства с помощью учетной записи Google.
- Перейдите в Настройки:
- Нажмите на центр экрана виртуального устройства и проведите пальцем вверх.

- Найдите приложение «Настройки» и нажмите на него.

- Нажмите на Google в настройках.

- Нажмите «Войти» и следуйте инструкциям, чтобы войти в свой аккаунт Google.

- Теперь вы должны войти в систему на устройстве.

Ваше виртуальное устройство Android готово к тестированию!
5. Добавьте зависимости
Продолжительность 5:00
Для выполнения вызовов API по протоколу OAuth нам сначала необходимо интегрировать необходимые библиотеки, которые позволят нам отправлять запросы на аутентификацию и использовать идентификаторы Google для этих запросов:
- libs.googleid
- libs.play.services.auth
- Перейдите в меню Файл > Структура проекта:

- Затем перейдите в раздел «Зависимости» > «Приложение» > «+» > «Зависимость библиотеки» .

- Теперь нам нужно добавить наши библиотеки:
- В диалоговом окне поиска введите googleid и нажмите «Поиск».
- Должна быть только одна запись, выберите её и самую последнюю доступную версию (на момент проведения этого семинара это версия 1.1.1).
- Нажмите ОК

- Повторите шаги 1-3, но вместо этого найдите "play-services-auth" и выберите строку с идентификатором группы "com.google.android.gms" и именем артефакта "play-services-auth".

- Нажмите ОК

6. Нижняя поверхностная эрозия

В нижней части экрана используется API Credential Manager для упрощения входа пользователей в ваше приложение с помощью их учетных записей Google на Android . Он разработан для быстрого и удобного использования, особенно для постоянных пользователей. Этот процесс должен запускаться при запуске приложения.
Сформировать запрос на вход в систему.
- Для начала удалите функции
Greeting()иGreetingPreview()из файлаMainActivity.kt, они нам не понадобятся. - Теперь нам нужно убедиться, что необходимые для этого проекта пакеты импортированы. Добавьте следующие операторы
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 - Далее нам нужно создать функцию для формирования запроса к нижнему листу. Вставьте этот код ниже класса 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)
}
}
}
}
}
Теперь мы можем сохранить наш проект ( Файл > Сохранить ) и запустить его:
- Нажмите кнопку запуска:

- После запуска приложения на эмуляторе должно появиться всплывающее окно входа в систему. Нажмите « Продолжить» , чтобы проверить вход в систему.

- Вы должны увидеть всплывающее сообщение, подтверждающее успешный вход в систему!

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

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

- Появится диалоговое окно с предложением переименовать файл и подтвердить каталог, в который он будет добавлен. Переименуйте файл в 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)
}
}
}
}
}
}
Теперь мы можем сохранить наш проект ( Файл > Сохранить ) и запустить его:
- Нажмите кнопку запуска:

- После запуска приложения на эмуляторе должно появиться окно BottomSheet. Щелкните за его пределами, чтобы закрыть его.

- Теперь вы должны увидеть созданную нами кнопку в приложении. Нажмите на неё, чтобы открыть диалоговое окно входа в систему.

- Нажмите на свою учетную запись, чтобы войти!
8. Заключение
Вы завершили этот практический урок! Для получения дополнительной информации или помощи по теме «Вход через Google на Android» см. раздел «Часто задаваемые вопросы» ниже:
Часто задаваемые вопросы
- Stackoverflow
- Руководство по устранению неполадок в диспетчере учетных данных Android
- Часто задаваемые вопросы о диспетчере учетных данных Android
- Центр поддержки проверки приложений OAuth
Полный код файла MainActivity.kt
Для справки, вот полный код файла MainActivity.kt:
package com.example.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.credentials.GetCredentialException
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
signIn(requestFalse, context)
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
signIn(coroutineScope, request, context)
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
var e: Exception? = null
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
return e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
}
return e
}