Edited at

WRITE_EXTERNAL_STORAGEとかのdangerousなpermissionはAndroidManifest.xmlに書いてあってもSecurityExceptionではじかれるという話

More than 3 years have passed since last update.

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は使わないので、即値を指定してしまってもかまいません。

そうするとどうなるかというと、ユーザがこの操作をしようとしたときに、許諾を求めるダイアログが現れます。そして、許諾してもなにもおこりません。

ユーザは、「あれっ?」と思いますが、やりたいことができてないので、改めて同じ操作をします。

そのときは許諾されているので、本来の動作をします。

とりあえず、落ちなくはなるので、ちゃんとした対処をするまでの時間は稼げそうです。