【Unity】Androidアプリリンクを実装してみた

技術

なぜアプリリンクか

会社の有志数名でUnity研究会を立ち上げました。
Unityって業務として触る機会はあまりありませんが、クロスプラットフォームなアプリケーションを作るのにとっても便利です。
会社のメンバーはみんなC#が得意だし。

「Unity研究会」では主にAndroid向けにアプリを作って公開したりしてるんですが、アプリを作っていく中で、複数のユーザーで1つのコード共有したい、という要件が出てきました。

最初はコードをメールやチャットなんかで共有するだけにしていました。
共有されたコードをユーザー自身の手で、コピペして、アプリに設定してもらう方式です。
でもやっぱりそれでは不便です。
せっかくなら

リンクをクリックするとアプリが起動して、コードがアプリに直接設定される仕組みにしたい

そう思って調べてみました。

リンクからアプリが起動する仕組みをディープリンクと言うそうです。
で、Androidのディープリンクがアプリリンク、iOSではユニバーサルリンクと呼ばれるらしい。

今回はAndroidのアプリリンクに絞って実装しました。
(iOSのユニバーサルリンクについてはまた後日やろうと思ってます)

Unityでアプリリンクを実装する

アプリリンクの実装は公式のドキュメントに沿ってやれば簡単です。
https://docs.unity3d.com/ja/2019.4/Manual/enabling-deep-linking.html

サーバーとドメインを用意しないといけないのが少し面倒ですね。

幸いなことにUnity研究会では会社のホームページが公開されているサーバーとドメインを使わせてもらえることになったので、そこはクリアできました。

サーバーには「https://{domain}/.well-known/assetlinks.json」でアクセスできるところに以下のファイルを配置しておきます。

[
    {
        "relation": ["delegate_permission/common.handle_all_urls"],
        "target": {
            "namespace": "android_app",
            "package_name": "com.example.sampleapp",
            "sha256_cert_fingerprints": ["XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX"]
        }
    }
]

sha256_cert_fingerprintsを計算するのが少し面倒ですが、公式のドキュメントに書かれている通りにやれば簡単にできました。
package_nameは公開するアプリに合わせたものにします。

注意しないといけないのは、ドメインの直下に配置して、リダイレクトなしでアクセスできなければいけないということ、それだけです。

サーバーにassetlinsk.jsonを配置したら、次はUnity側です。
まずは AndroidManifest.xmlを作ります。
プロジェクトのAssets/Plugins/Androidフォルダーに置いてください。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
  <application>
    <activity android:name="com.unity3d.player.UnityPlayerActivity" android:theme="@style/UnityThemeSelector" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <intent-filter 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="www.example.com" android:pathPrefix="/app/sampleapp" />
        <data android:scheme="http" />
      </intent-filter>
    </activity>
    <meta-data android:name="asset_statements" android:value='[{"include": "https://www.example.com/.well-known/assetlinks.json"}]' />
  </application>
</manifest>

実のところリンクからアプリを起動するだけであれば、クライアント側はAndroidManifest.xmlをプロジェクトに追加するだけで完了します。

続いてC#側のプログラムです。

public class SampleApp : MonoBehaviour
{
    void Awake()
    {
        Application.deepLinkActivated += url => {
            var code = GetCodeFromUrl(url);
            activated(code, false);
        };
        if (!string.IsNullOrEmpty(Application.absoluteURL)) {
            var code = GetCodeFromUrl(Application.absoluteURL);
            activated(code, true);
        }
    }
    private static string GetCodeFromUrl(string url)
    {
        var index = url.IndexOf("?");
        if (index < 0) return null;
        var parameters = System.Web.HttpUtility.ParseQueryString(url.Substring(index));
        return parameters["code"];
    }
    private void activated(string code, bool isStartup)
    {
        // TODO:ここにアプリリンクから起動した時の処理を実装する
    }
}

クラス名は適当です。
MonoBehaviourの派生クラスなら何でもいいと思います。

アプリリンクから起動した時はApplication.absoluteURLにリンクのURLが入っています。
GetCodeFromUrlメソッドではこのURLのクエリパラメータからcodeというパラメータの値を取り出しています。
取得したコードを使ってアプリリンクから起動した時の処理を実行すればいいというわけです。

アプリがすでに起動している状態で、アプリリンクがクリックされた場合はApplication.deepLinkActivatedイベントが呼び出されます。
deepLinkActivatedイベントは引数にリンクのURLが入っています。
こちらでもGetCodeFromUrlメソッドでクエリパラメータからcodeというパラメータの値を取り出して処理すればOKです。

