位置情報を正確にトラッキングする技術 in Kotlin — (第1回) バックグランドでの位置情報トラッキングを可能にするアーキテクチャ

UberやNike+、Pokemon Goのようなアプリのコアとなる位置情報トラッキング技術についてiOS、Android(Java)での実現方法について解説して来ました。
https://medium.com/location-tracking-tech

Android向けにJavaのサンプルコードを使った説明はこちらから始まる4回のブログシリーズを参照してください。

Android Studioが正式にKotlinをサポートしたため、今回このJavaのコードをKotlinですべて書き換えるとともにサポートライブラリ等を使いコードをシンプルにしました。またAndroid OS Oreo(API Level 26)からバックグランドでサービスを立ち上げて継続的な処理をすることに制限がかかったため位置情報を継続して取得するLocationServiceをOreo以降のAndroid OSでも動かすための対策を入れました。

今回はKotlin版のコードサンプルをつかいながら位置情報を正確にトラッキングする方法を解説を4回に渡って説明して行きたいと思います。ほとんどの部分はJava版のブログと重複しますが、最後までお付き合いください。

それでは、まずサンプルアプリの土台から作っていきましょう。

Projectの作成

まずAndroid Studioでプロジェクトを作りEmpty Activityを追加します。

LocationService class

Web上にある位置情報取得のチュートリアルやサンプルはActivityから位置情報取得を行うサンプルがほとんどですが、このやり方ですとActivityがAndroidにkillされた場合位置情報の取得も途絶えてしまいます。アプリが長時間バックグランドにいるような状態でも位置情報を取得し続けるためにはServiceを立ち上げそこから位置情報をトラッキングするのが一番確実な方法です。

そこでまずServiceの子クラスを作って位置情報トラッキングに特化したサービスにカスタマイズしていきましょう。

ファイルを配置したいパッケージを右クリックしNew > Service > Serviceからサービスクラスを作ります。

名前は下のようにLocaitonServiceとし、

LocationServiceクラスは他のアプリから参照するものではないので、Exportedフラグのチェックは外してください。

これで、Manifest.xmlにLocationServiceができていると思いますが、念のため、下のようなXMLのエレメントが追加されているか確認してください。

Permissions

次に同じManifest.xmlファイル内でパーミッションの設定をします。

ACCESS_FINE_LOCATIONだけを設定すればいいですが、開発段階では、エミューレーターからのダミーの位置情報も受信したいので、ACCESS_MOCK_LOCATIONもONにします。

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

LocationServiceの全体像

LocationServiceの各実装を見ていく前に、まず先に完成したものを下に貼りました。以降のチャプターで、このなかを1つずつ解説していきたいと思います。

それではここからLocationServiceの中の実装を行って行きますが、
こちらのリンク が、上記のLocationServiceのコードのGistのリンクになりますので、こちらを別のブラウザウィンドウで開きながら読むとわかりやすいと思います。

LocationManager

LocationServiceの一番の目的は位置情報を取得することですが、位置情報を取得するために一番重要なクラスがLocationManagerクラスです。

LocationServiceクラスの中ではstartUpdatingLocation()というメソッドを定義し、そのなかでLocationManagerオブジェクトを初期化します。

val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager

Criteria

LocationManagerが位置情報を取得する際にどのような正確さ、パフォーマンスで位置情報を取得してほしいのか、を記述するのがCriteaです。Criteriaオブジェクトは以下のステップで作成、設定していきます。

  • Criteriaを用意する
  • Minimum Time (前回位置情報を取得してから最低どれだけ待って次の位置情報を取得するか)を設定する。
  • Minimum Distance(前回位置情報を取得してから最低どれだけの距離をユーザーが移動したら次の位置情報を取得するか)を設定する。
  • accuracyをACCURACY_FINEにする
  • PowerRequirementを(to get best GPS signal)
  • altitude(高度)は今回使わないので、altitudeRequiredをfalseにする
  • locationManagerのスピード算出機能は今回使わないので、speedRequiredをfalseにする。
  • isCostAllowedフラグをtrueにする。このフラグをオンにするとLocationManagerが基地局とデータのやり取りをして位置情報の制度を高めることになります。基地局とのデータのやり取りが発生するのでパケット量が増えユーザーが通信事業者に支払うデータ料金が増える可能性があるため”CostAllowed”という名前になっています。
    Androidに搭載されているGPSはA-GPS(Assisted GPS)といって基地局の位置情報とそこからの距離でGPS衛星からの位置情報を補完するタイプのGPSのため、この機能をオンにするかしないかで位置情報の精度が大きく変わります。高精度の位置情報を取得するのであればオンにしなければなりませんが、パケット量に関してユーザーにフェアな設計にしたい場合は一度アラートを出してこの位置情報取得に基地局とのデータやり取りが発生してもいいか聞いてからこのフラグをオンにするというような実装をしてもいいかもしれません。
  • bearingRequiredをfalseにします。. “Bearing”とは、デバイスが指し示している方向のことです。今回方向の情報は使わないのでfalseにします。
  • horizontalAccuracyとverticalAccuracyをHIGHに設定します。

