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


Marshmallowになって、permissionの扱いが変わりました。
従来の、AndroidManifest.xmlにuses-permissionに記述してある権限をインストール時に一括で許諾するという考え方から、使用時に随時ユーザの許諾を得るようになっています（[protectionLevel](http://developer.android.com/reference/android/R.attr.html#protectionLevel)がnormalやsignatureなものは除く）。

## dangerousなpermission

[Manifest.permission](http://developer.android.com/reference/android/Manifest.permission.html)の記載をみると、個別に許諾を必要とする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で落ちます。

## じゃ、どうしろと

結論から言って、

```java

    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に絞るということならもっとすっきりしますが、実際には今の時点でそんなことはできませんね。

あまりにごちゃごちゃしているので、許諾に先立つ説明を省くと、こんな感じ。

```java

    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()を実装して、その引数で許諾したかどうかを受け取ります。

```java

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)

## （追記）ずるいこと思いついた

あくまでも急場しのぎということで。

```java

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

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

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