では次にサーバー側を用意しましょう。
といっても、こんな感じのhtmlを1つ用意して「https://{domain}/app/sampleapp」でアクセスできる場所に配置しただけです。

以上の設定で基本的にアプリリンクとして機能するはずです。

Android11までは!!

実はこのアプリリンクという機能、Android12から仕様が変わってユーザー自身が自らの手で明示的にアプリリンクを有効にする必要があるらしいです。

リンク設定を確認してリクエストする

さあ、困りました。
Android12以降はこのアプリリンクの関連付けが無効になるのがデフォルトなようです。
(それ以前は逆に「有効」になるのがデフォルトだったみたい)

Androidの公式によるとユーザー自身に設定してもらうようにプログラムからリクエストするのがいいとのこと。

ただ、その場合Unityから直接AndroidのAPIにアクセスしないといけないので若干面倒です。
いわゆるJNI(Java Native Interface)ってやつです。

幸い、UnityにはJNIを比較的簡単に利用する仕組みがあります。

using System;
using System.Collections.Generic;
using UnityEngine;
public static class AndroidDomainState {
    private const int DOMAIN_STATE_NONE = 0;
    private const int DOMAIN_STATE_SELECTED = 1;
    private const int DOMAIN_STATE_VERIFIED = 2;

    private const string ACTION_APP_OPEN_BY_DEFAULT_SETTINGS = "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS";

    public static Dictionary<string, bool> GetAllState()
    {
        var result = new Dictionary<string, bool>();
        try
        {
            using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
            using (var currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
            using (var context = currentActivity.Call<AndroidJavaObject>("getApplicationContext"))
            using (var manager = currentActivity.Call<AndroidJavaObject>("getSystemService", "domain_verification"))
            using (var userState = manager.Call<AndroidJavaObject>("getDomainVerificationUserState", context.Call<string>("getPackageName")))
            using (var map = userState.Call<AndroidJavaObject>("getHostToStateMap"))
            {
                var entries = map.Call<AndroidJavaObject>("entrySet");
                var ite = entries.Call<AndroidJavaObject>("iterator");
                while (ite.Call<bool>("hasNext"))
                {
                    var entry = ite.Call<AndroidJavaObject>("next");
                    var key = entry.Call<string>("getKey");
                    var stateValue = entry.Call<AndroidJavaObject>("getValue").Call<int>("intValue");
                    result.Add(key, stateValue != DOMAIN_STATE_NONE);
                }
            }
        }
        catch (Exception ex)
        {
            Debug.Log(ex.StackTrace);
        }
        return result;
    }

    public static void RequestLink()
    {
        try
        {
            using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
            using (var currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
            using (var context = currentActivity.Call<AndroidJavaObject>("getApplicationContext"))
            using (var uri = new AndroidJavaClass("android.net.Uri"))
            using (var intent = new AndroidJavaObject(
                "android.content.Intent",
                ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
            uri.CallStatic<AndroidJavaObject>("parse", $"package:{context.Call<string>("getPackageName")}")))
            {
                currentActivity.Call("startActivity", intent);
            }
        }
        catch (Exception ex)
        {
            Debug.Log(ex.StackTrace);
        }
    }
}

AndroidJavaClassとそのメソッドGetStaticCallがJNIの部分です。
たったこれだけでAndroidのJavaコードにアクセスできるわけです。
自前でC++を実装する手間とは比べ物にならないほど簡単ですね。

実装自体はここを参考にしました。
https://developer.android.com/about/versions/12/web-intent-resolution?hl=ja

AndroidDomainState.GetAllState() でアプリリンクが有効かどうか確認し
AndroidDomainState.RequestLink() でアプリリンクを設定する画面が表示されます。

この2つのメソッドを使ってアプリリンクをユーザー自身に設定してもらうようにリクエストできるって寸法です。

ちなみに

Unity研究会はまだ立ち上げたばかりで、正直どこまで続くかわかりませんが、会社のみんなに使ってもらえるようなアプリや簡単なゲームなんかを作っていければ面白くなるかなと思っています。

面白がってもらえるものが作れたらUnity研究会の活動にも興味を持ってくれる社員も増えるかもしれません。
一緒にやってみようかなと思う社員が増えて、Unity研究会が盛り上がるといいな、と夢を見る今日この頃です。

追記(2022/8/14)

ここで紹介した実装には問題がありました。
致命的とまでは言いませんが、考慮が少し足りなかった感じです。

下記の記事で修正について説明していますので、ぜひ合わせて確認してください。

コメント

タイトルとURLをコピーしました