Hituzi Ando's Blog

日々のアプリ開発についてや雑記など。

【Android】LocationManagerを使って位置情報をなるべく取得する

こんにちは、アプリ開発者の@HituziANDOです。

この記事では、AndroidのLocationManagerを使って位置情報をなるべく取得する方法を紹介します。"なるべく"取得するとは、位置情報の精度よりも取得できることそのものに重きを置いています。というのも、先日、LocationManagerを使ったアプリを作っていたのですが、位置情報が全然取得できないという事象に見舞われました。原因はGPS測位のみ有効にしていて、屋内で測位していたため、取得に時間がかかった or 取得できなかったようです。

GPS測位とネットワーク測位

AndroidのLocationManagerにはGPS測位とネットワーク測位の2種類があります。GPS測位とは衛星を使った位置測位であるのに対し、ネットワーク測位とは基地局Wi-Fiアクセスポイントを使った位置測位です。それぞれの特徴を大まかにまとめました。

GPS測位 NW測位
精度 高い 低い
測定時間 遅い 速い
バッテリー消費 多い 少ない

上記より、粗くてもとにかく位置情報を取りたいという場合はNW測位を使い、精度が最重要という場合はGPS測位を使うと良さそうです。ということで、なるべく取得できるようにするためには、NW測位を採用すると良いです。

ところで、この2つの測位方法ですが、同時には片方しか使えないわけではなく、同時に両方使うことができます。以下の実装例は同時に両方を使う例です。

LocationManagerを使った実装

AndroidManifest.xml

AndroidManifest.xmlに以下のパーミッションを追加します。ACCESS_FINE_LOCATIONはGPS測位用、ACCESS_COARSE_LOCATIONはNW測位用です。

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

どちらのパーミッションもProtection level: dangerousなため、ユーザ認可が必要です。

MainActivityクラス

ここでは、MainActivityクラスで位置測位するものとします。(この画面が表示されているときだけ位置測位します)

private const val REQUEST_CODE_PERMISSIONS = 1

class MainActivity : AppCompatActivity() {

    private val locationManager: LocationManagerUtil by lazy {
        val manager = LocationManagerUtil(this)
        manager.listener = locationListener
        manager
    }

    private val locationListener: LocationManagerUtil.Listener = object : LocationManagerUtil.Listener {
        // 位置情報に更新があると呼ばれる
        override fun onLocationUpdated(location: Location) {
            Log.d("""
                provider: ${location.provider} 
                lat: ${location.latitude} 
                lon: ${location.longitude} 
                acc: ${location.accuracy}
                """)
        }
    }

    private var isPermissionDenied = false
        set(value) {
            field = value

            if (!value) {
                // ユーザに権限が与えられているならば位置測位開始
                locationManager.start()
            }
            else {
                locationManager.stop()
            }
        }

    override fun onResume() {
        super.onResume()

        isPermissionDenied = !checkSelfPermissions()
    }

    override fun onPause() {
        super.onPause()

        // 位置測位の終了
        locationManager.stop()
    }

    private fun checkSelfPermissions(): Boolean {
        val deniedPermissions = arrayListOf<String>()

        // 位置情報の権限が与えられているか
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            deniedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
        }

        if (deniedPermissions.size > 0) {
            // 権限が無い場合はユーザに認可してもらえるようにリクエスト(ダイアログが表示される)
            ActivityCompat.requestPermissions(this, deniedPermissions.toTypedArray(), REQUEST_CODE_PERMISSIONS)
            return false
        }

        return true
    }

    // requestPermissionsの結果が返ってくる
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (grantResults.isNotEmpty()) {
                val deniedPermissions = arrayListOf<String>()

                grantResults.forEachIndexed { index, result ->
                    if (result != PackageManager.PERMISSION_GRANTED) {
                        deniedPermissions.add(permissions[index])
                    }
                }

                isPermissionDenied = deniedPermissions.isNotEmpty()

                if (isPermissionDenied) {
                    // TODO: ユーザに権限が与えられなかったときの処理
                }
            }

            return
        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
}

ポイントは位置情報の権限をチェックして、ユーザに認可されているならば位置測位を開始するようにしています。もし、権限が与えられていない場合は、権限を要求し、その結果でもって位置測位を開始するか判断しています。

LocationManagerUtilクラスは後述するLocationManagerのラッパークラスです。

LocationManagerUtilクラス

// 位置情報更新の最小間隔
private const val MIN_INTERVAL = 10 * 1000L // [ms]
// 位置情報更新の最小距離
private const val MIN_DISTANCE = 3.0F   // [meter]

class LocationManagerUtil(context: Context) {

    interface Listener {
        fun onLocationUpdated(location: Location)
    }

    var listener: Listener? = null
    var currentLocation: Location? = null
        private set
    private var isStarted = false

    private val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager

    private val locationListener: LocationListener = object : LocationListener {

        // GPS測位またはNW測位で位置情報が更新されると呼ばれる
        override fun onLocationChanged(location: Location?) {
            // TODO: 更新時間や精度などからGPS測位結果とNW測位結果のどちらかより良い方を採用する処理

            currentLocation = location

            location?.apply { listener?.onLocationUpdated(this) }
        }

        override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
        }

        override fun onProviderEnabled(provider: String?) {
        }

        override fun onProviderDisabled(provider: String?) {
        }
    }

    @SuppressLint("MissingPermission")
    fun start() {
        if (!isStarted) {
            isStarted = true

            // GPS測位とNW測位を開始する
            locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, MIN_INTERVAL, MIN_DISTANCE, locationListener)
            locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, MIN_INTERVAL, MIN_DISTANCE, locationListener)
        }
    }

    fun stop() {
        if (isStarted) {
            isStarted = false
            locationManager.removeUpdates(locationListener)
        }
    }
}

GPS測位とNW測位を開始しています。いずれも位置情報が更新されるとLocationListenerのonLocationChangedメソッドが呼ばれます。MIN_INTERVALとMIN_DISTANCEは位置情報が更新される頻度を調整するパラメータです。

上記の実装により、GPS測位できない状況下でもNW測位で救うことができる可能性があります。ただし、GPS測位もNW測位もできる場合、onLocationChangedメソッドが2重で呼ばれるため、どちらかより良い結果を採用するなど、ひと手間加えた方が良いと思います。

さいごに

AndroidのLocationManagerを使って位置情報を取得する方法を紹介しました。ところで、Android公式ドキュメントでは、FusedLocationProviderClientを使う方法が紹介されています。最近はこちらを使った方が良いのかもしれません。