LoginSignup
29
27

More than 5 years have passed since last update.

Android6.0以上の端末で動作するGoogleMapsアプリを作成ろうとしていきなりハマったことまとめ

Last updated at Posted at 2017-02-02

はじめに

Android6.0以上の端末で動作するGoogleMapを利用した簡単なアプリを作ろうと始めたところ、開始早々いきなりつまずきました。
そこで、同じようにハマった方がいた場合にその助けになればと思って色々と調査した内容をまとめます。
本記事のメインで説明するところは、Android6.0からサポートされたRuntime PermissionとGPSでの位置情報取得に関するAPIの基礎知識となります。
説明だけつらつら書くのがつらかったので、簡単なAndroidのサンプルアプリを作りながらその流れに沿って説明しようと思います。

作ろうとしているサンプルアプリの仕様は以下の通りです。

  1. Runtime Permissionを使用してパーミッション情報を取得します。(Android6.0以上)
  2. アプリ起動時、自身の位置を初期表示します。
  3. 画面右上の位置情報アイコンをタップすると、現在地に移動します。
  4. アプリを一度落として再度起動した時、前回の自分の位置を初期表示します。位置情報がとれた段階で現在地に移動します。

前提条件

以下の作業についてはこの記事では触れません。すでに多くの方がブログ等に書いてくださっているためそちらを参照ください。

  • 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.xmluses-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側、実際には許可ダイアログがユーザーの操作により決定されます。
アプリを実行したとき、よく以下のようなダイアログが出てくると思います。
これがパーミッションの許可ダイアログです。
(エミュレータでキャプチャしたので英語表記です。ご了承ください。)

allowDialog.png

実はこの許可ダイアログ、2回目以降はNever ask again(今後は確認しない)のチェックボックスが表示されます。
(理由は後述します。)

onceAllowDialog.png

許可ダイアログはアプリとは別の制御下で表示されているため非同期でアプリ側は処理を続行します。
そのため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条件に当てはまってしまうので、そこは事前にパーミッションチェックを行って「許可されていたらこの判定を行わない」という実装が必要になると思います。
これらを考えるとshouldShowRequestPermissionRationaleRuntime 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自体が管理しているため、例えばこのメソッドで引数の値を取得してアプリに権限を設定する・・といった操作は必要ありません。
このメソッドが呼ばれた時点で既にアプリへのパーミッション許可/拒否の設定は完了しています。

余談 エミュレータでのパーミッションリセット方法について

パーミッションの挙動を確認する際、エミュレータでのパーミッションリセット機能が必須でした。
私がやっていたリセット方法を載せておきます。

  1. エミュレータを起動した状態にしておく
  2. Android StudioのTerminalを開く
  3. インストールディレクトリのsdk/platform-toolsを開く (私の環境ではAndroid StudioをbrewでインストールしましたのでLibrary/Android/sdk/platform-toolsがパスでした。)
  4. 以下のコマンドを実行する。
./adb shell pm reset-permissions

