Marshmallowになって、permissionの扱いが変わりました。
従来の、AndroidManifest.xmlにuses-permissionに記述してある権限をインストール時に一括で許諾するという考え方から、使用時に随時ユーザの許諾を得るようになっています(protectionLevelがnormalやsignatureなものは除く)。
dangerousなpermission
Manifest.permissionの記載をみると、個別に許諾を必要とするdangerousなpermissionは、以下の23種類。
- ACCESS_COARSE_LOCATION
- ACCESS_FINE_LOCATION
- ADD_VOICEMAIL
- BODY_SENSORS
- CALL_PHONE
- CAMERA
- PROCESS_OUTGOING_CALLS
- READ_CALENDAR
- READ_CALL_LOG
- READ_CONTACTS
- READ_EXTERNAL_STORAGE
- READ_PHONE_STATE
- READ_SMS
- RECEIVE_MMS
- RECEIVE_SMS
- RECEIVE_WAP_PUSH
- RECORD_AUDIO
- SEND_SMS
- USE_SIP
- WRITE_CALENDAR
- WRITE_CALL_LOG
- WRITE_CONTACTS
- WRITE_EXTERNAL_STORAGE
ちょっとしたユーティリティなんかでも使いそうなpermissionも含まれてます。
で、自分はWRITE_EXTERNAL_STORAGEで引っかかりました。SecurityExceptionで落ちます。
じゃ、どうしろと
結論から言って、
void 例えばWRITE_EXTERNAL_STORAGEを使う処理() {
// activityは、これを実行するアクティビティ
// REQUEST_WRITE_PERMISSIONは、8bit以下の整数定数
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED) {
// 以前に許諾して、今後表示しないとしていた場合は、ここにはこない
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// ユーザに許諾してもらうために、なんで必要なのかを説明する
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage("なんで、そのパーミッションが必要なのかを説明");
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String[] permissions = new String[] {
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
ActivityCompat.requestPermissions(activity, permissions, WRITE_PERMISSION);
});
builder.setNegativeButton("だめ", null);
builder.show();
} else {
// startActivityForResult()みたいな感じで許諾を要求
String[] permissions = new String[] {
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
ActivityCompat.requestPermissions(activity, permissions, WRITE_PERMISSION);
}
} else {
// 許諾されているので、やりたいことをする
}
}
こんな感じになると思います。えらいめんどいです。ContextCompatやActivityCompatは、Marshmallow以降のActivityにメソッドが含まれてるので、ターゲットをMarshmallowに絞るということならもっとすっきりしますが、実際には今の時点でそんなことはできませんね。
あまりにごちゃごちゃしているので、許諾に先立つ説明を省くと、こんな感じ。
void 例えばWRITE_EXTERNAL_STORAGEを使う処理() {
// activityは、これを実行するアクティビティ
// REQUEST_WRITE_PERMISSIONは、8bit以下の整数定数
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED) {
// 以前に許諾して、今後表示しないとしていた場合は、ここにはこない
String[] permissions = new String[] {
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
ActivityCompat.requestPermissions(activity, permissions, REQUEST_WRITE_PERMISSION);
} else {
// 許諾されているので、やりたいことをする
}
}
要するに、checkSelfPermission()で、許諾してもらう必要があるかを判別して、許諾されてなかったらrequestPermissions()で許諾を要求するという流れです。
requestPermissions()で許諾を要求する場合には、requestPermissions()に渡すactivityで、ActivityCompat.OnRequestPermissionsResultCallbackインターフェースのonRequestPermissionsResult()を実装して、その引数で許諾したかどうかを受け取ります。
public class MyActivity extends AppCompatActivity
implements ActivityCompat.OnRequestPermissionsResultCallback {
:
:
public void onRequestPermissionsResult(int requestCode,
@NotNull String[] permissions,
@NotNull int[] grantResults) {
if (requestCode == REQUEST_WRITE_PERMISSION) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 許諾されたので、やりたいことをやる
}
}
}
}
Marshmallow以前では、簡単に順次処理できていたものが、いきなり非同期になって、しかも複数箇所に分かれてしまいます。めんどいです。
しかも、requestPermissions()のソースをたぐればわかりますが、引数に渡すのは、ActivityCompat.OnRequestPermissionsResultCallbackをimplementsしたActivityで無ければなりません。コールバックだけ別クラスにすることができません。Fragmentで機能を実装してる場合どうすればいいのよってことです。
まとめ
permission modelの変更は、Marshmallowの発表時から言われていたことなので、ちゃんとしてなかった自分が悪いわけですが、凶悪なインパクトがあります。
ということで、自分も涙目で修正作業を始めることにします。(T-T)
(追記)ずるいこと思いついた
あくまでも急場しのぎということで。
void 例えばWRITE_EXTERNAL_STORAGEを使う処理() {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED) {
String[] permissions = new String[] {
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
ActivityCompat.requestPermissions(activity, permissions, REQUEST_WRITE_PERMISSION);
} else {
// 許諾されているので、やりたいことをする
}
}
これだけを実装します。コールバックも受け取りません。REQUEST_WRITE_PERMISSIONは使わないので、即値を指定してしまってもかまいません。
そうするとどうなるかというと、ユーザがこの操作をしようとしたときに、許諾を求めるダイアログが現れます。そして、許諾してもなにもおこりません。
ユーザは、「あれっ?」と思いますが、やりたいことができてないので、改めて同じ操作をします。
そのときは許諾されているので、本来の動作をします。
とりあえず、落ちなくはなるので、ちゃんとした対処をするまでの時間は稼げそうです。