AppIndexing
UniversalLinks
deeplink
OrigamiDay 1

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

More than 1 year has passed since last update.

OrigamiでプログラマーやってるDと申します。

Origami Advent Calendar 2017の初日で恐縮です。

今日は真面目に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