UnityでiOSとAndroidのランタイムパーミッションの確認と要求を行う方法

### Unityでアプリのカメラ権限の確認＆要求ってどうやるの？

これまでのUnityでのアプリ開発では、iOS/Android端末の機能の権限のチェックやリクエストをするために、ネイティブプラグインを利用する必要がありました。
最近になってやっと、ランタイムパーミッション関係のAPIが追加されたようなので、自分なりに使い方を調べてみました。

---

#### [iOS] Application.RequestUserAuthorizationクラス
https://docs.unity3d.com/ja/2018.2/ScriptReference/Application.RequestUserAuthorization.html
**Unity2018.1以降**で利用可能
iOSでウェブカメラまたはマイクを使用するための承認をリクエストします。
（カメラとマイクの権限のみチェックとリクエストが可能）

#### [Android] Android.Permissionクラス
https://docs.unity3d.com/2018.3/Documentation/ScriptReference/Android.Permission.RequestUserPermission.html
https://docs.unity3d.com/ja/current/Manual/android-RequestingPermissions.html
**Unity2018.3以降**で利用可能
Androidでユーザーに、デバイスリソースまたは承認が必要な情報へのアクセスを許可するよう要求します。
（クラスで定義されていない権限でも文字列で指定してチェックとリクエストが可能）

---

それぞれのプラットフォームに対応している新しく追加されたAPIを使用することで、簡易的な処理ならばネイティブプラグインを使用しないで目的を達成できそうです。
今回はカメラとマイクの権限の確認と要求をセットで行う例の作成を試みました。
そして、何回も試行錯誤した結果、導きだしたコードがこちら。

