Unity iOS/Androidで位置情報を取得する

Unity上のiOS/Androidで位置情報を取得とiOS/Androidのパーミッションまわりについて試して見たことを記載します。

# Unity公式の位置情報取得のサンプルコード

[Unity公式の位置情報取得のサンプルコード](https://docs.unity3d.com/ja/current/ScriptReference/LocationService.Start.html)のコメントを訳したものを下記に記載します。

このコードでも位置情報を取得できますが、Unityの位置情報の取得を管理するクラス[LocationService](https://docs.unity3d.com/ja/current/ScriptReference/LocationService.html)がシンプルすぎて、iOS/Androidの位置情報取得の可否の状態によってどういう挙動をするのかがドキュメントからは不明瞭なので、本記事ではそのあたりを実機を動かして確認していきます。

```csharp
using UnityEngine;
using System.Collections;

public class TestLocationService : MonoBehaviour
{
    IEnumerator Start()
    {
        // 最初に、ユーザーがロケーションサービスを有効にしているかを確認する。無効の場合は終了する
        if (!Input.location.isEnabledByUser)
            yield break;

        // 位置を取得する前にロケーションサービスを開始する
        Input.location.Start();

        // 初期化が終了するまで待つ
        int maxWait = 20; // タイムアウトは20秒
        while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
        {
            yield return new WaitForSeconds(1); // 1秒待つ
            maxWait--;
        }

        // サービスの開始がタイムアウトしたら（20秒以内に起動しなかったら）、終了
        if (maxWait < 1)
        {
            print("Timed out");
            yield break;
        }

        // サービスの開始に失敗したら終了
        if (Input.location.status == LocationServiceStatus.Failed)
        {
            print("Unable to determine device location");
            yield break;
        }
        else
        {
            // アクセスの許可と位置情報の取得に成功
            print("Location: " + Input.location.lastData.latitude + " "
                               + Input.location.lastData.longitude + " "
                               + Input.location.lastData.altitude + " "
                               + Input.location.lastData.horizontalAccuracy + " "
                               + Input.location.lastData.timestamp);
        }

        // 位置の更新を継続的に取得する必要がない場合はサービスを停止する
        Input.location.Stop();
    }
}
```

# 動作環境

* Unity 2018.4f8 Mac版
* XCode 11.0

# 各OSの位置情報取得の許可の状態について

iOSとAndroidのOSごとの位置情報取得の許可の状態について下記で記載します。
Unityではこのあたりが[LocationService](https://docs.unity3d.com/ja/current/ScriptReference/LocationService.html)によって抽象化されてしまっています。

## iOS

[CLAuthorizationStatus](https://developer.apple.com/documentation/corelocation/clauthorizationstatus)に記載されているように、下記の5状態が存在します。

* notDetermined - ユーザーは未選択
* restricted - 許可されていない（ユーザーは特に拒否していないが何らかの理由で使用できない場合）
* denied - ユーザーは拒否
* authorizedAlways - 常に許可
* authorizedWhenInUse - アプリの使用中のみ許可

### notDetermined

ユーザーはアプリで位置情報サービスを使用できるかどうかを選択していない。

位置情報取得の確認ダイアログをまだ表示したことがないのがこの状態。

*iOS 13.2.1での位置情報取得の確認ダイアログ*
![位置情報取得の確認ダイアログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/7a673fb2-8ae5-234b-ea88-bd0eb8caa3e8.png)

### restricted

アプリは位置情報サービスの使用を許可されていない。

ユーザーは位置情報サービスの使用を特に拒否していないが、何らかの理由で使用できない場合にこの状態になるはず。
「設定 > プライバシー > 位置情報サービス」で位置情報サービスをオフにした場合はこの状態（iOS 13.1.2で確認）。

### denied

ユーザーがアプリの位置情報サービスの使用を拒否したか、設定でグローバルに無効化されている。

位置情報の取得確認ダイアログで拒否を選択したか、「設定 > プライバシー > 位置情報サービス」で「なし」を選択したかのどちらかがこの状態。

## authorizedAlways

ユーザーは、いつでも位置情報サービスを開始することをアプリに許可した。

位置情報の取得確認ダイアログで「常に」を選択するとこの状態になる。アプリがバックグラウンドにいてもアプリは位置情報が取得可能。

## authorizedWhenInUse

ユーザーは、アプリが使用中に位置情報サービスを開始することを承認した。

位置情報の取得確認ダイアログで「使用中のみ」を選択するとこの状態になる。アプリがフォアグラウンドの状態でのみアプリは位置情報が取得可能。

## Android

下記の3状態が存在する。

* 未選択・1度だけ拒否
* 許可済み
* 拒否

### 未選択・1度だけ拒否

ユーザーはアプリで位置情報を使用できるかどうかを選択していない。
または、アプリで位置情報を使用を許可していない。

位置情報取得の確認ダイアログをまだ表示したことがないのがこの状態。
または、位置情報取得の確認ダイアログで拒否を選択したのがこの状態。

* Androidでは、この状態で [PermissionChecker.checkSelfPermission](https://developer.android.com/reference/android/support/v4/content/PermissionChecker#checkselfpermission) を呼ぶと、 [PackageManager.PERMISSION_DENIED](https://developer.android.com/reference/android/content/pm/PackageManager.html?hl=ja#PERMISSION_DENIED)（未許可）が返ってきます。
* Androidでは、この状態で [Activity.requestPermissions](https://developer.android.com/reference/androidx/core/app/ActivityCompat.html?hl=ja#requestPermissions(android.app.Activity,%20java.lang.String%5B%5D,%20int)) を呼ぶと、位置情報取得の確認ダイアログが表示されます。

*Android 9での位置情報取得の確認ダイアログ*
![位置情報取得の確認ダイアログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/7202b6c4-0df9-7ed1-0275-341a2821a0d0.png)

### 許可済み

ユーザーは位置情報サービス使用することをアプリに許可した。

位置情報取得の確認ダイアログで許可を選択したのがこの状態。

* Androidでは、この状態で [PermissionChecker.checkSelfPermission](https://developer.android.com/reference/android/support/v4/content/PermissionChecker#checkselfpermission) を呼ぶと、 [PackageManager.PERMISSION_GRANTED](https://developer.android.com/reference/android/content/pm/PackageManager.html?hl=ja#PERMISSION_GRANTED) （許可済み）が返ってきます。

### 拒否

アプリで位置情報を使用を許可していない。

位置情報取得の確認ダイアログで「今後表示しない」を選択したのがこの状態。

* Androidでは、この状態で [PermissionChecker.checkSelfPermission](https://developer.android.com/reference/android/support/v4/content/PermissionChecker#checkselfpermission) を呼ぶと、 [PackageManager.PERMISSION_DENIED](https://developer.android.com/reference/android/content/pm/PackageManager.html?hl=ja#PERMISSION_DENIED)（未許可）が返ってきます。
* Androidでは、この状態で [Activity.requestPermissions](https://developer.android.com/reference/androidx/core/app/ActivityCompat.html?hl=ja#requestPermissions(android.app.Activity,%20java.lang.String%5B%5D,%20int)) を呼ぶと、即座に [Activity. onRequestPermissionsResult](https://developer.android.com/reference/android/support/v4/app/ActivityCompat.OnRequestPermissionsResultCallback.html#onRequestPermissionsResult(int,%20java.lang.String[],%20int[])) にコールバックが返ってきます。

*Android 9での位置情報取得の確認ダイアログで「今後表示しない」のチェックが存在するもの*
![位置情報取得の確認ダイアログで「今後表示しない」のチェックが存在するもの](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/c79164c9-b7ce-d2a5-e3f4-985a284082fc.png)

# 位置情報取得の許可ごとの挙動について

## LocationService.isEnabledByUser

[LocationService.isEnabledByUser](https://docs.unity3d.com/ScriptReference/LocationService-isEnabledByUser.html)
ユーザーが位置情報サービスを有効にするかどうか。

### iOS

「設定 > プライバシー > 位置情報サービス」で位置情報サービスをオフにした場合にfalseが返る。
位置情報の許可の有無とは関係なさそう。

|状態|LocationService.isEnabledByUserの返値|
|---|:---:|
|notDetermined|true|
|restricted|false|
|denied|true|
|authorizedAlways|true|
|authorizedWhenInUse|true|

### Android

|状態|LocationService.isEnabledByUserの返値|
|---|:---:|
|未選択・1度だけ拒否|false|
|許可済み|true|
|拒否|false|

## LocationService.Start

[LocationService.Start](https://docs.unity3d.com/ScriptReference/LocationService.Start.html)
位置情報サービスの更新を開始します。
`LocationService.Start`を呼び出し後、[LocationService.status](https://docs.unity3d.com/ScriptReference/LocationService-status.html)が[LocationServiceStatus.Running](https://docs.unity3d.com/ScriptReference/LocationServiceStatus.Running.html)になったら、[LocationService.lastData](https://docs.unity3d.com/ScriptReference/LocationService-lastData.html)より位置情報を取得できます。

### iOS

|状態|LocationService.Start呼び出し後の動作|
|---|---|
|notDetermined|位置情報の取得の確認ダイアログが表示される。|
|restricted|"位置情報を利用できるようにするには、位置情報をサービスをオンにしてください"というダイアログが表示される。LocationService.statusは`Failed`になる。|
|denied|LocationService.statusは`Failed`になる。|
|authorizedAlways|LocationService.statusは`Running`になる。|
|authorizedWhenInUse|LocationService.statusは`Running`になる。|

位置情報の取得の確認ダイアログで許可を選択した場合に、`LocationService.status`は`Running`となる。

*"位置情報を利用できるようにするには、位置情報をサービスをオンにしてください"のダイアログ*
!["位置情報を利用できるようにするには、位置情報をサービスをオンにしてください"のダイアログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/3791027d-4a30-2c8c-b454-9329aef461ff.png)


### Android

|状態|LocationService.Start呼び出し後の動作|
|---|:---:|
|未選択・1度だけ拒否|位置情報の取得の確認ダイアログが表示される。LocationService.statusは`Stopped`のままで、許可を選択すると`LocationService.isEnabledByUser`はtrueになる。|
|許可済み|LocationService.statusは`Running`になる。|
|拒否|LocationService.statusは`Stopped`になる。|

位置情報の取得の確認ダイアログで許可を選択しても、`LocationService.isEnabledByUser`はtrueになるが`LocationService.status`の状態に変化はない。
位置情報を取得する場合はもう一度`LocationService.Start`を呼ぶ必要がありそう。



## LocationService.Stop

[LocationService.Stop](https://docs.unity3d.com/ScriptReference/LocationService.Stop.html)
位置情報サービスの更新を停止します。

### iOS/Android

`LocationService.status`が`Stopped`になります。
`LocationService.lastData`の値が更新されなくなります。

# まとめ

* iOSは「設定 > プライバシー > 位置情報サービス」がオンかオフかは`LocationService.isEnabledByUser`を見れば分かる
* iOSはアプリの位置情報取得が許可されているかどうかはあらかじめ分からない。`LocationService.Start`を呼んだ挙動で判定するしかなさそう。
* iOSアプリは位置情報取得の許可ダイアログが表示されるかどうかはあらかじめ分からない。`LocationService.Start`を呼んだときにOSにより位置情報取得の許可ダイアログが表示される。

* Androidはアプリの位置情報取得が許可されているかどうかは`LocationService.isEnabledByUser`を見れば分かる
* Androidアプリは位置情報取得の許可が無い場合（`LocationService.isEnabledByUser`がfalseの場合）に、位置情報取得の許可ダイアログが表示されるかどうかはあらかじめ分からない。`LocationService.Start`を呼んだときにOSにより位置情報取得の許可ダイアログが表示される。

位置情報が取得できるかどうかをiOSとAndroidの両方に対応しようとすると、`LocationService.Start`を呼んだ上で`LocationService.status`の値がどうなるかを監視するくらししかなさそうです。

# コード

https://github.com/yhirano/UnityLocationServiceResearch

# 参考

* [Unity - Scripting API: LocationService](https://docs.unity3d.com/ScriptReference/LocationService.html)
* [CLAuthorizationStatus - Core Location | Apple Developer Documentation](https://developer.apple.com/documentation/corelocation/clauthorizationstatus)
* [ActivityCompat  |  Android Developers](https://developer.android.com/reference/android/support/v4/app/ActivityCompat)
* [PermissionChecker  |  Android Developers](https://developer.android.com/reference/android/support/v4/content/PermissionChecker)
* [\[Unity\] Android/iOSで位置情報を取得する - Qiita](https://qiita.com/lycoris102/items/35e338b32105879e0ab9)