Android 6.0
RuntimePermissionの実装と注意点
2015/11/14
GDG Kobe
Android6.0 Marshmallow勉強会
self-introduction
twitter @KatsukiNakatani
Facebook Katsuki.Nakatani
お仕事
大阪のSIerでインフラ(サーバーとかルーターとか。。。)のエンジニアをしてます
個人でアプリ作ってます
Android Windows Store
中谷 克紀
名前
8
みなさんこの数字をご存知ですか?
そう!今年でAndroidは8歳です
Android6.0 Marshmallow
この8年間、StoreInstalledPermissionだったモデルが変革の時を迎えました。
新しい権限モデルRuntimePermissionです
今日はそのRuntimePermissionの実装と私自身はまったところなどを交えて
今後皆様が実装する際の手助けとなれば幸いです
RuntimePermissionとは?
Android 6.0(SDK 23)で実装された、実行時許可ベースの権限モデル
前段
Permissionって皆さんご存知ですよね?
<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.miruker.mpermission">



<uses-permission

android:name="android.permission.CALL_PHONE"

android:required="false" />



<application

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:supportsRtl="true"

android:theme="@style/AppTheme">

<activity

android:name=".MainActivity"

android:label="@string/app_name"

android:theme="@style/AppTheme.NoActionBar">

<intent-filter>

<action android:name="android.intent.action.MAIN" />



<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>



</manifest>
端末内の特定のセンサーやデータにアクセスするためにアプリケーションごとに
付与するもの
今までの使い方
宣言するのは下記のファイルに記載すればOK
AndroidManifest.xml
権限の付与について
ユーザーはGooglePlayからアプリのインストール時に権限を確認し、
インストール=そのアプリに該当の権限をすべて付与する
開発者 ・・・必要な権限をManifestに記載する
ユーザー・・・インストール時に確認するだけ
とてもシンプルで両者にとってWIN­WINな関係
だったはずですが。。。
ユーザー目線で見ると
2011年
ミログが公開したアプリでユーザの同意を得る前にサーバにデータを送信
2012年
全国電話帳というアプリで端末内電話帳データをサーバに送信
2013年
Simejiが変換文字列を自動的にサーバに送信
?でもシンプルなモデルだからわかりやすいってさっき話してたよね?
 なぜこんなことが起こるんだろう
ユーザーはインストール時にそこまでパーミッションをみてない
アプリによる情報流出の被害者が後を絶たない
開発者目線では?
全然バージョンアップされてない
理由・・・Permission追加時は自動更新されず、ユーザの許諾アクションが必要
     ユーザが許諾する際、追加の場合には特になぜ追加したか納得しないと更新しない傾向に有る
Manifestに記載するだけでOKやし、権限なかったらExceptionで落ちるからテストでもわかるし、シンプルで楽やん?
また、それら以外に、インストール時にパーミッションを求めることで、どういう機能でPermissionが
必要なのかがわかりにくい側面があり、インストールがためらわれる面もあった
RuntimePermission!
そこで登場したのがRuntimePermissionです
インストール・更新時(自動含む)にユーザにPermissionの許諾は求めません
実行時必要なときに必要な分だけ、Permissonを取得して実行します
とまた、その前に
RuntimePermissionの動作は、build.gradleに
指定するTargetSDKのバージョンで変わります
TargetSDK/権限 端末のOS 許諾の不要なパーミッション 許諾が必要なパーミッション
23
6.0
インストール時に付与 付与されない
<23 インストール時に付与 インストール時に付与
- 6.0未満 インストール時に付与 インストール時に付与
RuntimePermission対応を実施するためにはTargetSDKのバージョンを23にしましょう
許諾の必要なパーミッション
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
group:android.permission-group.CAMERA permission:android.permission.CAMERA
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS
permission:android.permission.USE_FINGERPRINT
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
group:android.permission-group.MICROPHONE permission:android.permission.RECORD_AUDIO
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
基本的にProtectionLevelがDANGEROUSなパーミッションです
※FingerPrintはNormalなので注意が必要です!
※Auto用がもう少しあるようですが、今回は省いています
独自パーミッションも対象となります
<permission android:name="com.test.permission"

android:label="testPermission"

android:description="@string/description"

android:protectionLevel="dangerous"

/>



