美文网首页Flutter学习
App-Link配置记录-源码解析

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

作者: 余瑜雨鱼 | 来源:发表于2020-05-28 23:57 被阅读0次

背景简介

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);
    }
}

相关文章

网友评论

    本文标题:App-Link配置记录-源码解析

    本文链接:https://www.haomeiwen.com/subject/bdrtzhtx.html