Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

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

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away