位置情報設定をまとめて行うActivityを作ってみた

## 背景
Androidで位置情報を使う場合にすべきことは主に2つあります。

1. Permissionを有効にする
2. 端末の位置情報設定をONにする

上記２つを有効化するにはいくつか方法がありますが、今はアプリ内部だけでその処理を操作できるような仕組みが用意されています。

ただその方法が、少々手間…というか、まとめて設定したいのに以下のような理由からちょっと面倒くさい。。。

- 設定する許諾ダイアログを使うと、呼び出し元のActivityが中断される
- コールバックを受け取るためにはActivityが必要（Activityのメソッドが必要）
- google-play-servicesのv11.0未満だとGoogleApiClientが必須

そこで今回は、「Permissionも位置情報設定もまとめて有効化する専用Activity」を作って、どこからでも使えるようにしました。


## やりたいこと
ざっくりとですが、以下のようなフローで処理するActivityを作ります。

1. Activityを呼び出す
2. 最初にPermissionチェックをする
    - 許可されてない：許可のリクエストをする（ダイアログ表示）
    - 既に許可済み：→ 3
3. 次に位置情報設定のチェックをする
    - 設定されてない：設定のリクエストをする（ダイアログ表示）
    - 既に設定済み：呼び出し元へ結果を返却して戻る