最後にLocationManagerのrequestLocationUpdatesを呼びます。この時Minimum Timeを5秒、Minimum Distanceを5メートルに指定します。また先ほど作ったCriteriaもパラメーターとして渡します。
Minimum TimeとMinimum Distanceは位置情報取得時のコールバックが頻繁に呼ばれるのを避けるためです。位置情報取得後に様々な描画処理や計算処理をする場合はMinimum TimeとMinimum Distanceを設定しないとコールバックが頻繁に呼ばれてバッテリが消費されるので適切なMinimum値を設定するのは重要です。
(Minimum TimeとMinimum Distanceはあくまで上位レイヤーでのフィルタであり、実際のGPSデバイスは、Criteriaで設定された基準を満たすためにより頻繁に位置情報を取得しているので、Minimum TimeとMinimum Distanceを大きくしてもGPSデバイス自体の消費量を少なくすることはできません。)

今回はNike+のようなランニングアプリに必要な精度を出すためにMinimum TimeとMinimum Distanceを5秒&5メートルに設定しましたが、Uberのようなアプリだと10秒&10メートルくらいでもいいかもしれません。

それ以外にやることとしては、自分自信をlocationManager.addGpsStatusListener(this)によってGPSStatusListenrとして登録します、またrequestLocationUpdatesの第4パラメータに自分自身を渡すことによってLocationListnerとして登録します。

LocationListener

Location Providerという言葉がありますが、これはGPSかWifi Networkどちらが位置情報を供給しているのかという供給元の情報です。LocationListenerインターフェースを介してこのLocation Provider(位置情報の供給者)の情報を逐一取得することができます。

GPSStatus.Listener

GPSStatusListenerは下のような下位のGPSエンジンの情報を取得するためのインターフェースです。.

  • GPS_EVENT_STARTED
  • GPS_EVENT_STOPPED
  • GPS_EVENT_FIRST_FIX
  • GPS_EVENT_SATELLITE_STATUS

今回のサンプルコードでは、これらの情報は使用しませんが、衛星の取得状況などをモニタしたい場合はここに実装を加えることでできます。

override fun onGpsStatusChanged(event: Int) {}

LocationServiceを初期化してBindする

LocationServiceの準備ができましたので、次に、MainActivityからこのLocationServiceを使えるようにします。

MainActivityのonCreate()の中でstartLocationServiceWithPermissionCheck()という関数を呼んでいます。
この定義は、onCreate()のしたのstartLocationService()に記述されています(なぜ呼ぶときだけWithPermissionCheckという文字列がメソッド名につくのかは後述のPermission Checkの章で説明します)

startLocationService()のなかで、Intentを使ってLocationServiceのインスタンスを作り実行します。従来であればApplicationクラスのstartService()にこのIntentオブジェクトを渡せばLocationServiceがバックグラウンドで起動しました。
しかし、Android OS 8 (Oreo, API Level=26)からバックグランドでのサービスの実行に制限がかけられ、LocationServiceを継続して実行できなくなりました。そこで、Android OSがOreo以降の場合はstartForegroundSerivce()という関数を呼び、LocationServiceをForeground Serviceとして実行します。
Oreo以降のバックグラウンドサービスの制限については以下の公式ドキュメントに詳しく記述されています。

続いて、MainActivityの中でLocationServiceオブジェクトへの参照を取得するためにはServiceConnectionというクラスのオブジェクトを介して行わないといけないので、ServiceConnectionオブジェクトのインスタンスを生成し、このクラスの中で2つのメソッドをオーバーライドします。

