移动开发
Android 上的 Google Play 服务:可用性检查、用 Credential Manager 实现 Google 登录与推送通知
阅读需 11 分钟
Android 上的 Google Play 服务:可用性检查、用 Credential Manager 实现 Google 登录与推送通知
Filmoteca 是我项目归档里的一款 Android 应用,最初只是一个用 Kotlin 写的简单应用,用来记录看过的电影。读硕士期间,我给它加了两项依赖 Google Play 服务的功能:用 Google 账号登录的界面,以及基于 Firebase Cloud Messaging 的推送通知。这两项功能代码都不多,几乎所有麻烦都出在配置上,这篇文章讲的就是这些。
Play 服务到底是什么
Google Play 服务是预装在带 Google Play 的 Android 设备上的系统组件。它为应用提供定位、地图、登录等大量能力,而不需要把这些代码都打包进 APK。每个 API 都是独立的 Gradle 依赖,用到什么就引入什么:
dependencies {
implementation("com.google.android.gms:play-services-location:<version>")
// one artifact per API: ads, games, ML Kit, nearby, ...
}
由此带来两个实际影响。第一,有些 API 需要在 manifest 中额外声明:比如广告 SDK,如果缺少应用 ID 的 meta-data,应用一启动就会崩溃。第二,Play 服务并不保证一定存在——在某些市场,没有 Google Play 的设备很常见——所以应用在依赖它之前必须先检查。
先检查是否可用
检查代码很短,值得放在每个依赖 Play 服务的功能入口处:
fun ensurePlayServices(activity: Activity): Boolean {
val api = GoogleApiAvailability.getInstance()
val result = api.isGooglePlayServicesAvailable(activity)
if (result == ConnectionResult.SUCCESS) return true
if (api.isUserResolvableError(result)) {
// e.g. outdated Play Services: let the system offer an update
api.getErrorDialog(activity, result, REQUEST_PLAY_SERVICES)?.show()
} else {
Log.w("PlayServices", "This device is not supported")
}
return false
}
区分可解决的错误(服务版本过旧或被禁用,用户可以自行修复)和不受支持的设备很重要:前者要引导用户处理,后者则应隐藏该功能,让应用的其余部分照常运行。
没人讲清楚的 OAuth 客户端与 SHA-1
在 Google 知道是哪个应用发起请求之前,Google 登录是无法工作的。需要在 Google Cloud 控制台中创建一个Android 类型的 OAuth 客户端 ID,填写两项信息:应用的包名,以及为构建包签名的证书的 SHA-1 指纹。
指纹可以从 keystore 中读取,也可以让 Gradle 为每个构建变体打印出来:
keytool -list -v -keystore ~/.android/debug.keystore
./gradlew signingReport
我在这里踩坑后得到两条教训:
- debug 和 release 构建使用不同的密钥签名,因此 SHA-1 也不同。两个都要登记,否则登录在你自己的电脑上正常,到了真正发布的版本里却会失败。
- keystore 密码不应该写在
build.gradle里。 在课程练习中,为了尽快跑通,我把密码直接写进了签名配置。现在我把它们放在版本控制之外,构建时再读取:
val props = Properties().apply {
rootProject.file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
}
android {
signingConfigs {
create("release") {
storeFile = file(props.getProperty("signing.storeFile", "release.keystore"))
storePassword = props.getProperty("signing.storePassword")
keyAlias = props.getProperty("signing.keyAlias")
keyPassword = props.getProperty("signing.keyPassword")
}
}
}
从 GoogleSignInClient 到 Credential Manager
我最先学到的是经典的 GoogleSignInClient 流程:构建请求个人资料和邮箱的 GoogleSignInOptions,创建客户端,在 onStart 中检查 getLastSignedInAccount(),用 startActivityForResult 启动 getSignInIntent(),再在 onActivityResult 中读取账号。它能用,但一次登录被拆散在四个回调里,而且这套 API 现在已经被弃用。
取而代之的是 Credential Manager,它是 Jetpack 中统一处理密码、通行密钥(passkey)和联合登录的 API。配合 Google ID 库,整个登录过程只需一次挂起调用:
val credentialManager = CredentialManager.create(context)
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(WEB_CLIENT_ID) // the web client ID of your backend, not the Android one
.build()
val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
suspend fun signIn(activity: Activity) {
try {
val result = credentialManager.getCredential(activity, request)
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
) {
val google = GoogleIdTokenCredential.createFrom(credential.data)
// send google.idToken to the backend and verify it there
}
} catch (e: GetCredentialException) {
Log.w("Auth", "Sign-in failed or cancelled", e)
}
}
这次迁移让我的理解有了几点变化:
- 带 SHA-1 的 Android OAuth 客户端仍然必不可少;但代码里传入的是 Web 客户端 ID,因为 ID 令牌本来就是要交给你的服务器去验证的。
- 应用不应该单方面信任收到的个人资料。ID 令牌要发送到后端,由后端验证后再建立会话。
- 退出登录使用
credentialManager.clearCredentialState(ClearCredentialStateRequest()),这样账号选择器就不会悄悄沿用上一个账号。
用 Firebase Cloud Messaging 实现推送通知
FCM 是从服务器向 Android、iOS 和 Web 客户端发送消息的免费通道。它的接入方式和登录如出一辙:配置为主,代码很少。
- 创建 Firebase 项目,用包名和 debug 版 SHA-1 注册应用。
- 把
google-services.json下载到app模块中,并应用com.google.gms.google-services插件。 - 添加 Firebase BoM 和消息组件,保证各个库的版本一致。
- 在 manifest 中声明一个
FirebaseMessagingService,设置android:exported="false",并添加MESSAGING_EVENT意图过滤器。 - 通过 meta-data 设置默认的通知图标、颜色和通知渠道。如果不设置图标,Android 会把应用图标显示成一个白色剪影。
真正需要重视的是注册令牌,也就是某一次应用安装的“地址”:
class PushService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
// send it to the backend, replacing the previous one
}
override fun onMessageReceived(message: RemoteMessage) {
// show a notification or update local state
}
}
当用户重新安装应用、清除应用数据、在新设备上恢复应用,或者实例 ID 被删除时,令牌都会变化。如果后端只保存首次启动时的令牌,这些用户就会在毫无察觉的情况下收不到通知。应把 onNewToken 当作唯一可信的来源,每次触发都同步更新服务器。在较新的 Android 版本上,应用还必须在运行时申请通知权限,之后才能显示任何通知。
我现在遵循的检查清单
- 在使用任何 Play 服务功能之前先检查
GoogleApiAvailability,并做好优雅降级。 - 为每一个用于签名发布版本的密钥登记 SHA-1。
- keystore 密码和客户端密钥一律不进代码仓库。
- 新的登录代码使用 Credential Manager,并在服务器端验证 ID 令牌。
- 按安装实例保存 FCM 令牌,并在
onNewToken中及时更新。