Android位置情報取得についての覚書き

## 概要
｢移動している状態で位置情報を欲しいとき高精度で最新かつほぼ最新の値で取りたい。けど、位置情報を更新し続けて常に最新を保つほどでもない。電池効率も悪いし。｣
という状況になったときに
FusedLocationProviderClient.getCurrentLocation()
に当たりました。
ただこのような有用メソッドがありつつ、あまり調べてもあまり良い記事に当たらなかったので(調べ方が雑なだけ?)
[AndroidDevelopers](https://developer.android.com/training/location/retrieve-current "直近の位置情報を取得する")にもちょこっと書いてあるだけで具体的なコードはなし。

正直まだ分からないところもありますが、他の位置情報取得APIと合わせてメモ書き
前置きはいらねーぜという方は｢getCurrentLocation()を使う｣だけ見れば良い

## LocationManager
位置情報取得の一例としてはLocationManagerを使うのが昔ながら(?)のやり方だと思う。
こちらはGoogle開発者サービスがなくても動くらしい(?)
でも、開発者サービス入ってない端末は基本的に考えなくてもいいとも思う。
下記のFusedLocationProviderClientとは厳密には性質が異なるようだが大抵目的は位置情報を取得したいなのでほぼ同一。
古いAPIなのだろうが、未だにサンプルコードとしてゴロゴロ紹介されていたりする。
ただ、ドキュメントを見るとFusedLocationProviderClientを使っており、こちらは内心非推奨なんだなと感じている。(おそらく概要の目的はこちらでもできるのだろうが)

今回はこちらの内容については割愛。

## FusedLocationProviderClient
本題
いくつかのメソッドを目的に合わせて記載

:::note warn
非同期や権限確認については省略してあります。
:::

### あまり頻繁に更新する必要がない、ざっくりとした位置でいい場合
FusedLocationProviderClient.getLastLocation()
```kotlin:getLastLocation.kt
fun getLastLocation(){
    val fusedLocationProviderClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)

    client.getLastLocation()
        .addOnSuccessListener{ location ->
            //成功
        }
        .addOnFailureListener{ exception ->
            //失敗
        }
}
```

取得に成功した場合、その値をしばらくの間使い回す
新たに取得リクエストをしない限り、使いまわし続ける(?)
覚えてさえいれば電池消耗を最小限に素早く取得できる。

### 正確かつ｢決まったタイミングで更新し続けたい｣場合
コールバック onLocationResultにて受け取り続ける
[現在地の更新情報をリクエストする](https://developer.android.com/training/location/request-updates)
に記載してある内容

```kotlin:MyLocationManager.kt
class MyLocationManager(
    private val context: Context,
    private val listener: OnLocationResultListener,
) : LocationCallback() {
    private val fusedLocationProviderClient: FusedLocationProviderClient =
        LocationServices.getFusedLocationProviderClient(context)

    interface OnLocationResultListener {
        fun onLocationResult(locationResult: LocationResult?)
    }

    override fun onLocationResult(locationResult: LocationResult) {
        super.onLocationResult(locationResult)
        listener.onLocationResult(locationResult)
    }

    fun startLocationUpdate() {
    val request = LocationRequest.Builder(5000L)
            .setWaitForAccurateLocation(true)
            .setPriority(Priority.PRIORITY_HIGH_ACCURACY)
            .build()

    fusedLocationProviderClient.requestLocationUpdates(request,this, null)

    }

    fun stopLocationUpdates() {
        fusedLocationProviderClient.removeLocationUpdates(this)
    }

}
```

```kotlin:LocationActivity.kt
class LocationActivity(): AppCompatActivity(),
{

    private var latitude: Double? = null
    private var longitude: Double? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initLocationManager()
    }

    private fun initLocationManager() {
        myLocationManager = MyLocationManager(this, this)
        myLocationManager?.startLocationUpdate()
    }

    override fun onLocationResult(locationResult: LocationResult?) {
        locationResult?.let {
            latitude = it.lastLocation?.latitude
            longitude = it.lastLocation?.longitude
        }
    }
}


```
requestLocationUpdates()を使うことで常にFusedLocationProviderClientに対して値を要求し続けられる。
LocationRequest.Builder()に要求時間間隔やGPS機能精度を加えてその内容で実行する。
止めたい場合は.removeLocationUpdates()を行う

当然、GPS精度の高いものを選んだり、時間間隔を短くすればその分電池消耗が激しくなる諸刃の剣。
Android側の通知欄にも位置情報ピンが立ち、利用状態がユーザーにすぐ分かるようになってる。
そこを鑑みた場合、実行シーンは限定したほうが良いだろう。


### 位置情報を欲しいとき高精度で最新かつ最新の値で取りたい
上記を見た場合、古い(古くなってる)値か常に最新を取り続けるしかないんか?
その中間(リクエストしたときだけ最新を返す)が欲しい。と思う。

いくつか調べたら下記みたいなrequestLocationUpdates()して1回取ったらremoveLocationUpdates()してすぐやめるみたいな方法もあった。
```kotlin:requestAndRemove.kt
    val request = LocationRequest.Builder(5000L)
            .setWaitForAccurateLocation(true)
            .setPriority(Priority.PRIORITY_HIGH_ACCURACY)
            .build()

        fusedLocationProviderClient.requestLocationUpdates(
            request,
            object : LocationCallback() {
                override fun onLocationResult(result: LocationResult) {
                    super.onLocationResult(result)
                    val lastLocation = result.lastLocation
                    val lat = lastLocation?.latitude
                    val lng = lastLocation?.longitude
                    //any Send Location Data

                    fusedLocationProviderClient.removeLocationUpdates(this)
                }

            },
            Looper.getMainLooper()
        )
```

#### getCurrentLocation()を使う
本題
上記のような方法もできるけど、それは使い方としては邪道寄りだよねということでちゃんと用意されていた。
おまけに[DevelopersBlog:都市部でアプリの GPS 精度を向上する方法](https://android-developers-jp.googleblog.com/2020/12/improving-urban-gps-accuracy-for-your-app.html)にあるじゃないか。

>デベロッパーは、現在の位置情報を取得する簡単な方法を求めていました。
>新しい getCurrentLocation() API を使えば、1 回のリクエストで現在の位置情報を取得できます。
>位置情報の変化を継続的に取りにいく必要はありません。この新しい API は、必要なときだけ位置情報をリクエストできるようにすることで（また、自動的にタイムアウトしてオープンな位置情報リクエストをクローズすることで）、電池の寿命も改善します。

内容も求めていたもの。
が、肝心のサンプルが404(流行らない原因?)
幸い調べて実用的なコードがあったので実装としては組み込めたが、ドキュメントを見たがイマイチ分からないところがあった。
識者はコメントにて補足お願いします。

```kotlin:currentLocation.kt

    private fun getCurrentLocation(){
        val currentLocationRequest = CurrentLocationRequest.Builder()
            .setDurationMillis(30000L) 
            .setMaxUpdateAgeMillis(60000L)
            .setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL) 
            .setPriority(Priority.PRIORITY_HIGH_ACCURACY) 
            .build()

        fusedLocationProviderClient.getCurrentLocation(
            currentLocationRequest,
            createCancellationToken()
        )
            .addOnSuccessListener {
                //成功
            }
            .addOnFailureListener {
                //失敗
            }
    }

    private fun createCancellationToken(): CancellationToken =
        object : CancellationToken() {
            override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken = CancellationTokenSource().token
            override fun isCancellationRequested(): Boolean = false
        }

```
- CurrentLocationRequest.Builder()
<dl>
    <dt>setDurationMillis</dt>
    <dd>おそらくリクエストのタイムアウト時間の設定
    <br>取得タイミングで何かの要因で取得できない、過去のロケーション情報すらない場合時間が経過したらNullを返却する。
    <br>デフォルト値はLong.MAX_VALUE、ドキュメントでは30秒程度を推奨している。
    </dd>
    <dt>setMaxUpdateAgeMillis</dt>
    <dd>おそらくアップデートを受け付けない間隔
    <br>設定された時間内に移動してもこの間の時間のリクエストであれば過去の取得できた値を返却する(はず)。
    <br>getLastLocation()のように電池消耗を防ぐ狙いだろう
    <br>0にすればリクエスト = 最新を取得しなおし。
    <br>デフォルト値は1分(60000ミリ秒)、ただ現在のAPIでは1分というだけで今後変更される可能性があるとのこと。
</dd>
    <dt>setGranularity</dt>
    <dd>Android12からおおまかの位置情報がパーミッションにて設定できるようになった。
    <br>このリクエストではどちらの権限を使うかの設定
    <br>デフォルト値はGranularity.GRANULARITY_PERMISSION_LEVEL(許可された権限の方を使う)
    </dd>
    <dt>setPriority</dt>
    <dd>精度。
    <br>通常のLocationRequestと同じ内容
    <br>デフォルト値はPriority.PRIORITY_BALANCED_POWER_ACCURACY(バランス)
    </dd>
</dl>

- CancellationToken
キャンセルされた場合のコールバック
キャンセルするようなケースの場合には適切に処理する。
(getCurrentLocation()はPlayサービスAPIだからTask型)
詳細は[CancellationToken](https://developers.google.com/android/reference/com/google/android/gms/tasks/CancellationToken)
正直良くわからないが、今回はキャンセルする必要がないので入れておくだけ良いと思う。

------
以上になります。
あまり小難しいことしなくても、一発で最新位置がとれるというのは便利だと思います。
ドキュメントにてちゃんと紹介すればいいのにとも思いました。

Androidにおいて位置情報はユースケースとして基本的だと思うので一助になれば幸いです。