はじめに
Android6.0以上の端末で動作するGoogleMapを利用した簡単なアプリを作ろうと始めたところ、開始早々いきなりつまずきました。
そこで、同じようにハマった方がいた場合にその助けになればと思って色々と調査した内容をまとめます。
本記事のメインで説明するところは、Android6.0からサポートされたRuntime Permission
とGPSでの位置情報取得に関するAPIの基礎知識となります。
説明だけつらつら書くのがつらかったので、簡単なAndroidのサンプルアプリを作りながらその流れに沿って説明しようと思います。
作ろうとしているサンプルアプリの仕様は以下の通りです。
-
Runtime Permission
を使用してパーミッション情報を取得します。(Android6.0以上) - アプリ起動時、自身の位置を初期表示します。
- 画面右上の位置情報アイコンをタップすると、現在地に移動します。
- アプリを一度落として再度起動した時、前回の自分の位置を初期表示します。位置情報がとれた段階で現在地に移動します。
前提条件
以下の作業についてはこの記事では触れません。すでに多くの方がブログ等に書いてくださっているためそちらを参照ください。
- Android Studioのインストールと基本的な使い方
- エミュレータの作成方法など
- 実機端末への接続やデバッグ方法など
-
Google Maps Android API
の取得方法
また、この記事はサンプルアプリの作成が目的ではなく、あくまで私がハマったRuntime Permission
とGPSからの位置情報取得方法を解説するものです。
そのため、パーミッションや使用する主要なAPIの説明がメインですのでご了承ください。
作業環境
- macOS Sierra(バージョン10.12.2)
- Android Studio 2.2.3
- TargetAPI : Android 6.0以上(APIレベル23)
- 仮想端末(エミュレータ): Nexus5 API23 Android6.0
- 実機端末 : Nexus5X API25 Android7.1.1
1. AndroidStudio でのプロジェクト作成
Android Studioにて、以下の設定で新規プロジェクトを作成します。
- Target Android Devices: Minimum SDK API23 Android6.0(Marshmallow)
- Add an Activity to Mobile: Google Maps Activity
プロジェクトの生成が完了したら、初期状態でgoogle_maps_api.xml
が表示されると思います。
表示されてない場合、画面左のProjectツリーにて、res/values/google_maps_api.xml
を開いてください。
表示形式をProjectにしている場合は src/debug/res/values/google_maps_api.xml
です。
xmlファイルのYOUR_KEY_HERE
に、取得したGoogle Maps Android API
を設定します。
(Google Maps Android API
の取得方法はたくさん情報がありますのでここでは記載しません。)
- 動作確認
Keyの設定が終わったら一度デバッグ実行します。設定が正しければエミュレータ上にGoogleMapが表示され、シドニーが初期位置にくるはずです。
MapsActivity.java
を読むと、sydney の経度と緯度が指定されていることが分かります。 ここで何も表示されない場合、Google Maps Android API
のプロジェクト設定でパッケージ名などAPIへのアクセス制限をかけている可能性があります。 私は1度プロジェクトがグチャグチャになって再作成した際、パッケージ名を変更したらGoogleMapが表示されなくなりしばらく悩みました。
2. GPSを使用して現在地を初期表示する(失敗)
私は最初「とりあえず適当なサイトを参考にGPSで位置情報を取得するコードを書けばいいかな」と思って始めましたが、これがそもそもの間違いでした。
まず、今回使用するのはGoogle Map Android API v2
であり、v1とは大幅に仕様が異なっているようです。
それ把握していない状態でググって適当にひっかかったサイトを参考にして失敗しました。
例えば、よく引っかかるMapController
はv2には存在しないようですが最初は分からず引用して「あれ?エラーじゃないか」となりました。
色々読んでるとどうやらパーミッションを理解しないと無理そうだなということが分かりましたのでまずはパーミッションの説明をしたいと思います。
v2のAPIがどうとかいう話は、パーミッションを理解した次のステップのものですので一旦置いておきます。
3. GPSを使用するためのパーミッションについて
今回費やした時間のほぼ全てはこのパーミッションの理解だと思います。
まず、位置情報を取得するためのパーミッションは以下の2種類があるようです。
ACCESS_FINE_LOCATION : GPSやWi-Fiなどのネットワークを使用して位置情報を取得
ACCESS_COARSE_LOCATION : Wi-Fiなどのネットワークのみを使用して位置情報を取得
他のサイトを読んでいると2つのパーミッションどっちも指定している例をよくみます。
が、どうやらこれはGoogle Map Android API v1
時代の設定方法のようです。
このGoogle Maps Android APIのガイドを読む限り、いずれか一方を指定すれば良いとあります。
また、Android StudioでGoogle Maps Activity
を指定して新規作成したプロジェクトでもACCESS_FINE_LOCATION
のみが設定されており、そのコメントを読むと「v2からは位置情報を2つ設定する必要はありませんよ。」とあります。
(パーミッションはAndroidManifest.xml
のuses-permission
で確認することができます。)
Runtime Permission を使用するための基本的なメソッドについて
Android 6.0以前は、アプリに必要なパーミッションをインストール時に全て付与していたそうです。
そう言われると、確かに以前のアプリはインストール時に必要なパーミッションが表示されていた記憶があります。
しかし、Android 6.0からはアプリ内の機能を実行した時、その機能を実行するために必要なパーミッションを付与することが出来るようになったそうです。
この仕組みを使えば、1つのアプリに複数の機能があった時に「カメラ機能しか使わない人にはカメラのパーミッションだけを許可」「マップ機能しか使わない人には位置情報のパーミッションだけを許可」といったことが出来るので、無用なパーミッションをアプリに許可しなくて済むようになります。
この機能がRuntime Permission
です。このパーミッションの理解に時間がかかりました。
以下のサイトがとても参考になりましたので、このサイトの解説をベースに話を進めていきます。
https://developer.android.com/training/permissions/requesting.html?hl=ja
システムパーミッションは大別するとNormal
パーミッションとDangerous
パーミッションの2種類に分けられるようです。
今回許可したいのは位置情報を取得するためのパーミッションであり、それはDangerous
パーミッションに分類されます。
上記サイトの解説で出てくるRuntime Permission
の基本的なメソッドは以下の4つとなります。
メソッド名 | 解説 |
---|---|
checkSelfPermission | 指定したパーミッションが許可されているか確認 |
shouldShowRequestPermissionRationale | 一言では言い表せませんでした・・ |
requestPermissions | アプリに必要なパーミッションを指定し、許可依頼を投げる |
onRequestPermissionsResult | パーミッションの許可/不許可の結果(実際にはユーザーの返答結果)を受け取る |
以下、各メソッドを私なりの理解で説明します。
checkSelfPermissionについて
引数に指定したパーミッションが許可されているかを判定します。
以下、コードの使用例です。
if (PermissionChecker.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "許可されています。", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "許可されていません。", Toast.LENGTH_SHORT).show();
}
PackageManager
クラスのPERMISSION_GRANTED
と値が一致すれば許可されています。拒否の定数はPERMISSION_DENIED
のようです。
余談ですが、PERMISSION_GRANTED
の実態はint
の0でありcheckSelfPermission
は今のところ0か-1でチェック結果が返ってくるようです。
内部の値が気になったので調べただけで、ここで言う話ではありませんが今後仕様が変更になる可能性あるためマジックナンバーではなくPackageManager
の定数を使用する方が良いですね。
checkSelfPermission の実装クラスが2つある問題
上記のサイトにはContextCompat
クラスのcheckSelfPermission
を使用していましたが、色々検索して調べてみるとどうやらcheckSelfPermission
を実行しているクラスは2つあるようです。
- ContextCompat
- PermissionChecker
PermissionChecker
クラスのcheckSelfPermission
を使用した方がいいという話がちらほら見受けられました。
この理由は、どうやらRuntime Permission
がサポートされているAPIレベル23(Android 6.0)以上の端末で、APIレベル23未満にする互換モードでContextCompat.checkSelfPermission
を使用した場合、ある条件下ではパーミッションのチェック判定が正しく行えないようです。
従って、パーミッションのチェックはPermissionChecker
クラスのcheckSelfPermission
を使用した方が良いという理解をしました。
shouldShowRequestPermissionRationale と requestPermissions について
この2つのメソッドは一緒に説明します。
特にshouldShowRequestPermissionRationale
は一番理解に時間がかかりました。
APIリファンレンスでのメソッド概要を読んでも、最初の1行目で「???」になりました。
Gets whether you should show UI with rationale for requesting a permission.
このメソッドの使用ケースを考えてみます。よくサンプルで出てくるコードは以下のような感じです。
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
// ユーザーへの説明を促す何か(ダイアログ等)を表示する処理を書く
}
// elseではなくshouldShowRequestPermissionRationaleがどうあれrequestPermissionsを実行する
ActivityCompat.requestPermissions(MapsActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
MY_LOCATION_REQUEST_CODE);
先にrequestPermissions
の説明をします。ActivityCompat.requestPermissions
は、下のレイヤーであるAndroid OSに
「指定したパーミッションを使いたいので許可をください。」
という依頼を行うメソッドのようです。
このメソッドを実行すると、Android OS側はユーザーにパーミッションの許可を求めるダイアログを表示します。
従って、許可するか拒否するかはAndroid OS側、実際には許可ダイアログがユーザーの操作により決定されます。
アプリを実行したとき、よく以下のようなダイアログが出てくると思います。
これがパーミッションの許可ダイアログです。
(エミュレータでキャプチャしたので英語表記です。ご了承ください。)
実はこの許可ダイアログ、2回目以降はNever ask again
(今後は確認しない)のチェックボックスが表示されます。
(理由は後述します。)
許可ダイアログはアプリとは別の制御下で表示されているため非同期でアプリ側は処理を続行します。
そのためrequestPermissions
を実行後、パーミッションが必要な処理を書いた場合、初回はパーミッションが無い状態で処理が進むことになります。
許可ダイアログの結果はコールバックメソッドであるonRequestPermissionsResult
で受け取ることができます。
このメソッドの説明は後述します。
ではshouldShowRequestPermissionRationale
はなんなのかという解説を私なりの理解でしようと思います。
まず戻り値boolean
の判定パターンをみてみます。
[trueになるパターン]
1度、許可ダイアログを出しており、かつ2度目以降「今後は確認しない」にチェックがつけられていない
[falseになるパターン]
1. 1度も許可ダイアログを出していない
2. 1度、許可ダイアログを出しており、かつ2度目以降「今後は確認しない」にチェックがつけられた
最初、この挙動の意味がわかりませんでした。
特にfalse
の1つ目のパターンは「最初の1回だけは無条件でスルーする」という一見不思議な挙動をしています。
以下にこの挙動を説明しているdeverloper androidのサイトの文章を引用します。
状況によっては、アプリにパーミッションが必要な理由をユーザーに説明するとよいでしょう。
たとえば、ユーザーがカメラアプリを起動した場合、ユーザーは、カメラを使用するためのパーミッションが求められても驚くことはないでしょう。
しかし、アプリがユーザーの位置情報や連絡先へのアクセスを求める理由は理解できないかもしれません。
パーミッションをリクエストする前に、ユーザーに理由を説明することを検討してください。
ただし、説明を表示することでユーザーに負担を与えないように注意してください。
説明が多すぎると、ユーザーはストレスを感じ、アプリを削除してしまう可能性があります。
この問題の対処法の 1 つは、ユーザーがパーミッション リクエストを既に拒否している場合にのみ説明を表示することです。
ユーザーがパーミッションを必要とする機能を使おうとしながら、パーミッション リクエストを拒否し続けている場合、ユーザーは、その機能を利用するにはアプリにパーミッションが必要であることを理解していない可能性があります。
このような場合は、ユーザーに対して説明を表示するとよいでしょう。
https://developer.android.com/training/permissions/requesting.html?hl=ja
「アプリにパーミッションが必要な理由の説明」より引用
重要な部分は この問題の対処法の1つは〜 のところで
「1度パーミッションを拒否したユーザーに対し、パーミッションの必要性を説明するのが良いですよ」
といっています。
この引用部分をコードに落とすと、checkSelfPermission
の結果がPERMISSION_DENIED
であり、かつshouldShowRequestPermissionRationale
の結果がtrue
の場合は何らかの説明をしましょう、という話になるかと思います。
また、記載はありませんがユーザーが理解した上で「今後は確認しない」をチェックした場合、それ以上追求すべきではないと考えます。(操作を誤った場合は別ですが・・)
従って、以下の通り全パターンの説明がつくはずです。
[trueになるパターン]
1度、許可ダイアログを出しており、かつ2度目以降「今後は確認しない」にチェックがつけられていない
→許可ダイアログを表示されたユーザーは、内容を理解していないかまたは迷っているのでtrue
trueの場合はパーミッションの必要性を説明する。
[falseになるパターン]
1. 1度も許可ダイアログを出していない
→最初の1回目はとりあえずユーザーの意思を確認したいので問答無用でfalse(後続処理でダイアログを出ること前提)
2. 1度、許可ダイアログを出しており、かつ2度目以降「今後は確認しない」にチェックがつけられた
→許可ダイアログを2回以上表示されたユーザーは、内容を理解した上で「今後は確認しない」にチェックを付けたのでfalse
ただ、これだけだと「許可した場合」でもtrue
条件に当てはまってしまうので、そこは事前にパーミッションチェックを行って「許可されていたらこの判定を行わない」という実装が必要になると思います。
これらを考えるとshouldShowRequestPermissionRationale
はRuntime Permission
を提供するうえで、パーミッション許可の動作についてある程度の統一化を図るために必要なメソッドなのかなと理解しました。
これなら、初回表示の許可ダイアログに「今後は確認しない」のチェックがない理由もわかる気がします。
onRequestPermissionsResultについて
説明を後回しにしたメソッドで、許可ダイアログの結果を受け取ることができるコールバックメソッドとなります。
コードサンプルは以下の通りです。
@Override
/**
* このコールバックメソッドはActivityCompat.OnRequestPermissionsResultCallbackをimplementsする必要があります。
*
* @param requestCode requestPermissionsメソッドで指定したコード値です。コード値は自分で定義したものです。
* @param permissions[] 許可依頼を出したパーミッションが入っています。
* 一度に複数のパーミッションの許可を依頼した場合、その分String配列に入っています。
* @param grantResults 許可依頼を出したパーミッションが 許可された/拒否されたか が入っています。
*/
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
if (requestCode == MY_LOCATION_REQUEST_CODE) {
if (permissions[0].equals(Manifest.permission.ACCESS_FINE_LOCATION) && grantResults[0]
== PackageManager.PERMISSION_GRANTED) {
// 許可された
} else {
// 拒否された
}
}
}
サンプルに示した通り、引数に許可依頼を出したパーミッションとその結果(許可/拒否)が入っています。
ただ、どのアプリにどのパーミッションを許可/拒否しているかという設定はAndroid OS自体が管理しているため、例えばこのメソッドで引数の値を取得してアプリに権限を設定する・・といった操作は必要ありません。
このメソッドが呼ばれた時点で既にアプリへのパーミッション許可/拒否の設定は完了しています。
余談 エミュレータでのパーミッションリセット方法について
パーミッションの挙動を確認する際、エミュレータでのパーミッションリセット機能が必須でした。
私がやっていたリセット方法を載せておきます。
- エミュレータを起動した状態にしておく
- Android StudioのTerminalを開く
- インストールディレクトリの
sdk/platform-tools
を開く (私の環境ではAndroid StudioをbrewでインストールしましたのでLibrary/Android/sdk/platform-tools
がパスでした。) - 以下のコマンドを実行する。
./adb shell pm reset-permissions
パーミッションリセットは即反映されますのでエミュレータは再起動不要です。
(参考 http://stackoverflow.com/questions/34512880/how-to-debug-android-6-0-permissions)
4. GPSを使用して現在位置を初期表示する(2回目)
パーミッションの説明が完了したため、GPSを使用した位置情報の取得をする方法を説明します。
今回のアプリでやりたいことは以下の2つです。
- アプリ起動時に現在位置を初期表示する
- 現在位置アイコンを表示する
現在位置アイコンを表示する
こっちのほうが簡単ですので先に説明します。
今回はGoogle Maps API v2
を使用するため以下のコード1行で完了します。
位置情報を取得するためのパーミッションが必要ですのでご注意ください。
// mMapは private GoogleMap mMap; で宣言しています。
mMap.setMyLocationEnabled(true)
このsetMyLocationEnabled
で画面右上にアイコンが表示されると思います。
プロバイダー取得から位置情報の更新依頼まで行う
アプリ起動時に現在位置を初期表示する方法を説明します。
コードを元に説明した方が早いので、必要なところのみを抜粋して示します。
private LocationManager myLocationManager;
// ~この間にも処理がありますが、関係ないので省略します~
public void onMapReady(GoogleMap googleMap) {
// この間も省略
myLocationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
Criteria criteria = new Criteria();
String provider = myLocationManager.getBestProvider(criteria, true);
myLocationManager.requestLocationUpdates(provider, 0, 0, this);
// ここから先も省略
}
はじめにLocationManager
を取得します。
2018/10/17 追記
2018年10月現在(本当はかなり前から)LocationManager
の使用は推奨されていないことを今更知りました・・・
代わりにGoogle Location Services API
を使用するよう促しています。
参考: https://developer.android.com/reference/android/location/package-summary
myLocationManager
が位置情報の管理クラスになり、このクラスで色々と操作を行います。
位置情報を取得する際、色々な情報・・例えば消費電力、現在いる場所のGPS精度、ネットワークの強度などによって手段が変動します。
それらの条件を指定できるのがCriteria
クラスで使い方は以下のような感じになります。
Criteria criteria = new Criteria();
// 位置情報を高精度にする
crite.setAccuracy(Criteria.ACCURACY_FINE);
// 消費電力は中くらい
crite.setPowerRequirement(Criteria.POWER_MEDIUM);
ただ、このような指定をしてしまうとgetBestProvider
でのプロバイダー取得時に高精度位置情報に依存してしまうのでよくないという記事を目にしました。
確かに電波が入らないような屋内でWi-Fiは通っているような環境の場合、この指定は障壁にしかならないのかなと思います。
従って、何も設定せずLocationManager
に任せた方がいいとの結論に達しましたので、criteria
は使いますが細かい指定はしません。
端末の位置情報モードに応じて取得できるプロバイダーの種類
どんなプロバイダーが取得できるのか、ググれば色々出てくるのですがせっかくなので実機検証してみました。
Android端末の[設定]->[位置情報]で指定できる位置情報モードに応じた取得プロバイダーは以下の通りです。
(実機はNexus5XのAndroid7.1.1です。)
位置情報モードの設定 | getBestProviderの結果 |
---|---|
高精度 | gps |
バッテリー節約 | network |
端末のみ | gps |
位置情報OFF | passive |
なんか高精度と端末のみが同じgpsなのが納得いきませんが・・・
プロバイダーはプログラム上でString型なので、同じgpsでもAndroid OSに渡したときに高精度で位置情報を返してくれるのだと勝手に解釈しました。
位置情報の更新依頼を行う
話を戻してrequestLocationUpdates
について説明します。
このメソッドの定義は以下の通りです。
/**
* パラメータの説明をします。
* @param provider 上記で説明したプロバイダー名を指定します。
* @param minTime 位置情報の更新間隔をミリ秒で指定します。
* @param minDistance 位置情報の更新距離をメートルで指定します。
* @param listener LocationListenerを指定します。
*/
requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener)
今回はmyLocationManager.requestLocationUpdates(provider, 0, 0, this)
と指定しています。
更新間隔が0ms
ですので、後から出てくるremoveUpdates
をしないと常に初期位置に引き戻されます。
この話はコールバックメソッドremoveUpdates
のところでまた説明します。
位置情報が更新されたことを検知するには
requestLocationUpdates
で位置情報の更新依頼を行なっただけでは何も起こりません。位置情報が更新されたことをアプリ側が検知してMap上に設定してやる必要があります。
その更新結果を取得するためのコールバックメソッドがonLocationChanged
です。
以下、サンプルコードです。
//このコールバックメソッドはLocationListenerをimplementsする必要があります。
@Override
public void onLocationChanged(Location location) {
LatLng myLocation = new LatLng(location.getLatitude(), location.getLongitude());
mMap.addMarker(new MarkerOptions().position(myLocation).title("now Location"));
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(myLocation, 18));
try {
myLocationManager.removeUpdates(this);
} catch(SecurityException e) {
// エラー処理
}
}
GPSなりネットワークなりから位置情報を取得したAndroid端末は、アプリに引数location
で返してきます。
このLocation
からLatLng
クラスを生成し、MapsActivity.javaのonMapReady
メソッドに書かれていた方法で位置情報を設定します。
なお、newLatLngZoom
を使用することで最初からズームして表示することができます。
removeUpdatesについて
removeUpdates
はrequestLocationUpdates
で実行した位置情報更新リスナーを削除します。
requestLocationUpdates
とremoveUpdates
はセットで考えたほうが分かりやすいです。
そもそもrequestLocationUpdates
はrequest
とありますが
「プロバイダーと更新間隔を指定してリスナーを位置情報サービスに登録する」
という表現の方が正しいと思います。
ここからは私の憶測になりますが、位置情報サービスとはAndroid OS層あたりの低レイヤーにもっていて、APIガイドのプラットフォームアーキテクチャの図をみるとJava API Framework
のManagers
にLocation
とあるのでこれがLocationManager
を提供しており、さらに下層の部分(HALまでいくのでしょうか?)でGPSなりを使用して位置情報を取得するのではないかと思っています。
(すいません、あくまでこれは私の推察でちゃんと調べた訳ではありません・・)
一度requestLocationUpdates
に登録したリスナーはremoveUpdates
を行うまで保持し続けます。
試しに上記のonLocationChanged
のサンプルコードからremoveUpdates
を削除して動かしたところ、requestLocationUpdates
の引数で更新間隔を0にしてますので、マップを少し移動させただけで瞬時に元の位置に戻る挙動となりました。
今回のアプリは起動時に1回だけ位置情報を取得すればよいためonLocationChanged
でremoveUpdates
を実行しています。
onLocationChanged がうまく呼ばれない
onLocationChanged
だけでremoveUpdates
を実行すると初回アプリ起動時は意図した動作になってくれますが、戻るボタンでアプリを落として再度アプリを起動すると次の更新が一向にされません。この状態だとアフリカの南東部の海が初期位置になってしまいます。
エミュレータの場合、GPS設定画面で経度と緯度を自分で設定し、[SEND]ボタンをタップすることで、位置情報を端末に送信することができます。
以下の画像例では東京駅付近を指定しています。
あまり調べていないのでひょっとしたら定期的に位置情報を送る手段があるかもしれませんが、私はこの手段でいちいち作業していました。
[SEND]ボタンをタップした瞬間にonLocationChanged
が呼ばれます。
でもこれエミュレータはいいですが、実機はどうなるのでしょうか?
ということで実機で試しました。
Androidの位置情報を取得するタイミングは結構適当?
実機で検証した結果、やはりアフリカの海が初期位置になってしまいました。ただ、ある程度待つと現在位置に移動してくれました。
GPSの感度にもよると思うのですが、ググってみるとAndroid
の位置情報取得タイミングは結構ムラがあるようです。
取得タイミングにムラがあるので当然onLocationChanged
の呼び出しタイミングもムラがあることになります。
これとは別にremoveUpdates
を実行せずにアプリを終了してしまうと、次にアプリを起動してもしばらく位置の更新が行われずonLocationChanged
が呼ばれないようです。
これは実機検証時の話ですが、今回はonMapReady
でrequestLocationUpdates
を実行し、onLocationChanged
でremoveUpdates
を実行するようにしていました。
その結果「GPSが効かない?おかしい」とアプリを再起動しまくるとrequestLocationUpdates
だけが複数回実行されてしまい、一向にonLocationChanged
が呼ばれずずっと初期位置の状態から微動だにしない現象が多発しました。
この状態は[設定]->[位置情報]を高精度に設定していましたが、[位置情報]をネットワークにしてアプリを起動すると onLocationChanged
が呼ばれるようになりました。再び高精度に戻すとonLocationChanged
が呼ばれなくなりました。
この結果からrequestLocationUpdates
のリスナーはプロバイダー毎に登録されるようです。
また、このとき実機のステータスバーに位置情報のアイコンがあることを確認しました。
どうやらrequestLocationUpdates
でリスナーが登録されるとこのアイコンが表示されるようです。
(このアイコンというのは以下画像のLTE
の左側のアイコンのことをいってます。)
アプリ停止時でもremoveUpdates
が確実に実行されるようonDestroy
に書いて検証した結果、アプリ起動後数秒で毎回位置情報が更新できました。
本当はonPause
に書くのが望ましいかもしれませんので、作成するアプリとAndroid
のライフサイクルを考慮して入れる場所を検討した方が良いかもしれません。
5. アプリ起動時の初期位置について
前回の位置を設定する
これまでの検証から、GPSの位置情報さえ取得できれば現在位置に移動してくれるところまでいけました。
ただ、アプリ起動時はどうしてもアフリカになってしまうのでなんとかしようと思います。
パッと思いつく方法は以下の2つかなと思います。
- 強引ですがとにかく初期位置を決めうちしてしまう方法
- 最後に取得した位置を最初に表示する方法
位置を取得するまでマップを表示しない、という方法があればそれも視野に入れたかったのですが少し調べた限りではなさそうです。
2つ目の方法は「少なくとも1回はアプリを起動して位置情報を取得している」という前提条件が付きますので「じゃあ初回起動の1回目はどうするんだ」という問題があります。
従って今回は2つの方法どちらも採用しました。以下、サンプルコードです。
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
// ここで位置情報のパーミッションチェックを行ってどうするか決めます。
if (PermissionChecker.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
myLocationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
// プロバイダーに応じて最後に取得した位置情報があればそれを設定します。
// setLocationはprivateメソッドです。詳細はGithubに上がってるコードを確認ください。
String provider = getProvider();
Location lastLocation = myLocationManager.getLastKnownLocation(provider);
if(lastLocation != null) {
setLocation(lastLocation);
}
mMap.setMyLocationEnabled(true);
myLocationManager.requestLocationUpdates(provider, 0, 0, this);
} else {
// 最初の位置を決め打ちします。ここでは東京駅にしてます。
LatLng tokyo = new LatLng(35.681298, 139.766247);
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(tokyo, 18));
// 初回起動時やパーミッションが許可されていない場合はこっちのパーミッション確認ルートになります。
// confirmPermissionはprivateメソッドです。詳細はGithubに上がってるコードを確認ください。
confirmPermission();
}
}
requestLocationUpdates や removeUpdates のパーミッションチェックでエラーになる場合
requestLocationUpdates
やremoveUpdates
を使用するといちいちパーミッションチェックをやれというエラーが発生します。
Call requires permission which may be rejected by user. Code should explicitly check to see if permission is available.
これを回避するには これらのメソッドを使う前にパーミッションチェックを行うかSecurityException
に対応する必要があります。
まとめ
今回、地図を使ったAndroidアプリを作成しようとしてRuntime Permission
とrequestLocationUpdates
にかなり苦しめられました。
ここまでのコードはサンプルとしてGithubに上げておきます。
検証用コードですので1つのActivityで全部完結させています。本当はListenerクラスは別に分けるべきだと思うので、そこはご了承ください。
この検証結果もあまり納得いってなくて、実機で動かすと自分の位置(マップ上の青い丸)がかなりブレます・・
今回使用した位置情報は主にAndroid Location API
を使いましたがGoogle Location Services API
のほうがもっと高精度なのでしょうか。
stack overflowにあった以下のトピックを流し読みした限りでは、微妙そうな感じですが・・
機会があれば試そうかと思います。
2018/10/17 追記:
微妙そうな感じとか意味不明なこと書きましたが、android.locationのリファレンスにGoogle’s Location Services API
を強く推奨すると注意喚起があるのでandroid.locationは使わない方が良さそうです・・・
http://stackoverflow.com/questions/33022662/android-locationmanager-vs-google-play-services
2018/10/20 追記:
Google Location Services API
を試した記事を書きましたのでよければ位置情報の参考にしてください。
https://qiita.com/hotdrop_77/items/c8098bba9542a7898faf