Android10でGPSの2点から距離を求める

# 環境

IDE : Android Studio 4.0.1
OS : Android10
言語 : kotlin
ライブラリ : okhttp, YOLP(Yahoo API)

# ライブラリの登録

```build.gradle
dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'com.squareup.okhttp3:okhttp:4.4.0'
}
```

保存したら右クリックでsync nowを選択し、ライブラリをIDEに反映させる。

# パーミッションの追加

```AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.takilab.aroundhokkaido">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
```

インターネットへのアクセスを有効にする権限
android.permission.INTERNET

位置情報を取得する権限
android.permission.ACCESS_COARSE_LOCATION
android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_BACKGROUND_LOCATION

大文字小文字に注意すること（ハマったのは内緒）

# UI

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/locationText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="[0]"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/distanceText"
        android:layout_width="76dp"
        android:layout_height="17dp"
        android:layout_marginTop="24dp"
        android:text="[km]"
        android:textAlignment="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/locationText" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

位置情報と距離情報をとりあえず画面に出す。

# コード

```
package com.takilab.aroundhokkaido

import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.location.Location
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.ActivityCompat
import com.google.android.gms.location.*
import kotlinx.android.synthetic.main.activity_main.*
import okhttp3.*
import org.json.JSONObject
import java.io.IOException

class MainActivity : AppCompatActivity() {
    companion object {
        private const val PERMISSION_REQUEST_CODE = 1234
    }

    private val appid = "ひみつー"

    private lateinit var fusedLocationClient: FusedLocationProviderClient
    private lateinit var locationCallback: LocationCallback

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

        requestPermission()

        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)

        var updatedCount = 0
        var prevLocation : Location? = null

        val client : OkHttpClient = OkHttpClient()
        val url : String = "https://map.yahooapis.jp/dist/V1/distance?output=json&coordinates=%f,%f %f,%f&appid=%s"
        // 位置情報取得時のコールバック定義オブジェクト
        locationCallback = object : LocationCallback() {
            /**
             * 位置情報取得時の処理
             */
            @SuppressLint("SetTextI18n")
            override fun onLocationResult(locationResult: LocationResult?) {
                locationResult ?: return
                for (location in locationResult.locations) {
                    updatedCount++
                    if (prevLocation == null) {
                        locationText.text = "[${updatedCount}] ${location.latitude} , ${location.longitude}"
                    } else {
                        locationText.text = "[${updatedCount}] ${location.latitude} , ${location.longitude}" +
                                " - ${prevLocation!!.latitude} , ${prevLocation!!.longitude}"
                        // HTTPリクエストを作成
                        val request = Request.Builder()
                            .url(url.format(location.longitude, location.latitude, prevLocation!!.longitude, prevLocation!!.latitude, appid))
                            .build()
                        // HTTPリクエストを送信
                        client.newCall(request).enqueue(object : Callback {
                            /**
                             * HTTPリクエスト失敗時の処理
                             */
                            override fun onFailure(call: Call, e: IOException) {
                                println("fail : $e")
                            }

                            /**
                             * HTTPリクエスト成功時の処理
                             */
                            override fun onResponse(call: Call, response: Response) {
                                var str = response!!.body!!.string()
                                val jsonObject = JSONObject(str)
                                val jsonArray = jsonObject.getJSONArray("Feature")
                                for (i in 0 until jsonArray.length()) {
                                    val jsonData = jsonArray.getJSONObject(i)
                                    val geometry = jsonData.getJSONObject("Geometry")
                                    val distance = geometry.getDouble("Distance")
                                    // ハンドラを経由してメインスレッドからUIにアクセスしないとエラーになる。
                                    val mainHandler : Handler = Handler(Looper.getMainLooper())
                                    mainHandler.post(Runnable {
                                        distanceText.text = "$distance"
                                    })
                                }
                            }
                        })
                    }
                    prevLocation = location
                }
            }
        }
    }

    override fun onResume() {
        super.onResume()
        startLocationUpdates()
    }

    override fun onPause() {
        super.onPause()
        stopLocationUpdates()
    }

    /**
     * 権限の問い合わせを行う。
     */
    private fun requestPermission() {
        // 位置情報にアクセスする権限があるかどうかを確認する。
        if (!checkPermission()) {
            // 権限が無いため、許可を求める
            ActivityCompat.requestPermissions(this,
                arrayOf(
                    Manifest.permission.ACCESS_COARSE_LOCATION,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_BACKGROUND_LOCATION
                ),
                PERMISSION_REQUEST_CODE)
        }
    }

    /**
     * 権限の確認を行う
     */
    private fun checkPermission() : Boolean {
        return ActivityCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
                    ActivityCompat.checkSelfPermission(this,
                        Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
                    ActivityCompat.checkSelfPermission(this,
                        Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED
    }

    /**
     * 位置情報の取得開始
     */
    private fun startLocationUpdates() {
        // 権限の再確認（無いとIDE（Android Studio）に怒られる）
        if (!checkPermission()) {
            requestPermission()
            return
        }
        // 位置情報の取得開始
        val locationRequest = createLocationRequest() ?: return
        fusedLocationClient.requestLocationUpdates(
            locationRequest,
            locationCallback,
            null)
    }

    /**
     * 位置情報の取得を停止する
     */
    private fun stopLocationUpdates() {
        // 位置情報の取得停止
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }

    /**
     * 位置情報を行うインスタンスの作成
     */
    private fun createLocationRequest(): LocationRequest? {
        return LocationRequest.create()?.apply {
            interval = 10000
            fastestInterval = 5000
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        }
    }
}
```

GPSの情報を取得する前に、アプリに権限が付与されているかを確認する。
権限が無ければユーザーに権限付与を求めるダイアログを表示する。
startLocationUpdates()でGPSの情報を取得開始。
GPSのデータを取得すると、onLocationResult()が実行される。
現位置と前位置の情報を元に、YahooのAPIを使用して2点間の距離を問い合わせる。
Request.Builder()でHTTPリクエストを作成し、client.newCall()で送信。
HTTPレスポンスを受信するとonResponse()が実行される。
レスポンスからJsonを取得し、解析して、距離データ(km)を取得する。
UIに反映させるには、メインスレッドでなければエラーになるので、
ハンドラを使用してメインスレッドからUIに反映させる。