パーミッションリセットは即反映されますのでエミュレータは再起動不要です。
(参考 http://stackoverflow.com/questions/34512880/how-to-debug-android-6-0-permissions)

4. GPSを使用して現在位置を初期表示する(2回目)

パーミッションの説明が完了したため、GPSを使用した位置情報の取得をする方法を説明します。
今回のアプリでやりたいことは以下の2つです。

  1. アプリ起動時に現在位置を初期表示する
  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について

removeUpdatesrequestLocationUpdatesで実行した位置情報更新リスナーを削除します。
requestLocationUpdatesremoveUpdatesはセットで考えたほうが分かりやすいです。
そもそもrequestLocationUpdatesrequestとありますが
「プロバイダーと更新間隔を指定してリスナーを位置情報サービスに登録する」
という表現の方が正しいと思います。

ここからは私の憶測になりますが、位置情報サービスとはAndroid OS層あたりの低レイヤーにもっていて、APIガイドのプラットフォームアーキテクチャの図をみるとJava API FrameworkManagersLocationとあるのでこれがLocationManagerを提供しており、さらに下層の部分(HALまでいくのでしょうか?)でGPSなりを使用して位置情報を取得するのではないかと思っています。
(すいません、あくまでこれは私の推察でちゃんと調べた訳ではありません・・)

一度requestLocationUpdatesに登録したリスナーはremoveUpdatesを行うまで保持し続けます。
試しに上記のonLocationChangedのサンプルコードからremoveUpdatesを削除して動かしたところ、requestLocationUpdatesの引数で更新間隔を0にしてますので、マップを少し移動させただけで瞬時に元の位置に戻る挙動となりました。
今回のアプリは起動時に1回だけ位置情報を取得すればよいためonLocationChangedremoveUpdatesを実行しています。

onLocationChanged がうまく呼ばれない

onLocationChangedだけでremoveUpdatesを実行すると初回アプリ起動時は意図した動作になってくれますが、戻るボタンでアプリを落として再度アプリを起動すると次の更新が一向にされません。この状態だとアフリカの南東部の海が初期位置になってしまいます。

エミュレータの場合、GPS設定画面で経度と緯度を自分で設定し、[SEND]ボタンをタップすることで、位置情報を端末に送信することができます。
以下の画像例では東京駅付近を指定しています。

編集済み.jpg

あまり調べていないのでひょっとしたら定期的に位置情報を送る手段があるかもしれませんが、私はこの手段でいちいち作業していました。
[SEND]ボタンをタップした瞬間にonLocationChangedが呼ばれます。
でもこれエミュレータはいいですが、実機はどうなるのでしょうか?
ということで実機で試しました。

Androidの位置情報を取得するタイミングは結構適当?

実機で検証した結果、やはりアフリカの海が初期位置になってしまいました。ただ、ある程度待つと現在位置に移動してくれました。
GPSの感度にもよると思うのですが、ググってみるとAndroidの位置情報取得タイミングは結構ムラがあるようです。
取得タイミングにムラがあるので当然onLocationChangedの呼び出しタイミングもムラがあることになります。

これとは別にremoveUpdatesを実行せずにアプリを終了してしまうと、次にアプリを起動してもしばらく位置の更新が行われずonLocationChangedが呼ばれないようです。
これは実機検証時の話ですが、今回はonMapReadyrequestLocationUpdatesを実行し、onLocationChangedremoveUpdatesを実行するようにしていました。
その結果「GPSが効かない?おかしい」とアプリを再起動しまくるとrequestLocationUpdatesだけが複数回実行されてしまい、一向にonLocationChangedが呼ばれずずっと初期位置の状態から微動だにしない現象が多発しました。

この状態は[設定]->[位置情報]を高精度に設定していましたが、[位置情報]をネットワークにしてアプリを起動すると onLocationChangedが呼ばれるようになりました。再び高精度に戻すとonLocationChangedが呼ばれなくなりました。
この結果からrequestLocationUpdatesのリスナーはプロバイダー毎に登録されるようです。
また、このとき実機のステータスバーに位置情報のアイコンがあることを確認しました。
どうやらrequestLocationUpdatesでリスナーが登録されるとこのアイコンが表示されるようです。
(このアイコンというのは以下画像のLTEの左側のアイコンのことをいってます。)

スクリーンショット 2017-01-30 23.33.24.png

アプリ停止時でもremoveUpdatesが確実に実行されるようonDestroyに書いて検証した結果、アプリ起動後数秒で毎回位置情報が更新できました。
本当はonPauseに書くのが望ましいかもしれませんので、作成するアプリとAndroidのライフサイクルを考慮して入れる場所を検討した方が良いかもしれません。

5. アプリ起動時の初期位置について

前回の位置を設定する

これまでの検証から、GPSの位置情報さえ取得できれば現在位置に移動してくれるところまでいけました。
ただ、アプリ起動時はどうしてもアフリカになってしまうのでなんとかしようと思います。

パッと思いつく方法は以下の2つかなと思います。

  1. 強引ですがとにかく初期位置を決めうちしてしまう方法
  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 のパーミッションチェックでエラーになる場合

requestLocationUpdatesremoveUpdatesを使用するといちいちパーミッションチェックをやれというエラーが発生します。

Call requires permission which may be rejected by user. Code should explicitly check to see if permission is available.

これを回避するには これらのメソッドを使う前にパーミッションチェックを行うかSecurityExceptionに対応する必要があります。

まとめ

今回、地図を使ったAndroidアプリを作成しようとしてRuntime PermissionrequestLocationUpdatesにかなり苦しめられました。
ここまでのコードはサンプルとして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

29
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
27