セキュリティ保護が必要なデータや位置情報などの権限をリクエストする場合には、考慮すべきガイドラインがGoogleから示されています。変更が多いので確認しておきます
2021.2.1
権限のリクエスト
Permission、権限のリクエストとして位置情報について当てはめてみます。
FusedLocationProvider とGoogle Mapで地図を表示
この続きです。
1. 権限リクエストのワークフロー
1.1 ① マニュフェストで権限を宣言
1.2 ④ ユーザーが権限を既に付与しているか
1.3 ⑤ 権限の根拠を示す必要があるか
1.4 ⑥ システムが権限を要求する
1.5 ⑦ ユーザーが権限を許可したか
2. サンプルコード
権限リクエストのワークフロー
にあるリクエストのワークフローは
大体このようになります。
- 以前はアプリの初期にまとめて権限リクエストをしていたこともありましたが、必要に応じてリクエストをします
- リクエストを拒否されてもそれでアプリをシャットダウンするのではなく、その権限無しのまま継続できるようにアプリのUX設計をする
① マニュフェストで権限を宣言
マニュフェストに以下のACESS設定と Google play services の設定が必要です
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
AndroidManifest.xml
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <application ... |
④ ユーザーが権限を既に付与しているか
ユーザーがすでに権限をアプリに付与しているか確認するには、
ContextCompat.checkSelfPermission()
を使います。
付与しているかいないか、いずれかが返ってきます
- 付与している:PERMISSION_GRANTED
- 付与していない:PERMISSION_DENIED
1 2 3 4 5 6 7 |
if (ContextCompat.checkSelfPermission( CONTEXT, Manifest.permission.REQUESTED_PERMISSION) == PackageManager.PERMISSION_GRANTED) { // PERMISSION_GRANTED } else { // PERMISSION_DENIED } |
⑤ 権限の根拠を示す必要があるか
この権限のリクエストが初めての場合はパスされる
ユーザーが以前に権限付与を拒否していた場合には、必要な理由やメリットを説明して理解を促すため
ActivityCompat.shouldShowRequestPermissionRationale()
を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { // } //⑤a 権限の根拠を示す必要があるか else if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { AlertDialog.Builder builder = new AlertDialog.Builder(this); //⑤b 権限が必要な理由・メリットを説明 builder.setMessage(R.string.alert_dialog) .setPositiveButton(R.string.ok, (dialog, id) -> requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION)) .setNegativeButton(R.string.no_thanks, (dialog, id) -> toastMake(R.string.message1)); builder.create(); builder.show(); } else { // } |
ここの実装は各自にまかされているのですが、ここではAlertDialogを使いました
必要性とメリットをユーザーに示す
OKならば
requestPermissionLauncher.launch()
でシステムが権限のリクエストを再度要求する
No Thanksなら
システムは拒否されたとして再度リクエストをすることはない場合もあります
(このあたりの実装は実機によるかもしれません)
⑥ システムが権限を要求する
1 2 3 |
//⑥ システム権限を要求する requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION); |
⑦ ユーザーが権限を許可したか
権限のリクエストに対して権限が付与されたかどうかで処理をします
権限が付与されなくてもアプリはその機能を使わないままで継続することが推奨されています
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> { //⑦ ユーザーが権限を許可したか if (isGranted) { //⑧a 制限された機能にアクセスする requestingLocationUpdates = true; } else { //⑧b 制限された機能が無いままで継続 toastMake(R.string.message2); } }); |
サンプルコード
以下は権限リクエストを考慮してFusedLocationProviderClientで位置情報を取得しGoogleMapで地図を表示させるサンプルです
MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
//package your.package.name; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import android.Manifest; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Location; import android.net.Uri; import android.os.Bundle; import android.os.Looper; import android.provider.Settings; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.location.FusedLocationProviderClient; import com.google.android.gms.location.LocationCallback; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationResult; import com.google.android.gms.location.LocationServices; import com.google.android.gms.location.Priority; public class MainActivity extends AppCompatActivity { private LocationCallback locationCallback; private LocationRequest locationRequest; private FusedLocationProviderClient fusedLocationClient; private boolean requestingLocationUpdates = false; private TextView textView1, textView2; private final ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> { //⑦ ユーザーが権限を許可したか if (isGranted) { //⑧a 制限された機能にアクセスする requestingLocationUpdates = true; } else { //⑧b 制限された機能が無いままで継続 toastMake(R.string.message2); } }); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.button); //③ リクエストが必要になるまで待機"); button.setOnClickListener( v -> { //④ 権限が既に付与されているか if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { //⑧a 制限された機能にアクセスする requestingLocationUpdates = true; } //⑤a 権限の根拠を示す必要があるか else if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { AlertDialog.Builder builder = new AlertDialog.Builder(this); //⑤b 権限が必要な理由・メリットを説明 builder.setMessage(R.string.alert_dialog) .setPositiveButton(R.string.ok, (dialog, id) -> requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION)) .setNegativeButton(R.string.no_thanks, (dialog, id) -> toastMake(R.string.message1)); builder.create(); builder.show(); } else { //⑥ システム権限を要求する requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION); } }); textView1 = findViewById(R.id.text_view1); textView2 = findViewById(R.id.text_view2); locationRequest = LocationRequest.create() .setFastestInterval(5000) .setInterval(10000) // LocationRequest.PRIORITY_HIGH_ACCURACY deprecated .setPriority(Priority.PRIORITY_HIGH_ACCURACY); locationCallback = new LocationCallback() { @Override public void onLocationResult(@NonNull LocationResult locationResult) { for (Location location : locationResult.getLocations()) { // 緯度の表示 String str1 = " Latitude:" + location.getLatitude(); textView1.setText(str1); // 経度の表示 String str2 = " Longitude:" + location.getLongitude(); textView2.setText(str2); // Google Map moveToGMap(location); } } }; fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); Button button2 = findViewById(R.id.button2); button2.setOnClickListener( v -> { // アプリのSetting画面を開く String uriString = "package:" + getPackageName(); Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse(uriString)); startActivity(intent); }); } private void toastMake(int str){ Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT); toast.show(); } private void startLocationUpdates() { if (ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()); } @SuppressLint("QueryPermissionsNeeded") private void moveToGMap(Location location){ String str1 = String.valueOf(location.getLatitude()); String str2 = String.valueOf(location.getLongitude()); // geo:[lat,lng][?param[¶m]...], param:z=zoom Uri uri = Uri.parse("geo:"+str1+","+str2+"?z=14"); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } } private void stopLocationUpdates() { fusedLocationClient.removeLocationUpdates(locationCallback); } @Override protected void onResume() { super.onResume(); if (requestingLocationUpdates) { startLocationUpdates(); } } @Override protected void onPause() { super.onPause(); stopLocationUpdates(); } } |
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button" android:text="@string/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="20dp" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.501" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.2" /> <TextView android:id="@+id/text_view1" android:text="@string/text1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="20dp" android:textColor="#44f" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.501" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.35" /> <TextView android:id="@+id/text_view2" android:text="@string/text2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20sp" android:textColor="#f44" android:layout_margin="20dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.4" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button2" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.7" /> </androidx.constraintlayout.widget.ConstraintLayout> |
strings.xml
1 2 3 4 5 6 7 8 9 10 11 12 |
<resources> <string name="app_name">YourAppName</string> <string name="button">"位置情報の取得"</string> <string name="text1">Latitude</string> <string name="text2">Longitude</string> <string name="alert_dialog">"アプリを継続するためには位置情報の取得が必要です"</string> <string name="ok">OK</string> <string name="no_thanks">No thanks</string> <string name="message1">"位置情報の取得が無いままアプリを継続します"</string> <string name="message2">"位置情報の取得が無いままアプリを継続します"</string> <string name="button2">"アプリ権限の設定"</string> </resources> |
AndroidManifest.xml
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <application ... |
build.gradle(Module…)
1 2 3 4 5 |
... dependencies { implementation 'com.google.android.gms:play-services-location:20.0.0' ... } |
アプリの設定に入れるためのボタンを追加しています
何度も拒否をしているとシステムが権限リクエストすら表示しなくなることがあるためです
もう一度この権限リクエストを取得したい場合は、アプリを再インストールするか
Setting画面でユーザーが許可を与えるしかありません
1 2 3 4 5 |
// アプリのSetting画面を開く String uriString = "package:" + getPackageName(); Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse(uriString)); startActivity(intent); |
関連記事:
- FusedLocationProviderClient による位置情報取得
- 複数の権限をリクエストする
- FusedLocationProvider とGoogle Mapで地図を表示
- GPSでの位置情報
- アプリの権限、位置情報をリクエストする実装
References:
直近の位置情報を取得する – Android Developers
現在地の更新情報をリクエストする – Android デベロッパー
位置情報の設定を変更する – Android デベロッパー
FusedLocationProviderClient
アプリの権限をリクエストする