【Android】Google Location Services APIを使って位置情報を取得する方法

# はじめに
2017年頭に「[Android6.0以上の端末で動作するGoogleMapsアプリを作成ろうとしていきなりハマったことまとめ](https://qiita.com/hotdrop_77/items/bb8c97a3c197ee4f44df)」という記事を書きました。
この時は結局パーミッションの解説がメインになってしまい、かつよく調べていなかったため位置情報の取得に推奨されていない`Android Location API`を使用してしまいました。
最後のまとめに「`Google Location Services API`も機会があれば試そうと思います」と書いてから1年以上過ぎてしまいましたが、試した結果を書き残すことにしました。
検証したコードはGitHubに上げております。
https://github.com/hotdrop/pinmap

# Android Location APIとGoogle Location Services API
「[Android6.0以上の端末で動作するGoogleMapsアプリを作成ろうとしていきなりハマったことまとめ](https://qiita.com/hotdrop_77/items/bb8c97a3c197ee4f44df)」では位置情報の取得に`android.location`を使用していました。
しかし、developerサイトの[android.locationのリファレンス](https://developer.android.com/reference/android/location/package-summary)によれば

「このAPIでの位置情報取得は推奨しない。シンプルなAPI、高精度で低電力なジオフェンシングを提供する`Google Location Services API`の利用を強く推奨する。」

とあります。かなり今更ですがどうやら2013年のGoogle I/Oで発表があったようです。
実際、かなりシンプルかつ少量のコードで位置情報が取得できました。

# Google Location Services APIについて
このAPIは`Google Play Services APIs`というAPI群の一部で`Google Play services APK`(Google開発者サービス)で提供されています。
そのためこのAPIを使ったアプリを利用する場合、端末に「Google開発者サービス」アプリがインストールされており、かつ一定のバージョン以上である必要があります。
「Google開発者サービス」はほとんどのAndroid端末にインストールされていると思いますが、インストールされていない端末もありますしアップデートされていない可能性もあります。そのため、本APIを使用するにあたって「Google開発者サービス」のバージョンを確認する必要があるようです。
バージョンの確認方法等についてはdevelopersサイトの「[Set Up Google Play Services](https://developers.google.com/android/guides/setup)」を参照ください。
(本記事の検証ではバージョン確認は行なっていませんがGitHubに上げたサンプルコードでは一応チェックしています。)

# Google Location Services APIを使った位置情報の取得
以下、位置情報の取得方法です。

## 1. Gradleの指定
必要なライブラリは`play-services-location`です。2018年10月現在、https://developers.google.com/android/guides/setup に記載されていたverを書いています。

```build.gradle
implementation 'com.google.android.gms:play-services-location:16.0.0'
```

## 2. Clientを生成
`Google Play Service 6.5`以前は`LocationClient`というのが使われており、6.5から廃止されて代わりに`GoogleApiClient`を使うことになったようです。
ただ、Google Developersのブログ「[新しい Location API で負荷を軽減](https://developers-jp.googleblog.com/2017/07/reduce-friction-with-new-location-apis.html)」では、`GoogleApiClient`でLocationを扱う場合、ボイラープレートが多くなるし必要な項目を設定していないと予期せぬエラーになって良くないといっています。
代わりに`Google Play Service SDK 11`から提供された`LocationServices`を使用することを推奨しています。

```kotlin
val client = LocationServices.getFusedLocationProviderClient(this)
```

`FusedLocationProviderClient`とはシンプルでバッテリー効率の良い位置情報APIを提供してくれるクライアントです。
詳細は以下のdevelopersサイトを参照してください。
https://developers.google.com/location-context/fused-location-provider/　

## 3. 位置情報の取得
現在地を取得するコードです。最後に端末が認識した位置を取得するのでlastLocation（JavaだとgetLastLocation())というメソッド名になっています。

```kotlin
LocationServices.getFusedLocationProviderClient(this).let { client ->
  client.lastLocation.addOnCompleteListener(this) { task ->
    if (task.isSuccessful && task.result != null) {
      // 位置情報を取得できた場合
      // task.result.latitude、task.result.longitude で位置情報を取得
    } else {
      // 端末の位置情報をOFFにしているなどで、位置情報が取得できなかった場合
    }
  }
}
```

検証コードの全体像は以下を参照ください。
https://github.com/hotdrop/pinmap/blob/master/app/src/recentMap/java/jp/hotdrop/pinmap/MapActivity.kt

## 4. 余談
 - 位置情報取得時の精度など  
`getLastLocation`で位置情報を一度だけ取得する場合は、位置情報の精度などは設定の必要はなさそうです。  
定期的に位置情報を取得したり位置情報を変更する場合、`LocationRequest`で更新間隔や優先度を設定します。  
間隔や精度の詳細についてはdeveloperサイトの[Change location settings](https://developer.android.com/training/location/change-location-settings)を参照ください。
 - 後処理  
こちらも同様に`getLastLocation`で位置情報を一度だけ取得する場合は何もしなくてよさそうです。  
developerサイトの[Get the last known location](https://developer.android.com/training/location/retrieve-current)でも後処理については何も触れていませんし、GitHubの[android-play-locationサンプル](https://github.com/googlesamples/android-play-location)のBasicLocationSampleでも特に後処理していません。多分大丈夫だと思います。  
なお、定期的に位置情報を取得したり位置情報を変更する場合はonStop等で設定したコールバックをremoveする必要があります。

# まとめ
前回と比較するため今回も「アプリ起動時に1度だけ、最後に端末が受信した位置情報を取得する」という検証をしました。
`Google Location Services API`を使うと`android.location`よりはるかに楽に位置情報を取得できます。
あと余談ですがPermissionDispatcherも素晴らしいです。（前回の記事はほとんどRuntimePermissionの話だったため。）
この記事では取り扱いませんが、PermissionDispatcherを使うと非常に簡単にRuntimePermissionを実装できて便利です。

GitHubのサンプルコードも参照ください。
https://github.com/hotdrop/pinmap

比較しやすいよう前回地図記事を書いた時のサンプルコードと同じリポジトリに入れています。
KotlinとPermissionDispatcherの恩恵が大きいですが`Google Location Services API`を使うことでコード量も複雑さも減っています。

# 2018/11/3追記
lastLocationで位置情報を取得しようとすると`task.result`がnullで返ってきて一向に位置情報が取れないケースがありました。
（task.isSuccessfulはtrueで、task.resultがnullの状態です。）
操作は次の通りです。
  1. 位置情報をOFF
  2. アプリ起動（位置情報取得エラー）
  3. アプリ閉じて位置情報をON
  4. アプリ起動（ここで取得できる端末と何回やっても取得できない端末に分かれる）

最初はAndroid4.4端末で発生したのでKitkat特有のケースかと思いましたが、Android7.0のキャリア端末でも発生したのでなんか全般的に発生の可能性があるっぽいです。
`lastLocation`メソッドは上述したように最後に取得した位置情報を持ってくるのでキャッシュされていないとか位置情報がなくてnullになる可能性はありそうです。
developersサイトの[lastLocationメソッドの説明](https://developers.google.com/android/reference/com/google/android/gms/location/FusedLocationProviderClient)にも以下のようなことが書いてありました。
>If a location is not available, which should happen very rarely, null will be returned.

他にもググってみるとstackoverFlowなどで「nullが返ってくるんだけど・・」と言う話がチラホラ引っかかりました。
以下の質問の2つ目の回答が調べた中では結構わかりやすく、最後の位置情報の取得に間に合っていないような記述もありました。
https://stackoverflow.com/questions/29796436/why-is-fusedlocationapi-getlastlocation-null/29804454

と言うわけで、1回だけ位置情報を取得したい場合でも`lastLocation`メソッドだけだと微妙で、位置情報を通常の位置情報取得アプリのようにintervalを設定しコールバックで位置情報を取得する方法と合わせた方が安定して位置情報を取得できることが分かりました。

## 1. 位置情報の取得(位置情報の更新ありパターン)
「3. 位置情報の取得」の位置情報更新ありパターンです。位置情報が取得できなかった場合、`getUpdateLocation()`と言う更新メソッドを呼ぶようにしました。

```kotlin
// Gpsで位置情報を取得
LocationServices.getFusedLocationProviderClient(activity).let { client ->
  client.lastLocation.addOnCompleteListener(this) { task ->
    if (task.isSuccessful && task.result != null) {
      // 位置情報を取得できた場合
      // task.result.latitude、task.result.longitude で位置情報を取得
    } else {
      // 端末の位置情報をOFFにしているなどで、位置情報が取得できなかった場合
      getUpdateLocation()
    }
  }
}
```

## 2. 位置情報の更新設定
位置情報の更新通知を受診するコールバックを用意し`fusedLocationClient`にセットします。
`lastLocation`の時は1度実行されるだけでしたので後処理は考えませんでした。しかし、コールバックを設定するこの方法では裏でコールバックが定期間隔で動き続けるため不要になった時点でremoveする必要があります。そのためフィールドに切り出しました。
2回目以降、`lastLocation`で位置情報が取れればこいつらは不要なのでコールバックはlazyで必要な時のみ初期化しています。
`fusedLocationClient`をlazyではなくnull許容にしている理由は後述する後処理で使うためです。

```kotlin
private var fusedLocationClient: FusedLocationProviderClient? = null
private val locationCallback: LocationCallback by lazy {
    (object: LocationCallback() {
        override fun onLocationResult(p0: LocationResult?) {
            super.onLocationResult(p0)
            // 位置情報の更新が取得できた場合
            // p0?.lastLocationのlatitudeとlongitudeで位置情報を取得
        }
    })
}

private fun getUpdateLocation() {
    // とりあえず1回だけ取得
    val request = LocationRequest.create().apply {
        interval = 10000 // ms
        numUpdates = 1
    }
    fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
    fusedLocationClient?.let { client ->
        client.requestLocationUpdates(request, locationCallback, null)
                .addOnCompleteListener { task ->
                    if (task.result == null) {
                        // 1回目で失敗した場合は必ずここを通ります。
                        // その後、コールバックがうまくいけばlocationCallbackのonLocationResultに入ります。
                    }
                }
    }
}
```

## 3. 後処理
最後に設定したコールバックを削除します。ユーザーが一旦アプリを離れたら削除した方がいいかと思ってonStopにしています。
地図メインのアプリなどはonStartで更新を再開するとか復帰処理が入るかと思います。
また、今回のサンプルケースでは更新回数を1にしているので、それほど後処理にこだわる必要はないかもしれません。ただ普段の癖で書いておいたほうがいいかなと思います。
仮にアプリを開いた瞬間に位置情報の取得に失敗したのでアプリを閉じてしまう（裏でプロセスは動いている）ユーザーがいると考えると、裏で走る1回分のコールバックのバッテリー消費はないに越したことはないと思います。
と、書いてて思いましたがひょっとしたらこの程度ならAndroid7.0以降はデータセーバーがよしなにやってくれるかもしれませんね・・

```kotlin
override fun onStop() {
    super.onStop()
    fusedLocationClient?.removeLocationUpdates(locationCallback)
}
```