【Unity】Androidアプリリンク実装の続き – 不具合を直す!

技術

以前のAndroidアプリリンク実装には不具合がある!?

先日、Unity上でAndroidアプリリンクを実装する方法を紹介しました。

ただ、このアプリリンクという機能はAndroid12以降、デフォルト設定がOFFになっているらしくユーザー自らの手で ON に設定してもらわないといけないらしいです。

そこで先日の記事では『リンク設定を確認し、必要ならユーザーに設定をリクエストする方法』も紹介しました。

ところが紹介した方法にはいくつか問題がありました。

  1. 「リンクを追加」した後に「対応リンクを開く」をOFFにした場合が考慮されていない
  2. Andorid12(API LEVEL31)未満の時、リンク設定の確認に失敗しているが、そのことを明確に知る方法がない

これらの問題を解決してみようというのが今回のテーマです。

『”対応リンクを開く”がOFFの場合』を直す

先日の記事で紹介した実装がこちら(一部抜粋)です。

    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;
    }

この実装ではuserStateというオブジェクトを取得して、getHostToStateMapというメソッドで「リンクを追加」されたドメインのリストを取り出しています。

ところが「対応リンクを開く」の設定が ON なのか OFF なのかについて、一切確認していないのです。

そのため、下記のような状態で判定されてしまうことになるわけです。

対応リンクを開くリンクを追加判定
ONOFFリンク未設定
ONONリンク設定済み
OFFOFFリンク未設定
OFFONリンク設定済み
Androidの「対応リンクを開く」「リンクを追加」設定

つまり「対応リンクを開く」がOFFであるにもかかわらず「リンクを追加」がONであれば「リンク設定済み」と判定されてしまっています。
そこで、「対応リンクを開く」の設定をちゃんと確認するように実装を修正しましょう。

public static (bool isLinkHandlingAllowed, Dictionary<string, bool> domainStates) GetAllState()
{
    var linkAllowed = false;
    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"))
        {
            linkAllowed = userState.Call<bool>("isLinkHandlingAllowed");
            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 (linkAllowed, result);
}

まず、GetAllStateメソッドの戻り値をタプルに変更しました。

public static (bool isLinkHandlingAllowed, Dictionary domainStates) GetAllState()
{
    var linkAllowed = false;
    var result = new Dictionary();

        ・・・・(中略)・・・・

    return (linkAllowed, result);
}

これで「対応リンクを開く」の設定と「リンクを追加」されたドメインのリストの両方をメソッドの呼び出し元に返すことができます。

実際に「対応リンクを開く」の設定を取得しているのはこのコードです。

linkAllowed = userState.Call("isLinkHandlingAllowed");

userStateはDomainVerificationUserStateというクラスのインスタンスです。
このuserStateのisLinkHandlingAllowedメソッドで「対応リンクを開く」の設定が取得できます。

リンク設定確認に失敗していることを知る

次に、API LEVEL31未満のAndroidで動く時、リンク設定確認に失敗していることをGetAllStateメソッドの呼び出し元で知る方法を考えてみます。

API LEVEL31未満のAndroidで動く時、GetAllStateメソッド内では例外が発生します。
具体的にはgetSystemServiceで”domain_verification”を取得できずにnullを返すため、そのあとのロジックでnull参照例外になってしまうのです。

// manager には null が入る
var manager = currentActivity.Call<AndroidJavaObject>("getSystemService", "domain_verification")

GetAllStateメソッドは try-catchで例外を握りつぶして、戻り値のisLinkHandlingAllowedに”false”を、domainStatesに空っぽのDictionaryを設定して終了しているため、呼び出し元では例外が発生したことを知ることができません。

そこで、解決方法を検討してみたいと思います。

①try – catchしない

そもそも、メソッド内で例外をtry-catchしてしまうから、メソッドの呼び出し元が例外発生を知ることができないわけです。
であれば、メソッド内でtry-catchしなければ、呼び出し元で例外をキャッチすることができます。
そして、この修正がおそらく一番簡単だと思います

②nullチェックを追加する

残念ながら、①の方法は事情があって今回は採用しませんでした。
代わりに採用したのはgetSystemServiceの戻り値が null かどうかをチェックする方法です。

public static (bool? isLinkHandlingAllowed, Dictionary<string, bool> domainStates)  GetAllState()
{
    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"))
        {
            if (manager != null)
            {
                var linkAllowed = false;
                var result = new Dictionary<string, bool>();
                using (var userState = manager.Call<AndroidJavaObject>("getDomainVerificationUserState", context.Call<string>("getPackageName")))
                using (var map = userState.Call<AndroidJavaObject>("getHostToStateMap"))
                {
                    linkAllowed = userState.Call<bool>("isLinkHandlingAllowed");
                    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);
                    }
                }
                return (linkAllowed, result);
            }
            else
            {
                Debug.Log("Could not get \"domain_verification SystemService\". API level may be less than 31.");
                return (null, null);
            }
        }
    }
    catch (Exception ex)
    {
        Debug.Log(ex.StackTrace);
        return (null, null);
    }
}

まず、戻り値のisLinkHandlingAllowedを bool → bool? というnull許容型に変更しました。

次に、getSystemServiceの戻り値をmanagerという変数に代入した後、managerがnullでないかどうかを確認するようにしました。
もしmanagerがnullであればメソッドの戻り値は「return (null, null);」としました。
念のため 例外をキャッチした時の戻り値も「return (null, null);」としておきました。

これでGetAllStateの呼び出し元は isLinkHandlingAllowed が値を持っているかどうかでリンク設定の取得に成功したかどうかを知ることができます。

var result = AndroidDomainState.GetAllState();
if (result.isLinkHandlingAllowed.HasValue)
{
  // リンク設定の取得に成功した場合
}
else
{
  // リンク設定の取得に失敗した場合
}

③API LEVEL を取得する

API LEVELを取得して 31 以上かどうかを先に確認する、という方法もあります。
API LEVELの取得方法はこうです。

using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
{
    var level = version.GetStatic<int>("SDK_INT");
}

今回は使いませんでしたが、API LEVELを先に取得しておいて、31未満であればGetAllStateで「return (null, null);」してもいいし、そもそもGetAllStateを呼び出さないという方法をとることもできます。

まとめ

今回は以前の記事「【Unity】Androidアプリリンクを実装してみた」で紹介した内容の不備を補足する内容でした。
ちゃんと確認していたつもりでしたが甘々でした。お恥ずかしい・・・。

修正方法は今回紹介した以外にもいくつか考えられそうです。
他にもいい方法がありそうであればぜひコメントで教えてください。

最後にAndroidDomainState.cs の全文を記載しておきます。

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 (bool? isLinkHandlingAllowed, Dictionary<string, bool> domainStates)  GetAllState()
    {
        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"))
            {
                if (manager != null)
                {
                    var linkAllowed = false;
                    var result = new Dictionary<string, bool>();
                    using (var userState = manager.Call<AndroidJavaObject>("getDomainVerificationUserState", context.Call<string>("getPackageName")))
                    using (var map = userState.Call<AndroidJavaObject>("getHostToStateMap"))
                    {
                        linkAllowed = userState.Call<bool>("isLinkHandlingAllowed");
                        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);
                        }
                    }
                    return (linkAllowed, result);
                }
                else
                {
                    Debug.Log("Could not get \"domain_verification SystemService\". API level may be less than 31.");
                    return (null, null);
                }
            }
        }
        catch (Exception ex)
        {
            Debug.Log(ex.StackTrace);
            return (null, null);
        }
    }

    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);
        }
    }
}

コメント

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