1. Прежде чем начать
В этой лабораторной работе вы узнаете, как реализовать вход с помощью Google на Android с помощью диспетчера учетных данных.
Предпосылки
- Базовое понимание использования Kotlin для разработки на Android
- Базовые знания Jetpack Compose (более подробную информацию можно найти здесь )
Чему вы научитесь
- Как создать проект Google Cloud
- Как создать клиентов OAuth в Google Cloud Console
- Как реализовать вход через Google с помощью потока Bottom Sheet
- Как реализовать вход через Google с помощью кнопки
Что вам нужно
- Android Studio (скачать здесь )
- Компьютер, соответствующий системным требованиям Android Studio
- Компьютер, соответствующий системным требованиям эмулятора Android.
2. Создайте проект Android Studio
Продолжительность 3:00 - 5:00
Для начала нам необходимо создать новый проект в Android Studio:
- Открыть Android Studio
- Нажмите «Новый проект».
- Выберите «Телефон и планшет» и «Очистить активность».
- Нажмите «Далее» .
- Теперь пришло время настроить несколько частей проекта:
- Имя : это название вашего проекта.
- Имя пакета : будет заполнено автоматически на основе имени вашего проекта.
- Расположение сохранения : по умолчанию это папка, в которой Android Studio сохраняет ваши проекты. Вы можете изменить ее на любое другое место по своему усмотрению.
- Минимальный SDK : это минимальная версия Android SDK, на которой работает ваше приложение. В этом практическом занятии мы будем использовать API 36 (Baklava).
- Нажмите «Готово».
- Android Studio создаст проект и загрузит все необходимые зависимости для базового приложения. Это может занять несколько минут. Чтобы увидеть процесс, просто нажмите на значок сборки:
- После завершения Android Studio должна выглядеть примерно так:
3. Настройте свой проект Google Cloud
Создайте проект Google Cloud
- Перейдите в Google Cloud Console.
- Откройте свой проект или создайте новый проект
- API и сервисы Click
- Перейти к экрану согласия OAuth
- Чтобы продолжить, вам потребуется заполнить поля в разделе «Обзор» . Нажмите «Начать» , чтобы начать заполнение этой информации:
- Имя приложения : имя этого приложения, которое должно совпадать с тем, которое вы использовали при создании проекта в Android Studio.
- Электронная почта службы поддержки пользователей : здесь будет указана учетная запись Google, с которой вы вошли в систему, и все группы Google, которыми вы управляете.
- Аудитория :
- Внутреннее — для приложения, используемого только внутри вашей организации. Если у вас нет организации, связанной с проектом Google Cloud, вы не сможете выбрать этот вариант.
- Мы будем использовать External.
- Контактная информация : это может быть любой адрес электронной почты, который вы хотите указать в качестве контактного лица для подачи заявки.
- Ознакомьтесь со статьей «Сервисы API Google: Политика в отношении пользовательских данных».
- После ознакомления и согласия с Политикой обработки данных пользователей нажмите «Создать».
Настройка клиентов 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, связанным с этим кодом входа.
- Что такое подпись SHA-1?
Для веб-клиента нам понадобится только имя, которое вы хотите использовать для идентификации клиента в консоли.
Создать клиент Android OAuth 2.0
- Перейти на страницу «Клиенты»
- Нажмите «Создать клиента».
- Выберите Android в качестве типа приложения.
- Вам нужно будет указать имя пакета вашего приложения.
- Из Android Studio нам потребуется получить подпись SHA-1 нашего приложения и скопировать/вставить ее сюда:
- Перейдите в Android Studio и откройте терминал.
- Выполните эту команду:
Эта команда предназначена для вывода списка сведений о конкретной записи (псевдониме) в хранилище ключей.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
: Предоставляет пароль для закрытого ключа указанного псевдонима.
-
- Скопируйте значение подписи SHA-1:
- Вернитесь в окно Google Cloud и вставьте значение подписи SHA-1:
- Теперь ваш экран должен выглядеть примерно так, и вы можете нажать «Создать» :
Создать веб-клиент OAuth 2.0
- Чтобы создать идентификатор клиента веб-приложения, повторите шаги 1–2 из раздела «Создание клиента Android» и выберите «Веб-приложение» в качестве типа приложения.
- Дайте клиенту имя (это будет клиент OAuth):
- Нажмите «Создать».
- Скопируйте идентификатор клиента из всплывающего окна. Он понадобится вам позже.
Теперь, когда наши клиенты OAuth полностью настроены, мы можем вернуться в Android Studio и создать приложение «Войти через Google» для Android!
4. Настройте виртуальное устройство Android
Для быстрого тестирования приложения без физического устройства Android вам потребуется создать виртуальное устройство Android, на котором вы сможете собрать и сразу запустить приложение из Android Studio. Если вы хотите протестировать приложение на физическом устройстве Android, следуйте инструкциям из документации Android Devloper.
Создайте виртуальное устройство Android
- В Android Studio откройте диспетчер устройств.
- Нажмите кнопку + > Создать виртуальное устройство.
- Здесь вы можете добавить любое устройство, необходимое для вашего проекта. Для целей этой практической работы выберите Medium Phone и нажмите «Далее».
- Теперь вы можете настроить устройство для своего проекта, дав ему уникальное имя, выбрав версию 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()
: шаблон конструктора используется для настройки параметров. -
.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)
}
}
}
}
}
Теперь мы можем сохранить наш проект ( Файл > Сохранить ) и запустить его:
- Нажмите кнопку запуска:
- После запуска приложения на эмуляторе вы увидите всплывающее окно входа BottomSheet. Нажмите «Продолжить» , чтобы протестировать вход.
- Вы должны увидеть всплывающее сообщение о том, что вход выполнен успешно!
7. Поток кнопок
Кнопка «Войти через Google» упрощает для пользователей регистрацию или вход в ваше Android-приложение с использованием существующей учётной записи Google. Они нажмут на неё, если закроют нижнюю панель или просто предпочтут явно использовать учётную запись Google для входа или регистрации. Для разработчиков это означает более плавный процесс адаптации и меньше проблем при регистрации.
Хотя это можно сделать с помощью стандартной кнопки «Создать» Jetpack, мы будем использовать предварительно одобренный значок бренда со страницы «Руководство по брендингу для входа с помощью Google» .
Добавить значок бренда в проект
- Загрузите ZIP-архив предварительно одобренных значков брендов здесь.
- Распакуйте файл signin-assest.zip из архива (это зависит от операционной системы вашего компьютера). Теперь вы можете открыть папку signin-assets и просмотреть доступные значки. Для этой лабораторной работы мы будем использовать
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png
. - Скопировать файл
- Вставьте в проект в Android Studio в res > drawable , щелкнув правой кнопкой мыши по папке drawable и нажав «Вставить» (вам может потребоваться развернуть папку res , чтобы увидеть ее).
- Появится диалоговое окно с предложением переименовать файл и подтвердить каталог, в который он будет добавлен. Переименуйте ресурс в 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)
}
}
}
}
}
}
Теперь мы можем сохранить наш проект ( Файл > Сохранить ) и запустить его:
- Нажмите кнопку запуска:
- После запуска приложений на эмуляторе должен появиться 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
}