<uses-permission android:name="com.test.permission"/>
dangerousのレベルを指定した場合に対象となります
RuntimePermissionで許可対象のまとめ
PermissionGroupに所属するパーミッションまたは、
所属しないがProtectionLevelがdangerousなパーミッションが対象
逆に言うと、それ以外のインターネットアクセスなどは
ユーザーに意識することなくアプリに権限を付与することが可能
これとても嬉しい!!
Google Playでの動作
TargetSDKが22未満 TargetSDKが23
インストール時にダイアログが表示 インストール時にダイアログが表示されない
[注意!]Android6.0端末&TargetSDK 23未満
TargetSDKが23未満のアプリでも、6.0では後から
設定画面でパーミッションを剥奪することが可能です
ただし、外すとPermission Denialで強制終了したり、空のデータが返って来たりします
警告はしてくれます
TargetSDK 23で実装しよう!
Permission Workflow
Cameraパーミッションが許可されているかチェック
CameraViewを表示
一度パーミッション要求を拒否しているかチェックYES
NO
パーミッション要求
YES
今後は確認しないがONになっている
例:ボタンをタップしたら、カメラViewが起動します
NO
YES
パーミッションが必要な理由を明示するOK
NG
エラーメッセージなどでCameraを起動しない
NO
アプリ設定画面に飛ばす
YES
NO
チェック
public void showCamera(View view) {

if (PermissionChecker.checkSelfPermission(context, Manifest.permission.CAMERA)

!= PermissionChecker.PERMISSION_GRANTED) {

//権限がない場合はパーミッションを要求するメソッドを呼び出し

requestCameraPermission();

} else {

//権限がある場合はそのまま処理を呼び出し

showCameraPreview();

}

}
AndroidManifest.xmlに必要なパーミッションを記載
<uses-permission android:name="android.permission.CAMERA"/>
PermissionCheckerのcheckSelfPermissionメソッドで、パーミッションがGranted(許可)かどうかを確認します
結果がGrantedの場合は、カメラを起動します
Permission Workflow
Cameraパーミッションが許可されているかチェック
CameraViewを表示
一度パーミッション要求を拒否しているかチェックYES
NO
パーミッション要求
YES
今後は確認しないがONになっている
例:ボタンをタップしたら、カメラViewが起動します
NO YES
パーミッションが必要な理由を明示するOK
NG
エラーメッセージなどでCameraを起動しない
NO
アプリ設定画面に飛ばす
YES
NO
要求
private void requestCameraPermission() {

if (ActivityCompat.shouldShowRequestPermissionRationale(context,

Manifest.permission.CAMERA)) {

Snackbar.make(mLayout, R.string.permission_camera_rationale,

Snackbar.LENGTH_INDEFINITE)

.setAction(R.string.ok, new View.OnClickListener() {

@Override

public void onClick(View view) {

ActivityCompat.requestPermissions(MainActivity.this,

new String[]{Manifest.permission.CAMERA},

REQUEST_CAMERA);

}

})

.show();

} else {

ActivityCompat.requestPermissions(context, new String[]{Manifest.permission.CAMERA},

REQUEST_CAMERA);

}

}
ActivityCompat.shuldShowRequestPermissionRationalメソッドで、
パーミッションが一度拒否され、説明が必要であるかのチェックをします
結果がTrue(一度拒否されている状態)で返って来た場合、このケースだとSnackbarで必要性を表示し、Action処理内で
ActivityCompat.RequestPermissionsで、パーミッションの要求を実行しています
一度も実行されたことがない場合は、ActivityCompat.RequestPermissionsでパーミッションの要求をします
Permission Workflow
Cameraパーミッションが許可されているかチェック
CameraViewを表示
一度パーミッション要求を拒否しているかチェックYES
NO
パーミッション要求
YES
今後は確認しないがONになっている
例:ボタンをタップしたら、カメラViewが起動します
NO YES
パーミッションが必要な理由を明示するOK
NG
エラーメッセージなどでCameraを起動しない
NO
アプリ設定画面に飛ばす
YES
NO
結果
@Override

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,

