App-Link配置記錄-源碼解析

背景簡介

Deep Link 和 App Link 官方文檔

Deep link 是基于intentFilter(action ,category,data),可以讓用戶直接轉(zhuǎn)到特定應(yīng)用上的網(wǎng)址,但是如果多個應(yīng)用都符合相同的intent,例如發(fā)郵件,打開網(wǎng)頁,系統(tǒng)不知道用戶希望用哪個應(yīng)用打開,就會彈框讓用戶自己選。這時候就可以用到app-link。

App Link ,Android 6.0及以上才支持,可以理解為在deep-link上做了優(yōu)化,如果有很符合的intent,會直接打開相關(guān)應(yīng)用,不會再彈框提示。

屬性 Deep Link App Link
intent 網(wǎng)址協(xié)議 http、https 或自定義協(xié)議 需要 http 或 https
intent 操作 任何操作 需要 android.intent.action.VIEW
intent 類別 任何類別 需要 android.intent.category.BROWSABLE 和 android.intent.category.DEFAULT
鏈接驗證 需要通過 HTTPS 協(xié)議在您的網(wǎng)站上發(fā)布 Digital Asset Links 文件
用戶體驗 可能會顯示一個消除歧義對話框,以供用戶選擇用于打開鏈接的應(yīng)用 無對話框;您的應(yīng)用會打開以處理您的網(wǎng)站鏈接
兼容性 所有 Android 版本 Android 6.0 及更高版本

配置

主要流程官網(wǎng)上都有,簡單說下在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" />

        <!--
        系統(tǒng)會匹配 
         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

當(dāng)assetlinks.json已經(jīng)配置好了,需要把assetlinks.json上傳到設(shè)置的域名下的/.well-known/文件夾下,通過上面圖片中的Link and Verify按鈕驗證,驗證通過后就可以愉快的運行app了,注意app link只會在應(yīng)用安裝時校驗,注意官網(wǎng)有一句說明

確認要與您的應(yīng)用關(guān)聯(lián)的網(wǎng)站列表,并且確認托管的 JSON 文件有效后,請立即在您的設(shè)備上安裝應(yīng)用。等待至少 20 秒,讓系統(tǒng)完成異步驗證流程。

這一步就是在異步請求驗證app-link的合法性。

除了通過android studio自帶的工具驗證json文件外,還可以通過以下命令驗證

https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://yourhost&relation=delegate_permission/common.handle_all_urls

保證以上鏈接在公網(wǎng)能訪問就行,需要梯子

驗證

adb驗證
adb shell dumpsys package domain-preferred-apps //輸出當(dāng)前手機所有l(wèi)ink的包名

Package: com.vdian.android.lib.testforgradle // 關(guān)注自己的包名就行
Domains: example.test.com //之前設(shè)置的域名
Status:  always : 200000017 //這里有4種狀態(tài) 

undefined — app沒有在manifest中啟用鏈接自動驗證功能。
ask — app驗證失敗(會通過打開方式對話框詢問用戶)
always — app通過了驗證(點擊這個域名總是打開這個app)
never — app通過了驗證,但是系統(tǒng)設(shè)置關(guān)閉了此功能。

adb 直接喚起,如果是成功狀態(tài),可以直接喚起APP的指定頁面
adb shell am start -a android.intent.action.VIEW \
        -c android.intent.category.BROWSABLE \
        -d "https://example.test.com" 
瀏覽器驗證
<a >點我link</a>

坑點

  1. app在安裝驗證時,會驗證Androidmanifest里配置的所有域名,而不是配置android:autoVerify="true"的Activity下的域名,既然這樣,autoVerify應(yīng)該配置在application標(biāo)簽比較合理。
  2. 校驗所有域名時,一旦有一個域名不符合正則,代碼會拋異常,流程就斷了,網(wǎng)上有很多說要翻墻什么的。其實根本不需要,真正的原因應(yīng)該就是域名校驗的問題。
  3. 瀏覽器喚起時,只有在chrome或者基于chrome的瀏覽器才可以,市面上大部分都不支持,例如小米自帶的瀏覽器,qq瀏覽器,百度瀏覽器等都不支持。
  4. 總的來說在國內(nèi)意義不大,像微信內(nèi)置瀏覽器攔截了系統(tǒng)的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;
            }
        }
    }

    //如果需要驗證,那就獲取當(dāng)前的配置的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());
                    // 只要系統(tǒng)權(quán)限沒有關(guān)閉,這里都會把host加載進去
                    mIntentFilterVerifier.addOneIntentFilterVerification(
                            verifierUid, userId, verificationId, filter, packageName); 
                    count++;
                }
            }
        }
    }
}
PackageManagerService類
//緊接著發(fā)送廣播
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不符合正則校驗,整個流程就斷了
最合理的應(yīng)該是 只校驗配置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方法里調(diào)用了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獲取服務(wù)端配置,和本地校驗,到這一步流程就走完了。
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);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容