AndroidアプリでGoogle Mapをライブラリとして使うGoogle Maps Android APIというのがGoogle Play servicesにあるのですが、こいつの setMyLocation()
まわりがここ1年でずいぶん変わりました。
Android 6.0 / Google Play services 8.4.0現在、これを正しく設定する方法を調べたので記録しておきます。
なおこれらのtipsは半径Nキロメートルという物件検討用メモアプリを作る際に調べたもので、このアプリにはもう適用済みです。
実装については、「半径N」では当初PermissionDispatcher を使おうとしたのですが、パーミッションチェックのコード量が減るわけではないので結局すべて自前で実装することにしました。
さて、まず問題は2つ。それぞれ見ていきます。
GoogleMap#setMyLocationEnabled()
のruntime permissions対応
GoogleMap#getMyLocation()
と GoogleMap#setOnMyLocationChangeListener()
がdeprecatedになった対応
GoogleMap#setMyLocationEnabled()
のruntime permissions対応
GoogleMap#setMyLocationEnabled()
はruntime permissionsを要求するようになりました。ACCESS_FINE_LOCATION
または ACCESS_COARSE_LOCATION
(以降は総称してlocation permissionsとします)が必要です。
なお GoogleMap自体はlocation permissionsがなくても動作する ので、まず本当にMyLocation(=デバイスの位置情報)が必要かどうかを検討してください。「半径N」の場合はMyLocationは必須ではないため、パーミッションの要求が拒否されても動作するようにしました。
これの対応の概要は以下のとおりです。
setMyLocationEnabled()
を呼び出す前にパーミッションのチェックと requestPermissions()
の呼び出しを行う
Activity#onRequestPermissionsResult()
で requestPermissions()
の結果を受け取って、GRANTEDであればもう一度 setMyLocationEnabled()
の呼び出しを行う
今回は shouldShowRequestPermissionRationale()
は使いませんでした。
requestPermissions()
runtime permissionsのフローは Android 6.0 の Runtime Permissions (M Permissions) に対応するためのアクティビティ図 - visible true がよくまとまっています。
requestPermissions()
は、パーミッションが必要なタイミングでコントローラが行います。パーミッションの必要なメソッドを呼ぶとIDEがlint errorを出し、IDEにしたがってコードテンプレートを生成すればだいたい合ってます。
今回は、以下のようなテンプレートが生成されます。
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
TODO
return;
}
map.setMyLocationEnabled(true);
コメントに書いてあるとおり、 requestPermissions()
を呼んで onRequestPermissionsResult()
で権限付与を処理しろとありますね。これにしたがうと以下のようなコードになります。
static final int RC_LOCATION_PERMISSIONS = 0x01;
static final String[] PERMISSIONS = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
};
void setMyLocationEnabled() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, PERMISSIONS, RC_LOCATION_PERMISSIONS);
return;
}
map.setMyLocationEnabled(true);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == RC_LOCATION_PERMISSIONS) {
onRequestLocationPermissionsResult(permissions, grantResults);
}
}
@DebugLog
void onRequestLocationPermissionsResult(String[] permissions, int[] grantResults) {
int[] granted2 = {PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED};
if (Arrays.equals(permissions, PERMISSIONS) && Arrays.equals(grantResults, granted2)) {
setMyLocationEnabled();
} else {
Toast.makeText(this, "No location permissions granted", Toast.LENGTH_LONG).show();
}
}
厄介なのは、パーミッションを要求するメソッドを呼び出すたびに checkSelfPermission()
しなければならないことです。そうしないと、lint errorになります。したがって、パーミッションを要求するメソッドはすべて checkSelfPermission()
を呼び出すコードでラップして、そのラッパーメソッドを呼び出すようにするのがいいでしょう。
GoogleMap#getMyLocation()
と GoogleMap#setOnMyLocationChangeListener()
がdeprecatedになった対応
runtime permissionsと直接は関係ないと思いますが、GoogleMapでMyLocationを取得するメソッドが非奨励になりました。かわりにGoogleApiClientから取得しなければいけません*1。GoogleMapはたとえ setMyLocationEnabled(true)
していても自動でMyLocationにカメラを移動したりはしないので、「カメラの初期位置をMyLocationにする」というだけのためにGoogleApiClientを使うことになります。
「半径N」の場合は位置情報まわりの操作をPlaceEngine
というクラスにまとめているので少しごちゃごちゃしていますが、シンプルに実装すると以下のようになるでしょう。 getLastLocation()
はlocation permissionsを要求するので、そのハンドルがまた必要です。「半径N」の場合はマップのカメラの初期位置を設定するだけなので、情報がとれなければそのまま諦めています*2。
void initiGoogleAPiClient() {
googleApiClient = new GoogleApiClient.Builder(context)
.addApi(LocationServices.API)
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(@Nullable Bundle bundle) {
handleLastLocation();
}
@Override
public void onConnectionSuspended(int i) {
Timber.i("GoogleApiClient connection suspended");
}
})
.addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
Timber.w("GoogleApiClient connection failed: %s", connectionResult.getErrorMessage());
}
})
.build();
}
private void handleLastLocation() {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
return;
}
Location currentLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
if (currentLocation != null) {
castMyLocation(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()), true);
}
}
さらなる工夫
ざっくり必要最低限の実装をしましたが、さらなる工夫もできます。
まず実サービスの場合、 shouldShowRequestPermissionRationale()
を使ってパーミッションが必要な理由を説明すべきです。
また、location permissionsの場合、高精度位置情報(ACCESS_FINE_LOCATION
)と低精度位置情報(ACCESS_COARSE_LOCATION
)の いずれか が必要なのですが、パーミッション取得の説明画面で高精度位置情報を落とすオプションを選ばせることも、技術的には可能だと思います。実装は大変ですが、サービスの性質次第では必要かもしれません。