DEV Community

loading...

真面目にDeep Link対応したい話

dongri profile image D ・3 min read

今日は真面目にDeep Link対応したい話をしようと思います。今更Deep Link?感はありますが、真面目にやろうと思います。

会社のアプリでいきなり本番検証は無理なのでテスト用のアプリを作って検証することにしました。

Webはこちら( https://lgtm.lol )で、Androidはこちら( https://play.google.com/store/apps/details?id=lol.lgtm )を使ってます。
iOSは開発中です。

用語から

よく聞く Universal Links, Deep Link, App Indexing, App Linksなどなど、いろいろ用語があって、まずは混乱しますよね?整理しますとこんな感じになるかと思います。

deep_link.png

Google: App Indexing
https://firebase.google.com/docs/app-indexing/

Twitter: Twitter カード
https://developer.twitter.com/en/docs/tweets/optimize-with-cards/guides/getting-started

Facebook: App Links
https://developers.facebook.com/docs/applinks

Apple: Universal Links
https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html

ディープリンク(Deep Link)とは?

アプリの特定の画面に遷移させることのできるリンクのこと。

Custom URL Scheme (iOS, Android共通)

<a href="app-name://product/abc123">商品ページをアプリで開く</a>

のようにapp-nameというアプリのproductページを開くやつです。

問題は同じapp-nameを持つアプリを2つインストールされた場合どれが起動するか保証できません。

Universal Links (iOS)

iOS用のDeep Linkです。

iOS 9(2015年9月16日)以降利用可能で、サーバーからjsonを返す必要あります。

https://lgtm.lol/apple-app-site-association


{
  "applinks": {
       "apps": [],
        "details": [
           {
               "appID":"6SRWK494FT.lol.lgtm.ios.LGTM",
               "paths":[ "/i/*" ]
           }
        ]
    }
}

iOS側は、Associated Domainsを有効にしてドメインを追加します。

Screen Shot 2017-11-30 at 22.10.20.png

書き出されたentitlementsファイルはこのようになります。

Screen Shot 2017-11-30 at 22.11.10.png

Custom URL Schemeまで対応するとこうなります。

Screen Shot 2017-11-30 at 22.50.00.png

受け取ったリンクを処理する


func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
    if (url.scheme == "lgtm" && url.host == "item") {
        let components = url.pathComponents
        let itemId = components[1]
        let vc = ItemViewController()
        vc.itemId = itemId
        self.window?.rootViewController?.present(vc, animated: true, completion: nil)
    }
    return true
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
        let url = userActivity.webpageURL!
        if (url.scheme == "https" && url.host == "lgtm.lol") {
            let components = url.pathComponents
            if (components[1] == "i") {
                let itemId = components[2]
                let vc = ItemViewController()
                vc.itemId = itemId
                self.window?.rootViewController?.present(vc, animated: true, completion: nil)
            }
        }
    }
    return true
}

アプリが起動される時に Associated Domains に定義してるドメインの /apple-app-site-association にアクセスして許可してるパスを取得してアプリに認識させるみたいです。(サーバーログから)

これで外部リンクから https://lgtm.lol/i/234 にアクセスされた時は LGTM アプリのItemViewControllerが立ち上がるようになりました。

App Indexing (Android)

サーバー側でjsonをレンダリングするように設定します。

https://lgtm.lol/.well-known/assetlinks.json


[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target" : { "namespace": "android_app",
      "package_name": "lol.lgtm",
                 "sha256_cert_fingerprints": ["0E:C7:C8:9F:40:03:28:73:9F:7B:8E:62:09:B1:C4:2E:B9:A3:02:65:F1:2A:29:C6:7D:40:56:DE:D7:B7:84:42"] }
  }
]

package_nameはandroidのパッケージ名です。
sha256_cert_fingerprintsは公式ドキュメントでは

keytool -list -v -keystore my-release-key.keystore 

このように書いていて、そのまましたら adb install release.apk の時は通るけど、playstoreからインストールした場合は認証通らないみたいです。
play store consoleのリリース管理 -> アプリの署名 -> アプリへの署名証明書の SHA-256 証明書のフィンガープリント を設定したところPlay Storeからインストールして認証通るようになりました。

Android側のAndroidManifest.xmlは以下のようになります。


<activity
    android:name=".ItemActivity">
    <intent-filter android:label="@string/app_name" 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="https" android:host="lgtm.lol" android:pathPrefix="/i"></data>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="lgtm" android:host="item" />
    </intent-filter>
</activity>

これで https://lgtm.lol/i/234lgtm://item/234 をサポートすることになります。

受け取ったリンクを処理する

ItemActivity.java#onCreate


String data = intent.getDataString(); // https://lgtm.lol/i/234 | lgtm://item/234
if (data != null) {
    String pathId = data.substring(data.lastIndexOf("/") + 1); // 234
    itemId = Integer.valueOf(pathId);
    setImageFromItemId(itemId);
} else {
    itemId = intent.getIntExtra("id", 0);
    imageView.setImageUrl(intent.getStringExtra("url"), Controller.getPermission().getImageLoader());
}

Androidでは対応するアプリが複数個ある場合どれで開くか選ばせるんですが、特定のアプリがデフォルトで開くかどうかは以下のコマンドで確認できます。


$ adb shell dumpsys package domain-preferred-apps 
  Package: lol.lgtm
  Domains: lgtm.lol
  Status:  always : 20000002c

Statusが ask の場合はurlクリックした時にブラウザで開くか、アプリで開くか選択させるやつが出ます。

以下のコマンドでテストできます。


$ adb shell am start -a android.intent.action.VIEW \
 -c android.intent.category.BROWSABLE \
 -d "https://lgtm.lol/i/234"

ここまでやるとGoogle検索結果か、ページにリンク( https://lgtm.lol/i/234 )クリックした時にデフォルトでアプリが起動するようになりました

成果

Google検索結果にアプリアイコン表示

play store consoleの「開発ツール -> サービスとAPI」でドメイン追加して、google search consoleで認証を行います。
Google様がindex作成するのに時間かかるのでここは待つしかないかと思います。

終わりに

引き続きDeep Link勉強して会社のアプリに対応しようと思います。後は社内で https://lgtm.lol のAPIをgRPC対応しろ!の声もあるので、近いうちにgRPC対応しようとかと思います。

追記

Googleが検索結果にアプリを表示してくれました。実際Android向けにサーバー設定してから反映されるまで約二週間かかりましたね

追記2

コードに関して問い合わせが来たのでアプリソースコードをgithubに公開しました

iOS: https://github.com/dongri/LGTM-iOS
Android: https://github.com/dongri/LGTM-Android

Discussion (0)

pic
Editor guide