techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

Android 7.0 Nougatの新しい外部ストレージアクセスを試してみた。

Android 7.0 Nougatでは新しい外部ストレージへのアクセス方法が提供されました。

developer.android.com

developer.android.com

これまでAndroidで外部ストレージにアクセスするには、ManifestファイルにPermissionを定義し、さらに6.0からはパーミッションリクエストも必要です。
また、WRITE_EXTERNAL_STORAGEはアプリとして必要でないディレクトへの読み書きも許可してしまいます。

新しい外部ストレージアクセスのAPIを利用することで、利用したいストレージへのアクセスを簡単に行うことができます。

ユーザにアクセス許可を得る

今回は外部ストレージのDCIMディレクトリ配下にファイルの読み書きをしてみることにします。
f:id:uentseit:20160827015834p:plain:w200

先のリンクのサンプルにもありますが、DCIMディレクトリにアクセスするには以下のようにします。

    @TargetApi(24)
    private void startStorageAccessIntent(int requestCode){
        sm = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
        StorageVolume volume = sm.getPrimaryStorageVolume();

        Intent intent = volume.createAccessIntent(Environment.DIRECTORY_DCIM);
        startActivityForResult(intent, requestCode);
    }

ポイントは4行目以降です。
StorageManager#getPrimaryStorageVolume()はAndroid端末のプライマリストレージボリュームの情報(StorageVolumeクラスのインスタンス)を取得します。取得されるボリュームは、これまで利用してきたgetExternalStorageDirectory()メソッド、 getExternalFilesDir(String)メソッドと同じです。
プライマリストレージはあらかじめシステムに決められており、必ずしもSDカードになるわけではないことに注意です。もし別のストレージにアクセスしたいような場合には、StorageManager#getStorageVolume()メソッドを使用します。
StorageVolumeクラスにはストレージの情報が入っており、端末にマウントされているかどうかといった情報も持っています。6行目ではcreateAccessIntent()メソッドを利用し、ディレクトリへのアクセス許可をユーザに確認するダイアログを表示するためのIntentを取得します。引数にアクセスしたいディレクトリを指定することで、そのディレクトへのアクセスのみを許可します。
7行目でIntentを実行すると、以下のようなダイアログが表示されます。
f:id:uentseit:20160827022851p:plain:w200
ユーザがこのダイアログで許可、あるいは拒否を選ぶと、onActivityResultが呼ばれます。

ファイルを書き込む

onActivityResultの内容は以下のような感じにしてみました。

    @SuppressLint("NewApi")
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_CANCELED) return;

        Uri uri = data.getData();
        getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        DocumentFile pickedDir = DocumentFile.fromTreeUri(this, uri);

        switch (requestCode) {
            case (REQUEST_CODE_WRITE):
                DocumentFile newFile = pickedDir.createFile("text/plain", OUTPUT_FILE_NAME);
                try {
                    OutputStream out = getContentResolver().openOutputStream(newFile.getUri());
                    out.write("My name is seit.".getBytes());
                    out.close();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            case (REQUEST_CODE_READ):    // 以下読み込み
                try {
                    int DEFAULT_BUFFER_SIZE = 1024 * 4;
                    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
                    pickedDir.findFile(OUTPUT_FILE_NAME);
                    for (DocumentFile file : pickedDir.listFiles()) {
                        int n = 0;
                        InputStream input = getContentResolver().openInputStream(file.getUri());
                        while (-1 != (n = input.read(buffer))) {
                            String result = new String(buffer, "UTF-8");
                            mTextResult.setText(result);   // 画面上のTextViewに表示
                        }
                        input.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

                break;
            default:
                break;
        }
    }

まずポイントは7行目です。getContentResolver().takePersistableUriPermission()を呼び出して、FLAG_GRANT_WRITE_URI_PERMISSION(ついでにFLAG_GRANT_READ_URI_PERMISSIONも)を保存します。こうすることで、一度ユーザがアクセスを許可すると2回目以降、ユーザに対して同じダイアログは表示しません。使い勝手を考えると、アクセスのたびにダイアログで使用許可を求められるのは面倒でしょう。
6行目、dataオブジェクトの中にDCIMへのURI(content://...DCIM)が入っているのでそれを取り出し、8行目以降でDCIMディレクトリにテキストファイルを出力しています。
ちなみに、ここで無理やりURIの中身を変えるなどしてDCIM以外のディレクトリに書き込みしようとすると、12行目でnullが返ってきたり、takePersistableUriPermission()メソッドでSecurityExceptionが発生したりします。

Readボタンを押して読み込む(23行目以降)と、以下のように書き込んだ内容がテキストで表示されます。
f:id:uentseit:20160827025222p:plain:w200

なお、Androidマニフェストへのパーミッション定義やパーミッションリクエストをユーザに求める処理も必要ありません。かなり楽ちんになった気がします。

今日はこんなところです。