セキュリティ保護が必要なデータや位置情報などの権限をリクエストする場合には、考慮すべきガイドラインが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 24 25 26 27 28 29 |
//④ 権限が既に付与されているか if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { // } //⑤a 権限の根拠を示す必要があるか else if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { val builder = AlertDialog.Builder(this) //⑤b 権限が必要な理由・メリットを説明 builder.setMessage(R.string.alert) // OK .setPositiveButton(R.string.ok ) { _: DialogInterface?, _: Int -> requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION ) } // NO .setNegativeButton(R.string.no_thanks) { _, _ -> toastMake(R.string.message1) } builder.create() builder.show() } else { //⑥ システム権限を要求する } |
ここの実装は各自にまかされているのですが、ここではAlertDialogを使いました
必要性とメリットをユーザーに示す
OKならば
requestPermissionLauncher.launch()
でシステムが権限のリクエストを再度要求する
No Thanksなら
システムは拒否されたとして再度リクエストをすることはない場合もあります
(このあたりの実装は実機によるかもしれません)
⑥ システムが権限を要求する
1 2 3 4 |
//⑥ システム権限を要求する requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION ) |
⑦ ユーザーが権限を許可したか
権限のリクエストに対して権限が付与されたかどうかで処理をします
権限が付与されなくてもアプリはその機能を使わないままで継続することが推奨されています
1 2 3 4 5 6 7 8 9 10 11 12 |
val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> //⑦ ユーザーが権限を許可したか if (isGranted) { //⑧a 制限された機能にアクセスする requestingLocationUpdates = true } else { //⑧b 制限された機能が無いままで継続 toastMake(R.string.message2) } } |
サンプルコード
以下は権限リクエストを考慮してFusedLocationProviderClientで位置情報を取得しGoogleMapで地図を表示させるサンプルです
MainActivity.kt
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 184 185 186 187 |
//package your.package.name import androidx.activity.result.contract.ActivityResultContracts 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.widget.Button import android.widget.TextView import android.widget.Toast import android.provider.Settings 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 class MainActivity : AppCompatActivity() { private lateinit var locationCallback: LocationCallback private lateinit var locationRequest: LocationRequest private lateinit var fusedLocationClient: FusedLocationProviderClient private var requestingLocationUpdates = false private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> //⑦ ユーザーが権限を許可したか if (isGranted) { //⑧a 制限された機能にアクセスする requestingLocationUpdates = true } else { //⑧b 制限された機能が無いままで継続 toastMake(R.string.message2) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val button = findViewById<Button>(R.id.button) //③ リクエストが必要になるまで待機"); //③ リクエストが必要になるまで待機"); button.setOnClickListener { //④ 権限が既に付与されているか if (ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED ) { //⑧a 制限された機能にアクセスする requestingLocationUpdates = true } else if (ActivityCompat.shouldShowRequestPermissionRationale( this, Manifest.permission.ACCESS_FINE_LOCATION ) ) { val builder: AlertDialog.Builder = AlertDialog.Builder(this) //⑤b 権限が必要な理由・メリットを説明 var negativeButton = builder.setMessage(R.string.alert_dialog) .setPositiveButton(R.string.ok) { _, _ -> requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION ) } .setNegativeButton(R.string.no_thanks) { _, _ -> toastMake(R.string.message1) } builder.create() builder.show() } else { //⑥ システム権限を要求する requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION ) } } locationRequest = LocationRequest.create().apply{ interval = 10000 fastestInterval = 5000 // priority = LocationRequest.PRIORITY_HIGH_ACCURACY priority = Priority.PRIORITY_HIGH_ACCURACY } locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { for (location in locationResult.locations) { val textView1 = findViewById<TextView>(R.id.text_view1) val textView2 = findViewById<TextView>(R.id.text_view2) // 緯度の表示 val str1 = " Latitude:" + location.latitude textView1.text = str1 // 経度の表示 val str2 = " Longitude:" + location.longitude textView2.text = str2 // Google Map moveToGMap(location) } } } fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) val button2: Button = findViewById(R.id.button2) button2.setOnClickListener { // アプリのSetting画面を開く val uriString = "package:$packageName" val intent = Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse(uriString) ) startActivity(intent) } } private fun toastMake(str: Int) { val toast = Toast.makeText(this, str, Toast.LENGTH_SHORT) toast.show() } private fun startLocationUpdates() { if (ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) != PackageManager.PERMISSION_GRANTED ) { return } fusedLocationClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() ) } @SuppressLint("QueryPermissionsNeeded") private fun moveToGMap(location: Location) { val str1 = java.lang.String.valueOf(location.latitude) val str2 = java.lang.String.valueOf(location.longitude) // geo:[lat,lng][?param[¶m]...], param:z=zoom val uri: Uri = Uri.parse("geo:$str1,$str2?z=14") val intent = Intent(Intent.ACTION_VIEW, uri) startActivity(intent) if (intent.resolveActivity(packageManager) != null) { startActivity(intent) } } private fun stopLocationUpdates() { fusedLocationClient.removeLocationUpdates(locationCallback) } override fun onResume() { super.onResume() if (requestingLocationUpdates) { startLocationUpdates() } } override fun 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 6 7 |
// アプリのSetting画面を開く val uriString = "package:$packageName" val intent = Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse(uriString) ) startActivity(intent) |
関連記事:
- FusedLocationProviderClient による位置情報の取得
- 複数の権限、LOCATIONとCAMERAをリクエスト
- FusedLocationProvider からGoogle Map地図の表示
- Kotlin でGPS位置情報を取得するアプリを作る
- アプリの権限リクエスト
References:
直近の位置情報を取得する – Android Developers
現在地の更新情報をリクエストする – Android デベロッパー
位置情報の設定を変更する – Android デベロッパー
FusedLocationProviderClient
アプリの権限をリクエストする