Android
permission
Marshmallow
SecurityException

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

そうするとどうなるかというと、ユーザがこの操作をしようとしたときに、許諾を求めるダイアログが現れます。そして、許諾してもなにもおこりません。
ユーザは、「あれっ?」と思いますが、やりたいことができてないので、改めて同じ操作をします。
そのときは許諾されているので、本来の動作をします。

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