このServiceConnectionオブジェクトと、Intentを引数にしてサービスを”bind”します。bindとは”くっつける”という意味で、文字通りActivityにServcieをくっつけて管理できるようにすることをbindServiceは行います。

ServiceConnectionの中でオーバーライドしたonServiceConnected() の中でLocationServiceのインスタンスを取得しlocationServiceメンバー変数に参照を格納します。これでMainActivityからlocationServiceのメソッドにアクセスできるようになります。

onServiceDisconnected() はLocationServiceが何らかの理由で消滅した時に呼ばれるので、locationServiceメンバー変数にNULLを代入しておきます。

これで、Activityの中からLocationServiceにアクセスできるようになりました。

Permission Check

さきほど、startLocationService()関数をstartLocationServiceWithPermissionCheck()というふうにMainActivityのonCreateのなかで呼んでいると説明しました。

これはPermissionDispatcherというライブラリ(下)をつかって、Android OS 6.0から必須となったランタイムパーミッションの取得をスマートに行うためにこのようなWithPermissionCheckというpostfixが使ったPermissionDispatcherの記法を使っているからです。

またstartLocationService()関数の定義の前に@NeedPermission(Manifest.permission.ACCESS_FINE_LOCATION)というアノテーションがついていると思いますが、これはstartLocationServiceACCESS_FINE_LOCATIONのパーミッションを必要とする処理を含んでいるためこの関数を呼び出したときにユーザーにパーミションの許可ダイアログを出すためのアノテーションです。

@NeedsPermission(Manifest.permission.ACCESS_FINE_LOCATION)    
fun startLocationService() {

もう1つRuntime PermissionをPermissionDispatcherを使って実現するために以下の関数をMainActivityでオーバーライドしています。

@SuppressLint("NeedOnRequestPermissionsResult")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated function
onRequestPermissionsResult
(requestCode, grantResults)
}

詳しくはPermissionDispatcherのページを覗いてみてください。

通常の実装方法よりもシンプルにRuntime Permissionの実装ができるのでおすすめです。

Run the app

ここまでできたら、Emulatorでアプリを走らせてみます。

シミュレーターのメニューバー(縦長でアイコンが並んでいるもの)の一番下の“…”をクリックするとExtended controlsというダイアログが開きます。ここでダミーの位置情報を送ることができます。

マップの適当なところをクリックしてください。すると、その場所を登録するためのSnackbarが下からでてきます。SAVE POINTをクリックして登録します。

途中このようなポップアップがでるので、OKをクリックします。

登録した場所を右のSaved pointsのリストから選択し、SET LOCATIONボタンを押します。

すると、onLocationChanged()が呼ばれ位置情報がAndroidStudioのログキャットに表示されると思います。

GPXとKMLファイルを取り込むこともできます。(マラソン大会のGPX、KMLファイルはネット上に落ちているので試してみてください。)

IMPORT GPX/KMKボタンからGPXファイルをロードします。次に、PLAY ROUTEボタンを押すとGPXの中に保存されているマラソンなどのルートを再現した位置情報の入力をアプリに行うことができます。

このチュートリアルのコードは下のRepositoryにあります。

masterブランチの最新のコードはこのチュートリアルのコードをすべて含んだ完成形になっています。

vol1_locationserviceというブランチにこの記事までの実装のみを含んだコードが置かれているので、

git checkout vol1_locationservice

で、vol1_locationserviceブランチに移動し、ここまで自分で実装してみてなにか動かないことがあれば自分のコードとこちらのコードとを比較してみてください。

次回は、マップにトラッキングした経路や自分の位置、さらには今現在の位置情報の精度を示す円などをマップ上に描画する方法を説明します。デバッグ目的でもマップへ位置情報をビジュアライズするとデバッグのヒントがたくさん得られるので位置情報取得のさらに深いテクニックを説明する前にサンプルアプリにマップ表示の機能を搭載したいと思います。

(第2回)走行データをGoogle Mapにマッピングする

このブログの内容に関する質問や位置情報トラッキング機能開発に関するご相談などはこちらにご連絡いただければと思います。
@mizutory
mizutori@goldrushcomputing.com

--

--

Taka Mizutori (JP)
位置情報を正確にトラッキングする技術

Goldrush Computing株式会社代表。iPhone/Androidアプリのプログラマー。守備範囲はSwift, Kotlin, Python, Js, Java, Obj-C, C#, C, Assembly。前職はSonyEricsson。3児のパパ。