### ランタイムパーミッション要求の例
```csharp:
using UnityEngine;
using System.Collections;
using System;

#if UNITY_ANDROID && UNITY_2018_3_OR_NEWER
using UnityEngine.Android;
#endif

namespace PermissionAuthorizationTest
{
    // iOS/Androidプラットフォームで動作するランタイムパーミッション要求のサンプル
    // 参考URL: https://forum.unity.com/threads/requestuserauthorization-as-a-coroutine-bugged.380666/
    public class PermissionAuthorizationExample : MonoBehaviour
    {
        IEnumerator Start ()
        {
            #if UNITY_IOS && UNITY_2018_1_OR_NEWER
        
            // iOS
            //
            // カメラパーミッションが許可されているか調べる
            if (!Application.HasUserAuthorization (UserAuthorization.WebCam)) {
                // 権限が無いので、カメラパーミッションのリクエストをする
                yield return RequestUserAuthorization (UserAuthorization.WebCam);
            }
            // マイクパーミッションが許可されているか調べる
            if (!Application.HasUserAuthorization (UserAuthorization.Microphone)) {
                // 権限が無いので、マイクパーミッションのリクエストをする
                yield return RequestUserAuthorization (UserAuthorization.Microphone);
            }
            // リクエストの結果、アプリ機能に必要なパーミッションが全て許可されたか調べる
            if (Application.HasUserAuthorization (UserAuthorization.WebCam) && Application.HasUserAuthorization (UserAuthorization.Microphone)) {
                // 権限が許可されたので、権限が必要なAPIを使用する処理へ進む（iOSでは権限拒否された状態でAPIを使用しようとするとアプリが落ちる）
                findWebCams ();
                findMicrophones ();
            } else {
                // 権限が許可されなかったので、ユーザーに対して権限の使用用途の説明を表示して自分でOSのアプリ設定画面で権限許可を行うようにアナウンスする必要がある。
                // (iOSでは初回の権限リクエストが拒否された場合は、次回からはリクエスト自体が表示されなくなる)
            }       

            #elif UNITY_ANDROID && UNITY_2018_3_OR_NEWER

            // Android
            //
            // カメラパーミッションが許可されているか調べる
            if (!Permission.HasUserAuthorizedPermission (Permission.Camera)) {
                // 権限が無いので、カメラパーミッションのリクエストをする
                yield return RequestUserPermission (Permission.Camera);
            }
            // マイクパーミッションが許可されているか調べる
            if (!Permission.HasUserAuthorizedPermission (Permission.Microphone)) {
                // 権限が無いので、マイクパーミッションのリクエストをする
                yield return RequestUserPermission (Permission.Microphone);
            }
            // リクエストの結果、アプリ機能に必要なパーミッションが全て許可されたか調べる
            if (Permission.HasUserAuthorizedPermission (Permission.Camera) && Permission.HasUserAuthorizedPermission (Permission.Microphone)) {
                // 権限が許可されたので、権限が必要なAPIを使用する処理へ進む
                findWebCams ();
                findMicrophones ();
            } else {
                // 権限が許可されなかったので、ユーザーに対して権限の使用用途の説明を表示してから再度のリクエストを行う。
                // もしも拒否時に「今後表示しない」がチェックされた場合は、次回からリクエスト自体が表示されなくなる、
                // そのためユーザーには自分でOSのアプリ設定画面で権限許可を行うようにアナウンスする必要がある。
                // （Permissionクラスにはそれらの違いを調べる方法は用意されていない）
            }

            #endif

            yield break;
        }

#if (UNITY_IOS && UNITY_2018_1_OR_NEWER) || (UNITY_ANDROID && UNITY_2018_3_OR_NEWER)
        bool isRequesting;

        // OSの権限要求ダイアログを閉じたあとに、アプリフォーカスが復帰するのを待ってから権限の有無を確認する必要がある
        IEnumerator OnApplicationFocus(bool hasFocus)
        {
            // iOSプラットフォームでは1フレーム待つ処理がないと意図通りに動かない。
            yield return null;

            if (isRequesting && hasFocus)
            {
                isRequesting = false;
            }
        }

        #if UNITY_IOS
        IEnumerator RequestUserAuthorization(UserAuthorization mode)
        {
            isRequesting = true;
            yield return Application.RequestUserAuthorization(mode);
            // iOSではすでに権限拒否状態だとダイアログが表示されず、フォーカスイベントが発生しない。
            // その状態を判別する方法が見つからないので、タイムアウト処理をする。

            // アプリフォーカスが戻るまで待機する
            float timeElapsed = 0;
            while (isRequesting)
            {
                if (timeElapsed > 0.5f){
                    isRequesting = false;
                    yield break;
                }
                timeElapsed += Time.deltaTime;

                yield return null;
            }
            yield break;
        }
        #elif UNITY_ANDROID
        IEnumerator RequestUserPermission(string permission)
        {
            isRequesting = true;
            Permission.RequestUserPermission(permission);
            // Androidでは「今後表示しない」をチェックされた状態だとダイアログは表示されないが、フォーカスイベントは通常通り発生する模様。
            // したがってタイムアウト処理は本来必要ないが、万が一の保険のために一応やっとく。

            // アプリフォーカスが戻るまで待機する
            float timeElapsed = 0;
            while (isRequesting)
            {
                if (timeElapsed > 0.5f){
                    isRequesting = false;
                    yield break;
                }
                timeElapsed += Time.deltaTime;

                yield return null;
            }
            yield break;
        }
        #endif
#endif

        void findWebCams ()
        {
            foreach (var device in WebCamTexture.devices) {
                Debug.Log ("Name: " + device.name);
            }
        }

        void findMicrophones ()
        {
            foreach (var device in Microphone.devices) {
                Debug.Log ("Name: " + device);
            }
        }
    }
}
```

読みにくくてすみません、コードの意図は必要以上に詳細なコメントを読んで理解してください。
iOS/Android実機でコードを試した結果、とりあえず期待通りに動作していることを確認しました。

OnApplicationFocus()を使用してアプリフォーカスの復帰を待機する処理は、
[RequestUserAuthorization as a coroutine - bugged?](https://forum.unity.com/threads/requestuserauthorization-as-a-coroutine-bugged.380666/)
のスレッドのやりとりをヒントにしています。
このためコードがかなり複雑化してしまいました。

とにかく、権限が必要なAPIを利用する前に、あらかじめこのような処理を挟んでおくこで、アプリがクラッシュするなどの事態を避けることが出来るようになりました。

### APIが追加される前の古いUnityバージョンではどうする？
Androidでは以下の記事の方法で可能です。ネイティブプラグインが必要無いので気軽に導入できる方法です。
[UnityでAndroidのランタイムパーミッションに対応する](https://qiita.com/niusounds/items/2c10bbc08e602d0d575c)
iOSでもAndroidでもAssetプラグインを使えば同様の事が実現可能です。たぶん無料のものも存在するはず。

### 最新のUnityのAPIでも権限の処理には不十分な点も
UnityのAPIは必要最小限の簡易的なものしか用意されていません。
ネイティブアプリと同等の権限の処理をしたい場合は、やはりネイティブプラグインの利用を検討する必要があるでしょう。

### 参考リンク
[UnityにおけるAndroidアプリのパーミッション付与について](https://qiita.com/relzx/items/2bcf2199892c5dcabfc4)