Android M 的 App Links 实现详解
谷歌 2015 年的 I/O 大会上宣布了一个 新特性 :允许开发者将 app 和他们的 web 域名关联。这一举措是为了最小化用户遇到“打开方式”对话框的概率。
比如,我们安装了两个 Twitter 应用 - 官方的和 Falcon Pro 。当你在某个地方点击了 Twitter URL 的时候,你会看到如下的对话框:
但是在安卓 M 中,如果一个 app 明确的指定了 App 链接-这个对话框将不复存在。点击一个链接将立即打开官方的 app,没有第三方 app 的机会,更不会打开一个浏览器。
在上图的例子中,当你点击了那个链接,安卓系统会检查是否有一个 app 可以处理 twitter.com URL,然后跟 twitter.com 核对哪个 app(s)可以处理该域名的链接,这样我们就能避免影响用户。
注意安卓并不会在点击链接的时候才核对这些链接,因此在安卓决定使用哪个 app 之前并不会有网络阻塞。关于这点后面有更多讨论。
虽然这会使安卓更方便- 多数情况下,你确实希望点击一个链接打开的是最合适的那个 app- 但是对于那些喜欢使用第三方 app 的人来说,似乎是一件坏事。不过这种行为可以在 Android M 的系统设置中关掉。
app 开发者如何实现 App Links
实现的细节可以在 Android Developers' Preview 网站 上找到。
如果你有一个需要处理链接(比如 example.com)的 app,你应该:
- 有上传文件到 example.com 根路径的权限,如果没有,你无法让你的 app 成为这些链接的默认打开方式。
- 在 build.gradle 文件中设置 compileSdkVersion 'android-MNC'。
- 为每个这样的<intent-filter>标签
<intent-filter> <data android:scheme="http" /> ... </intent-filter>
添加属性 android:autoVerify="true"。http 也可以是 https。
注:这里对 filter 的写法略去了很多,其实官网(上面的那个链接)有完整的示例:
<activity ...> <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="http" android:host="www.android.com" /> <data android:scheme="https" android:host="www.android.com" /> </intent-filter> </activity>
认证是以主机名为单位的而不是以 intent filter 为单位的,因此从技术上讲,并不需要为每个标签都添加该属性,但是添加了也不会出什么问题。
创建 JSON 文件
为了能让安卓可以认证,你的 app 需要被允许使用 app 链接行为,为此,需要提供一个 JSON 文件,JSON 文件中需要包含 app 的 ID 以及 APK 的公钥证书。
这个文件必须包含一个 JSON 数组,数组中可以有一个或者多个对象,每个对象对应一个你想认证的 app ID:
[ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.myapp", "sha256_cert_fingerprints": ["6C:EC:C5:0E:34:AE....EB:0C:9B"] } } ]
比如,你现在有一个 com.example.myapp 的发行版 app,同时还有一个叫 com.example.myapp.beta 的 beta 版 app,你可以通过在数组中设置两个对象来允许两者都可以接受认证,每个对象带有各自的 app ID 和公钥值。
注意,这个文件的验证是非常严格的:数组中的每个对象都必须和上面的那个一模一样。在数组中添加了任意其他的对象,或者对象中有额外的属性都会导致整个验证失败。- 即便这个 app 对象是有效的。
为了防止为同一个构建注册了不同的 key,貌似一个 app 可以指定多个 SHA256 指纹证书,不管怎样,指纹证书可以通过如下方式获得:
echo | keytool -list -v -keystore app.keystore 2> /dev/null | grep SHA256:
上传 JSON 文件
创建完文件之后,你需要上传它同时保证它可以使用这个 URL 访问 http://example.com/.well-known/statements.json。
目前这个 URL 是 http 的,最终的 M 版本将只允许通过 HTTPS 访问该 URL。 在第一个 M 预览版中,重定向到 HTTPS,或者任何其他重定向(301,302 或者 307)貌似都会被忽略并被视为失败。
URL 的 scheme 和<intent-filter>标签中的 android:scheme 值是互不相干的,即使你有一个只接受 HTTPS URLs 的 filter,认证 URL 仍然需要通过 HTTP 访问。
在了解了安卓如何使用这些信息之后,我们来看看如果 debug 这个过程。
Android M 是如何实现 App Links 的
App 链接认证涉及到安卓系统的两个组建:Package Manager 和 Intent Filter Verifier。
PackageManager 是一个无处不在的标准组建 - 它负责验证所安装的 apk 是否有效,授予 app 权限,另外还可以通过它知道系统上安装了些什么 app。
而 Intent Filter Verifier 则是 Android M 上才有的新玩意儿。这个组建负责获取链接指向的 JSON 认证,解析它,验证它,然后将报告返回给 PackageManger。
虽然这个组建不是用户轻易就能替换的,但似乎系统中只能有一个活动状态的 Intent Filter Verifier - 想要注册成为一个 verifier,必须要有 android.permission.INTENT_FILTER_VERIFICATION_AGENT 权限,而这个权限只有签名了系统密钥的 app 才能得到。
你可以通过如下的命令查看当前激活的 intent filter verifier:
adb shell dumpsys package ifv
在第一个 M 预览版中,com.android.statementservice 完全扮演了这个角色。
How App Links are verified
App 链接认证在安装的时候就一次性完成。这就是为什么刚刚我们说不必在每次点击链接的时候都阻塞网络。
当一个 package 安装的时候,或者现有的 package 升级的时候:
- 1.PackageManager 对即将安装的 apk 做常规的验证。
- 2.如果成功,这个 package 将被安装,同时发出一个带有 android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION 的广播 intent,intent 中还携带有该 package 的信息。
- 3.Intent Filter Verifier 的广播接收器将获取这个广播。
- 4.从 package 的<intent-filter>标签中编译出一个特有主机名的列表。
- 5.verifier 尝试从每个特有的主机名中获取 statements.json。
- 6.每一个被获取的 JSON 文件都会检查它的 application ID 和安装包的证书。
- 7.只有当所有文件同时满足时,才会发送成功信息到 PackageManager,否则失败。
- 8.PackageManager 存储结果。
如果认证失败,app 链接将无法指向你的 app - 你的 app 会像往常一样出现在“打开方式”对话框中(除非另一个 app 通过了同一域名的验证)。
就我所了解的而言,认证只会在安装和升级的时候会发生,因此对大多数用户来说,再次通过验证的机会是在 app 下一次升级的时候。
Intent Filter Verifier 的行为
主机名
example.com 和 www.example.com 会被认为是两个独立的主机名。因此要求 statements.json 在两个主机名下都是可直达的。
比如,如果你将所有的请求都重定向到 http://www.example.com/* to https://example.com/*,则会让这个主机名的认证失败,从而导致整个 app 链接认证失败。
这种情况下,你可能需要为你的 web 服务器做特殊的配置,确保每个对于 statements.json 的请求都有能直接返回 HTTP 200。这个约束在后面的版本中可能会有所放松。
响应时间
如果 verifier 不能在 5 秒之内和你的 web 服务器建立链接并接收到 HTTP 响应,认证会失败。
缺少链接环境
同样的,如果在认证开始的时候设备是离线的,或者网络环境很差,认证也会失败。
HTTP 缓存
目前 Intent Filter Verifier 的实现基本遵循 HTTP 缓存规则。
如果你的 statements.json 响应包含了 Cache-Control: max-age=[seconds]头部,那么这个响应将被 verifier 缓存到磁盘。虽然 60 秒以下的'max-age'会被忽略,但是 60 秒似乎也足够了。同样的一个 Expires
header 也会被缓存。
如果你有 ETag 或者最近更新的 headers,那么 verifier 将在下一次视情况使用这些值来验证相应的主机。据我所知,如果这些 header 子退出之后没有明确指定缓存控制头,那么响应的缓存时间将是不确定的。
缓存头部只理会 http 200 的响应,如果是一个 404 响应,那么下次将忽略,verifier 需要直接连接主机。
Debugging App Links
当安卓系统试图认证你的 app 链接时,PackageManager 除了向 logcat 中报告 true/false 值之外,还有一些其他反馈,比如:
IntentFilter ActivityIntentInfo{1a61a0a com.example.myapp/.MainActivity} verified with result:true and hosts:example.com www.example.com
但是,你可在任意时刻向系统查询 package 的 app 链接认证状态:
adb shell dumpsys package d
这会返回如下的认证条目信息:
Package Name: com.example.myapp Domains: example.com www.example.com Status: always
可能会有多个关于你 package 的条目:一个是系统的,零个或者多个用户的- 有些用户的偏好覆盖了系统参数。
可能会产生的状态值大致如下:
- undefined — app 没有在 manifest 中启用链接自动验证功能。
- ask — app 验证失败(会通过打开方式对话框询问用户)
- always — app 通过了验证(点击这个域名总是打开这个 app)
- never — app 通过了验证,但是系统设置关闭了此功能。
如果你没能通过认证,你可以再次尝试重新安装同一版本:
adb install -r app/build/outputs/apk/app-debug.apk
如果你在安装的时候没有在服务器上看见 statements.json URL 的请求,你可以清空 Intent Verifier 服务的 HTTP 缓存,这样下次就会直接请求服务器:
adb shell pm clear com.android.statementservice
如果你不知道 statements.json 的内容是否正确返回,可以使用安卓模拟器的-tcpdump 选项来检查网络上发送的是什么 - 注意安卓 M 最终版出来之后就没那么容易了,因为数据是加密的。
还有一种选择,那就是使用模拟器的-http-proxy 选项,让所有的网络请求都通过代理,比如 Charles 代理。
总结
虽然在开始我担心 App Links 会取代目前安卓上非常酷的 intent 机制,但是也乐于看到这对于大多数情况都是有用的,况且如果用户不喜欢,使用简单的方法就可以把它关掉。
考虑到 verifier 服务对 JSON 解析和 HTTP 请求异常严格,希望这里所提到的一些细节对你实现 app linking 有所帮助。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论