実際の成果物は[こちら](https://github.com/jabachan/locset)

実際のコードについては以下で説明します。

## 1. Activityを呼び出す
（こちらは成果物のsampleをご確認ください）

## 2. 最初にPermissionチェックをする
まずPermissionについて少し説明します。

OS6.0以上からRuntime Permissionという仕組みが入ったことで、位置情報としてアプリ実行中に許可が必要なPermission(dangerous指定)は以下の２つです。

（Permission自体の詳細は[公式リファレンス](https://developer.android.com/reference/android/Manifest.permission.html)などで調べてください）

| permission | 内容 |
|:--|:--|
| ACCESS_COARSE_LOCATION | ネットワークベースによるおおよその位置情報へのアクセス |
| ACCESS_FINE_LOCATION | GPSなど特定の位置情報へのアクセス |


そこで、まず最初にActivityのonCreateにて、上記のPermissionが許可されているかどうか(OS6.0以上の場合)のチェックを行い、以下のように処理をします。

- 許可されてない：許可のリクエストをする
- 既に許可済み：位置情報設定の処理へ移行


```
private static final int REQUEST_CODE_LOCATION_PERMISSION = 1;
private static final String[] PERMISSIONS = {
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
};

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    if (isPermissionGranted()) {
        // (6.0未満 or 既に許可済み)
        // 許可されているので端末の位置情報設定を行う
        requestLocationSetting();
        return;
    }
    // (6.0以上)
    // パーミッションをリクエスト
    ActivityCompat.requestPermissions(this, PERMISSIONS, REQUEST_CODE_LOCATION_PERMISSION);
}

private boolean isPermissionGranted() {
    for (String permission : PERMISSIONS) {
        if (PermissionChecker.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
            return false;
        }
    }
    return true;
}
```

Permissionのリクエストがされると以下のようなダイアログが表示されます。

![permission_dialog.png](https://qiita-image-store.s3.amazonaws.com/0/84437/7feaf160-837b-ec94-7201-4c5516de049d.png)

次にダイアログからのコールバックを受け取りますが、許可が成功していたら位置情報設定へ移行、拒否したら終了させるようにします。

```
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    if (permissions.length != grantResults.length) {
        setResultCode(Locset.ResultCode.PERMISSION_FAILURE);
        return;
    }
    for (int i = 0; i < grantResults.length; i++) {
        if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
            // 拒否された
            //  1.まだ一度も要求していないパーミッションを渡した場合、falseが返る
            //  2.要求したパーミッションが、「今後は表示しない」というチェックを入れて拒絶された場合、falseが返る
            //  3.要求したパーミッションが拒絶されたことがあり、かつ今後表示しないチェックがつけられていない場合、trueが返る
            //  注）1と2が判別できないので注意
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
                // 呼び出し元へ結果を返却（失敗）
                finishForResult(Locset.ResultCode.PERMISSION_FAILURE);
            } else {
                // 呼び出し元へ結果を返却（失敗）
                finishForResult(Locset.ResultCode.PERMISSION_FAILURE_DO_NOT_ASK_AGAIN);
            }
            return;
        }
    }
    // 許可されたので端末の位置情報設定を行う
    requestLocationSetting();
}

private void finishForResult(@Locset.ResultCode int resultCode) {
    setResult(resultCode);
    finish();
}
```

以上でPermissionの処理は終了です。

## 3. 次に位置情報設定のチェックをする
位置情報の設定は、端末設定画面や通知領域から直接行う方法もありますが、[GooglePlayServices](https://developers.google.com/android/guides/setup)の機能を使うことでアプリ内から直接設定することができます。

とりあえずbuild.gradleにplay-services-locationを追加します。
```
implementation 'com.google.android.gms:play-services-location:11.8.0'
```

ちなみに、GooglePlayServicesのバージョンによって実装が少し異なっています。
はっきり言えばv11以上の方が実装がかなり楽になってます。

| version | 実装方法 |
|:--|:--|
| 11.0 未満 | GoogleApiClientが必要<br>LocationServices.SettingsApiを使う<br>[リファレンス](https://developers.google.com/android/reference/com/google/android/gms/location/SettingsApi) |
| 11.0 以上  | SettingsClientを使う<br>[リファレンス](https://developers.google.com/android/reference/com/google/android/gms/location/SettingsClient) |

また位置情報設定には、欲しい情報の精度やバッテリー消費量などを考慮できるようになっています。
基本的には以下の4つのPriorityを指定することができます。

```
public final class LocationRequest extends zzbfm implements ReflectedParcelable {
    public static final int PRIORITY_HIGH_ACCURACY = 100;
    public static final int PRIORITY_BALANCED_POWER_ACCURACY = 102;
    public static final int PRIORITY_LOW_POWER = 104;
    public static final int PRIORITY_NO_POWER = 105;
```

AndroidではOSバージョン(機種別もあるかも)で位置情報設定の項目が多少異なっていますが、このGooglePlayServicesを利用すると、上記のPriorityに適した設定にしてくれるメリットがあります。

### v11.0未満
v11未満だと、まずGoogleApiClientの接続処理をする必要があります。

```
private void requestLocationSetting() {
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addApi(LocationServices.API)
            .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
                @Override
                public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
                    finishForResult(Locset.ResultCode.GOOGLE_API_FAILURE);
                }
            })
            .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
                @Override
                public void onConnected(Bundle bundle) {
                    // 接続後に位置情報設定の取得を開始
                    requestLocationSettingGoogleApi();
                }

                @Override
                public void onConnectionSuspended(int i) {
                    finishForResult(Locset.ResultCode.GOOGLE_API_FAILURE);
                }
            }).build();
    mGoogleApiClient.connect();
}

```
GoogleApiClientに接続後、実際に端末の位置情報設定に対するチェックと、設定するためのダイアログを表示するための処理を行います。（補足ですが、Permissionが無くてもこの設定処理は可能です）

![locationsetting.png](https://qiita-image-store.s3.amazonaws.com/0/84437/d05525ac-d548-fe59-bb80-2b2a5602e82d.png)

```
private static final int REQUEST_CODE_LOCATION_SETTING = 2;

private void requestLocationSettingGoogleApi() {
    final LocationSettingsRequest request = new LocationSettingsRequest.Builder()
            .setAlwaysShow(true) //これをしないと変更しなかった場合に次に呼び出せなくなる
            .addLocationRequest(LocationRequest.create().setPriority(mPriority))
            .build();

    final PendingResult<LocationSettingsResult> result = LocationServices.SettingsApi.checkLocationSettings(
            mGoogleApiClient,
            request);

    result.setResultCallback(new ResultCallback<LocationSettingsResult>() {
        @Override
        public void onResult(@NonNull LocationSettingsResult locationSettingsResult) {
            final Status status = locationSettingsResult.getStatus();
            switch (status.getStatusCode()) {
                case LocationSettingsStatusCodes.SUCCESS:
                    // 指定した優先度の位置情報が利用できる
                    finishForResult(Locset.ResultCode.SUCCESS);
                    return;
                case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                    try {
                        // ユーザに位置情報設定を変更してもらうためのダイアログを表示する
                        status.startResolutionForResult(LocsetActivity.this, REQUEST_CODE_LOCATION_SETTING);
                        return;
                    } catch (IntentSender.SendIntentException e) {
                        // failure
                    }
                    break;
                case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                    // 位置情報が取得できず、その状態からの復帰も難しい時呼ばれる
                    break;
            }
            finishForResult(Locset.ResultCode.LOCATION_SETTING_FAILURE);
        }
    });
}
```

最後にonActivityResultで設定結果のコールバックを受け取ります。

```
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    // 位置情報設定ダイアログからのコールバックを受け取る
    if (requestCode == REQUEST_CODE_LOCATION_SETTING) {
        if (resultCode == Activity.RESULT_OK) {
            finishForResult(Locset.ResultCode.SUCCESS);
        } else {
            finishForResult(Locset.ResultCode.LOCATION_SETTING_FAILURE);
        }
    }
}
```

### v11.0以上
v11以上では、GoogleApiClientが不要になり、SettingsClientというクラスを使って行います。
コールバックの受け取り方には特に変更はありません。

```
private void requestLocationSetting() {
    final LocationSettingsRequest request = new LocationSettingsRequest.Builder()
            .setAlwaysShow(true)
            .addLocationRequest(LocationRequest.create().setPriority(mPriority))
            .build();
            
    final Task<LocationSettingsResponse> result = LocationServices.getSettingsClient(this)
            .checkLocationSettings(request);

    result.addOnCompleteListener(new OnCompleteListener<LocationSettingsResponse>() {
        @Override
        public void onComplete(@NonNull Task<LocationSettingsResponse> task) {
            try {
                task.getResult(ApiException.class);
                // 既に設定済み
                finishForResult(Locset.ResultCode.SUCCESS);
            } catch (ApiException exception) {
                switch (exception.getStatusCode()) {
                    case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                        try {
                            // ユーザに位置情報設定を変更してもらうためのダイアログを表示する
                            resolvable.startResolutionForResult(LocsetActivity.this, REQUEST_CODE_LOCATION_SETTING);
                            return;
                        } catch (IntentSender.SendIntentException e) {
                            // Ignore the error.
                        } catch (ClassCastException e) {
                            // Ignore, should be an impossible error.
                        }
                        break;
                    case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                        // 位置情報が取得できず、その状態からの復帰も難しい時呼ばれる
                        break;
                }
                finishForResult(Locset.ResultCode.LOCATION_SETTING_FAILURE);
            }
        }
    });
}
```

## 最後に
というわけで出来上がったのが[こちら](https://github.com/jabachan/locset)になります。