今日は真面目に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などなど、いろいろ用語があって、まずは混乱しますよね?整理しますとこんな感じになるかと思います。
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を有効にしてドメインを追加します。
書き出されたentitlementsファイルはこのようになります。
Custom URL Schemeまで対応するとこうなります。
受け取ったリンクを処理する
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/234
と lgtm://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
Top comments (0)