位置情報を正確にトラッキングする技術 in Android — (第1回) バックグランドでの位置情報トラッキングを可能にするアーキテクチャ
UberやNike+、Pokemon Goのようなアプリのコアとなる位置情報トラッキング技術について今まで5回に分けてiOSでの実現方法について解説して来ました。
https://medium.com/location-tracking-tech
今回からはAndroidで同じように精度の高い位置情報を取得する方法を解説してきながら、最終的にはNike+やMapMyRunを超える位置情報トラッキングエンジンを搭載したサンプルとしてGitHub上に作り上げていきますので最後までお付き合いください。
それでは、まずサンプルアプリの土台から作っていきましょう。
本ブログはJavaのコードで説明していますがこのたびKotlin版のコードを公開しました。こちらもご参考ください!
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を初期化してBindする
サービスは他のクラスは、初期化と初期化下サービスへの参照の取得方法が特殊なのでここで説明します。
- MainActivityのonCreate()の中でIntentを使ってLocationServiceのインスタンスを作り実行します。
- MainActivityの中でLocationServiceオブジェクトへの参照を取得するためにはServiceConnectionというクラスのオブジェクトを介して行わないといけないので、ServiceConnectionオブジェクトのインスタンスを生成し、このクラスの中で2つのメソッドをオーバーライドします。
- このServiceConnectionオブジェクトと、Intentを引数にしてサービスを”bind”します。bindとは”くっつける”という意味で、文字通りActivityにServcieをくっつけて管理できるようにすることをbindServiceは行います。
- ServiceConnectionの中でオーバーライドしたonServiceConnected() の中でLocationServiceのインスタンスを取得しlocationServiceメンバー変数に参照を格納します。これでMainActivityからlocationServiceのメソッドにアクセスできるようになります。
- onServiceDisconnected() はLocationServiceが何らかの理由で消滅した時に呼ばれるので、locationServiceメンバー変数にNULLを代入しておきます。
これで、Activityの中からLocationServiceにアクセスできるようになりました。
LocationManager
それではここからLocationServiceの中の実装を行って行きます。
まず、LocationServiceクラスの中にstartUpdatingLocation()というメソッドを実装します。
(前回までのiOSの方のサンプルとアーキテクチャやメソッド名などをある程度まで揃えています。)
startUpdatingLocation()の中でまずLocationManagerオブジェクトを初期化します。
LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
Criteria
次にLocationManagerが位置情報を取得する際のルールを細かく設定しなければいけなくこれがやや複雑です。大まかにいうと
- Criteriaを用意する
- Minimum Time (前回位置情報を取得してから最低どれだけ待って次の位置情報を取得するか)を設定する。
- Minimum Distance(前回位置情報を取得してから最低どれだけの距離をユーザーが移動したら次の位置情報を取得するか)を設定する。
- accuracyをACCURACY_FINEにする
- PowerRequirementを(to get best GPS signal)
- altitude(高度)は今回使わないので、altitudeRequiredをfalseにする
- locationManagerのスピード算出機能は今回使わないので、speedRequiredをfalseにする。
- costAllowedフラグをtrueにする。このフラグをオンにするとLocationManagerが基地局とデータのやり取りをして位置情報の制度を高めることになります。基地局とのデータのやり取りが発生するのでパケット量が増えユーザーが通信事業者に支払うデータ料金が増える可能性があるため”CostAllowed”という名前になっています。
Androidに搭載されているGPSはA-GPS(Assisted GPS)といって基地局の位置情報とそこからの距離でGPS衛星からの位置情報を補完するタイプのGPSのため、この機能をオンにするかしないかで位置情報の精度が大きく変わります。高精度の位置情報を取得するのであればオンにしなければなりませんが、パケット量に関してユーザーにフェアな設計にしたい場合は一度アラートを出してこの位置情報取得に基地局とのデータやり取りが発生してもいいか聞いてからこのフラグをオンにするというような実装をしてもいいかもしれません。 - bearingRequiredをfalseにします。. “Bearing”とは、デバイスが指し示している方向のことです。今回方向の情報は使わないのでfalseにします。
- horizontalAccuracyとverticalAccuracyをHIGHに設定します。
最後にLocationManagerのrequestLocationUpdatesを呼びます。この時Minimum Timeを1秒、Minimum Distanceを1メートルに指定します。また先ほど作った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を1秒&1メートルに設定しましたが、Uberのようなアプリだと3秒&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
今回のサンプルコードでは、これらの情報は使用しませんが、衛星の取得状況などをモニタしたい場合はここに実装を加えることでできます。
Run the app
ここまでできたら、Emulatorでアプリを走らせてみます。
シミュレーターのメニューバー(縦長でアイコンが並んでいるもの)の一番下の“…”をクリックするとExtended controlsというダイアログが開きます。ここでダミーの位置情報を送ることができます。ダミーの緯度、経度情報を入力するとonLocationChanged()が呼ばれ位置情報がログキャットに表示されると思います。
(同じ位置情報を続けて送ってもLogcatには最初の位置情報しか表示されません。これは、Minimum Distanceのフィルタが効いているからです。適当に緯度/経度の値を変えて送ってみましょう。)
GPXとKMLファイルを取り込むこともできます。マラソン大会のGPX、KMLファイルはネット上に落ちているので試しにここからロードして一番下の再生ボタンを押すとマラソンでの走りを再現することができます。(サンプルプロジェクトのトップディレクトリの下にHalf_Marathon.gpxという名前でGPXファイルが入っているので使ってみてください。)
ここまでのサンプルは以下のGitHubレポジトリのコミット
faa85d2f5cacd94f4d9ff005336680f3f9b27891で見ることができます。
https://github.com/mizutori/AndroidLocationStarterKit
次回は、マップにトラッキングした経路や自分の位置、さらには今現在の位置情報の精度を示す円などをマップ上に描画する方法を説明します。デバッグ目的でもマップへ位置情報をビジュアライズするとデバッグのヒントがたくさん得られるので位置情報取得のさらに深いテクニックを説明する前にサンプルアプリにマップ表示の機能を搭載したいと思います。
このブログの内容に関する質問や位置情報トラッキング機能開発に関するご相談などはこちらにご連絡ください。
@mizutory
mizutori@goldrushcomputing.com