@NonNull int[] grantResults) {



if (requestCode == REQUEST_CAMERA) {

if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

Snackbar.make(mLayout, R.string.permision_available_camera,

Snackbar.LENGTH_SHORT).show();

} else {

if (ActivityCompat.shouldShowRequestPermissionRationale(this,

Manifest.permission.CAMERA)) {

//今後確認しないがCheckされていない

Snackbar.make(mLayout, R.string.permissions_not_granted,

Snackbar.LENGTH_SHORT).show();

} else {

//今後確認しないがCheckされているため、アプリケーション設定画面へ遷移する

Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);

Uri uri = Uri.fromParts("package", getPackageName(), null);

intent.setData(uri);

startActivity(intent);

}

}
} else {

super.onRequestPermissionsResult(requestCode, permissions, grantResults);

}

}
結果はint配列で帰ってきます(複数のPermissionを要求した場合、要求した順序で返却されます)
このケースだと、ひとつのチェックだけですが、必要に応じてループするなりしてチェックしましょう
(googleSampleのveryfyPermissionをUtilクラスなどで実装しておくと良いかと)
許可された場合、Snackbarで通知していますが、必要に応じて画面遷移するといいでしょう
onRequestPermissionsResult内で、ActivityCompat,shouldShowRequestPermissionRationaleを呼ぶと、
今後確認しないのCheckが入っていない場合Trueが返却され、入っている場合Falseが返却されるため、このメソッド内で
判断するようにしましょう
RuntimePermissionの端末内での管理
Permissionはグループ単位で管理されており、グループ単位で許諾します
例えば、この連絡先という項目には下記が含まれます
android.permission-group.CONTACTS
・android.permission.GET_ACCOUNTS
・android.permission.READ_CONTACTS
・android.permission.WRITE_CONTACTS
グループでは管理されますが、Manifestでの指定や
RuntimePermissionの指定は、Permission単位です!
RuntimePermissionの端末内での管理
RuntimePermissionはユーザー単位で管理されます(従来のPermissionは違います。あくまでRuntimePermissionのみ)
/data/system/users/{UserID}/runtime-permissions.xml
<pkg name="com.miruker.mpermission">
<item name="android.permission.READ_CALL_LOG" granted="false" flags="1" />
<item name="android.permission.CALL_PHONE" granted="false" flags="1" />
</pkg>
if(PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.READ_CALL_LOG) ==
PermissionChecker.PERMISSION_GRANTED) {

callPhone();

}else{

ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.READ_CALL_LOG}, CALL_PHONE_REQUEST_CODE);
}
<pkg name="com.miruker.mpermission">
<item name="android.permission.READ_CALL_LOG" granted="true" flags="0" />
<item name="android.permission.CALL_PHONE" granted="true" flags="0" />
</pkg>
同じパーミッショングループのため発信できてしまうけど、こういうのは良くないです
RuntimePermissionの実装のTips①
必要なタイミングで必要なパーミッションだけを要求しましょう。
アプリ全体で利用する本質的なパーミッションはアプリ起動時に要求し
オプションとして、利用する機能については、その機能を利用する際に要求するのが
ベター
Activityで、本質的なパーミッションをチェックして
何かのイベントボタンではそこで使うPermissionのチェックをするってことでOK
RuntimePermissionの実装のTips②
Activityの起動時だけや、実行前だけのチェックでは不十分
Permissionが必要なActivityやFragmentの
Resume等でPermissionはチェックしたほうがいい
例えばこんな動作
 1,アプリを起動する
 2,カメラボタンをタップ!(PermissionCheckして許可する)
 3,カメラViewを起動する
 4,タスク切り替えからパーミッションカメラを剥奪する
 5,タスク切り替えで戻ってくる
多分残念な結果になります
RuntimePermissionの実装のTips③
Permissionの要求は、ContextCompatのcheckSelfPermissionではなく
PermissionCheckerのcheckSelfPermissionを使いましょう
AppOpsで権限を剥奪した際の考慮もされているようです
public final class PermissionChecker {

/** Permission result: The permission is granted. */

public static final int PERMISSION_GRANTED = PackageManager.PERMISSION_GRANTED;



/** Permission result: The permission is denied. */

public static final int PERMISSION_DENIED = PackageManager.PERMISSION_DENIED;



/** Permission result: The permission is denied because the app op is not allowed. */

public static final int PERMISSION_DENIED_APP_OP = PackageManager.PERMISSION_DENIED - 1;



@IntDef({PERMISSION_GRANTED,

PERMISSION_DENIED,

PERMISSION_DENIED_APP_OP})

@Retention(RetentionPolicy.SOURCE)

public @interface PermissionResult {}



private PermissionChecker() {

/* do nothing */

}
PermissionChecker.java
RuntimePermissionの実装のTips④
public void onClick(View view) {

if(PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.READ_CALL_LOG) ==

PermissionChecker.PERMISSION_GRANTED) {

fetchCallLog();

}else{

ActivityCompat.requestPermissions(MainActivity.this,

new String[]{Manifest.permission.READ_CALL_LOG}, 

READ_CALL_REQUEST_CODE);

}

}
何気ないこのコード
Jelly Bean以上では動くけど、ICSでは一向に呼ばれない
要は常にGrantedではない状態
APIレベルで存在しないPermissionについては常にGrantedではない値が返ってくるので
利用するPermissionのAPILevelには気をつけましょう
その他注意事項や補足
RuntimePermissionの実装(TargetSDK23への変更)をストアへアップロードした後に
TargetSDK22のAPKへ戻すことは出来ません。
必ずリリース前にテストしましょう
PermissionCheckの実装がめんどくさい?そんな人は、下記ライブラリを見てみるといいかもしれません
https://github.com/tbruyelle/RxPermissions
RxPermissions
PermissionsDispatcher
https://github.com/hotchemi/PermissionsDispatcher
今流行の?RxJavaっぽくPermission処理をかけるライブラリです
アノテーションベースでPermission処理をかけるライブラリです
まとめ
自動更新されない問題が解消するのでぜひとも実装しましょう
インストール時のパーミッションダイアログでユーザーが嫌がってインストールしないことも減ります
ただし、TargetSDK23にしてしまったら戻せないので、必ずテストしましょう
おまけ
夢のMinSDK=23のアプリをリリースしました!
ただ単にバッテリー充電中に通知LEDで充電残量をお知らせてくれるだけのアプリです
良かったらダウンロードしてね
ご清聴ありがとうございました

Android6.0 RuntimePermissionの実装と注意点