App-Link配置记录-源码解析

背景简介

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文件,注意文件名必须是这个,后面会解释。

打开Android Studio Tools->App Links Assistant
image.png

当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>

坑点

  1. app在安装验证时,会验证Androidmanifest里配置的所有域名,而不是配置android:autoVerify="true"的Activity下的域名,既然这样,autoVerify应该配置在application标签比较合理。
  2. 校验所有域名时,一旦有一个域名不符合正则,代码会抛异常,流程就断了,网上有很多说要翻墙什么的。其实根本不需要,真正的原因应该就是域名校验的问题。
  3. 浏览器唤起时,只有在chrome或者基于chrome的浏览器才可以,市面上大部分都不支持,例如小米自带的浏览器,qq浏览器,百度浏览器等都不支持。
  4. 总的来说在国内意义不大,像微信内置浏览器拦截了系统的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);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343