背景简介
Deep Link 和 App Link 官方文档
Deep link 是基于intentFilter(action ,category,data),可以让用户直接转到特定应用上的网址,但是如果多个应用都符合相同的intent,例如发邮件,打开网页,系统不知道用户希望用哪个应用打开,就会弹框让用户自己选。这时候就可以用到app-link。
App Link ,Android 6.0及以上才支持,可以理解为在deep-link上做了优化,如果有很符合的intent,会直接打开相关应用,不会再弹框提示。
属性 | Deep Link | App Link |
---|---|---|
intent 网址协议 | http、https 或自定义协议 | 需要 http 或 https |
intent 操作 | 任何操作 | 需要 android.intent.action.VIEW |
intent 类别 | 任何类别 | 需要 android.intent.category.BROWSABLE 和 android.intent.category.DEFAULT |
链接验证 | 无 | 需要通过 HTTPS 协议在您的网站上发布 Digital Asset Links 文件 |
用户体验 | 可能会显示一个消除歧义对话框,以供用户选择用于打开链接的应用 | 无对话框;您的应用会打开以处理您的网站链接 |
兼容性 | 所有 Android 版本 | Android 6.0 及更高版本 |
配置
主要流程官网上都有,简单说下在demo上的配置流程
在Androidmanifest配置activity
<activity android:name=".applink.AppLinkActivity">
<!--这个必须要有-->
<intent-filter android:autoVerify="true">
<!--这个action 和 这两个category必须要有-->
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!--
系统会匹配
https://example.test.com
http://example.test.com
-->
<data
android:scheme="https" />
<data android:scheme="http" />
<data android:host="example.test.com" />
</intent-filter>
</activity>
处理Activity
public class AppLinkActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ATTENTION: This was auto-generated to handle app links.
Intent appLinkIntent = getIntent();
String appLinkAction = appLinkIntent.getAction();
Uri appLinkData = appLinkIntent.getData();
android.util.Log.i("AppLink","appLinkData is "+appLinkData);
}
}
配置生成assetlinks.json文件,注意文件名必须是这个,后面会解释。
当assetlinks.json已经配置好了,需要把assetlinks.json上传到设置的域名下的/.well-known/文件夹下,通过上面图片中的Link and Verify按钮验证,验证通过后就可以愉快的运行app了,注意app link只会在应用安装时校验,注意官网有一句说明
确认要与您的应用关联的网站列表,并且确认托管的 JSON 文件有效后,请立即在您的设备上安装应用。等待至少 20 秒,让系统完成异步验证流程。
这一步就是在异步请求验证app-link的合法性。
除了通过android studio自带的工具验证json文件外,还可以通过以下命令验证
https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://yourhost&relation=delegate_permission/common.handle_all_urls
保证以上链接在公网能访问就行,需要梯子
验证
adb验证
adb shell dumpsys package domain-preferred-apps //输出当前手机所有link的包名
Package: com.vdian.android.lib.testforgradle // 关注自己的包名就行
Domains: example.test.com //之前设置的域名
Status: always : 200000017 //这里有4种状态
undefined — app没有在manifest中启用链接自动验证功能。
ask — app验证失败(会通过打开方式对话框询问用户)
always — app通过了验证(点击这个域名总是打开这个app)
never — app通过了验证,但是系统设置关闭了此功能。
adb 直接唤起,如果是成功状态,可以直接唤起APP的指定页面
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://example.test.com"
浏览器验证
<a href="https://wdb-applink.weidian.com">点我link</a>
坑点
- app在安装验证时,会验证Androidmanifest里配置的所有域名,而不是配置android:autoVerify="true"的Activity下的域名,既然这样,autoVerify应该配置在application标签比较合理。
- 校验所有域名时,一旦有一个域名不符合正则,代码会抛异常,流程就断了,网上有很多说要翻墙什么的。其实根本不需要,真正的原因应该就是域名校验的问题。
- 浏览器唤起时,只有在chrome或者基于chrome的浏览器才可以,市面上大部分都不支持,例如小米自带的浏览器,qq浏览器,百度浏览器等都不支持。
- 总的来说在国内意义不大,像微信内置浏览器拦截了系统的deeplink和applink,自己维护了一套(https://wiki.open.qq.com/index.php?title=mobile/%E5%BA%94%E7%94%A8%E5%AE%9D%E5%BE%AE%E4%B8%8B%E8%BD%BD),需要申请白名单才行。
源码解析
PackageManagerService类
// 判断是否需要验证host
// If any filters need to be verified, then all need to be.
boolean needToVerify = false;
for (PackageParser.Activity a : pkg.activities) {
for (ActivityIntentInfo filter : a.intents) {
if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {
if (DEBUG_DOMAIN_VERIFICATION) {
Slog.d(TAG,
"Intent filter needs verification, so processing all filters");
}
needToVerify = true; //只要有1个Actvity带有autoVerify,那就是true
break;
}
}
}
//如果需要验证,那就获取当前的配置的host
if (needToVerify) {
final int verificationId = mIntentFilterVerificationToken++;
for (PackageParser.Activity a : pkg.activities) {
for (ActivityIntentInfo filter : a.intents) {
if (filter.handlesWebUris(true) && needsNetworkVerificationLPr(filter)) {
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
"Verification needed for IntentFilter:" + filter.toString());
// 只要系统权限没有关闭,这里都会把host加载进去
mIntentFilterVerifier.addOneIntentFilterVerification(
verifierUid, userId, verificationId, filter, packageName);
count++;
}
}
}
}
}
PackageManagerService类
//紧接着发送广播
private void sendVerificationRequest(int verificationId, IntentFilterVerificationState ivs) {
//注意这里是广播的action
Intent verificationIntent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION);
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID,
verificationId);
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME,
getDefaultScheme());
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS,
ivs.getHostsString()); //所有的host
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME,
ivs.getPackageName());
verificationIntent.setComponent(mIntentFilterVerifierComponent);
verificationIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
final long whitelistTimeout = getVerificationTimeout();
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setTemporaryAppWhitelistDuration(whitelistTimeout);
DeviceIdleController.LocalService idleController = getDeviceIdleController();
idleController.addPowerSaveTempWhitelistApp(Process.myUid(),
mIntentFilterVerifierComponent.getPackageName(), whitelistTimeout,
UserHandle.USER_SYSTEM, true, "intent filter verifier");
mContext.sendBroadcastAsUser(verificationIntent, UserHandle.SYSTEM,
null, options.toBundle());
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
"Sending IntentFilter verification broadcast");
}
这个广播的接收在IntentFilterVerificationReceiver这个类
这里有第二个坑点,会验证pms里搜集的所有host,一旦有一个host不符合正则校验,整个流程就断了
最合理的应该是 只校验配置android:autoVerify="true"的host。
try {
ArrayList<String> sourceAssets = new ArrayList<String>();
for (String host : hostList) {
// "*.example.tld" is validated via https://example.tld
if (host.startsWith("*.")) {
host = host.substring(2);
}
sourceAssets.add(createWebAssetString(scheme, host)); //这里正则校验
finalHosts.add(host);
}
extras.putStringArrayList(DirectStatementService.EXTRA_SOURCE_ASSET_DESCRIPTORS,
sourceAssets);
} catch (MalformedURLException e) {
Log.w(TAG, "Error when processing input host: " + e.getMessage());
sendErrorToPackageManager(context.getPackageManager(), verificationId);
return;
}
private String createWebAssetString(String scheme, String host) throws MalformedURLException {
//校验不通过就抛异常了。。
if (!Patterns.DOMAIN_NAME.matcher(host).matches()) {
throw new MalformedURLException("Input host is not valid.");
}
if (!scheme.equals("http") && !scheme.equals("https")) {
throw new MalformedURLException("Input scheme is not valid.");
}
return String.format(WEB_ASSET_FORMAT, new URL(scheme, host, "").toString());
}
如果以上校验都通过了,会启动DirectStatementService去验证,最终走的逻辑是IsAssociatedCallable里的verifyOneSource方法。而verifyOneSource方法里调用了DirectStatementRetriever的retrieveStatements方法。
DirectStatementRetriever类
//注意这个常量,说明为什么文件名和路径都必须按照官方的来
private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";
@Override
public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
if (source instanceof AndroidAppAsset) {
return retrieveFromAndroid((AndroidAppAsset) source);
} else if (source instanceof WebAsset) { //这里的source都是webAsset类型
return retrieveFromWeb((WebAsset) source);
} else {
throw new AssociationServiceException("Namespace is not supported.");
}
}
DirectStatementRetriever类
//通过http获取服务端配置,和本地校验,到这一步流程就走完了。
private Result retrieveStatementFromUrl(String urlString, int maxIncludeLevel,
AbstractAsset source)
throws AssociationServiceException {
List<Statement> statements = new ArrayList<Statement>();
if (maxIncludeLevel < 0) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
WebContent webContent;
try {
URL url = new URL(urlString);
if (!source.followInsecureInclude()
&& !url.getProtocol().toLowerCase().equals("https")) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url,
HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS,
HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY);
} catch (IOException | InterruptedException e) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
try {
ParsedStatement result = StatementParser
.parseStatementList(webContent.getContent(), source);
statements.addAll(result.getStatements());
for (String delegate : result.getDelegates()) {
statements.addAll(
retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
.getStatements());
}
return Result.create(statements, webContent.getExpireTimeMillis());
} catch (JSONException | IOException e) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
}