[Android][Lollipop]Android5.0で可能になったSDカードへの書き込みを試す


この記事はAndroid5.0で新しくSDカードへの書き込みが自由にアクセスできる方法が追加されたので、それについてのメモ、というか以下のstackoverflowのまとめです。

* [How to use the new SD-Card access API presented for Lollipop?](http://stackoverflow.com/questions/26744842/how-to-use-the-new-sd-card-access-api-presented-for-lollipop)

こんなニュースがきっかけですね。

* [Android 5.0（Lollipop）でSDカードなどの外部ストレージ全体への書き込みが再度サポートされる](http://juggly.cn/archives/133097.html)

権限についての言及が正しくないし、使ってみたかったので使ってみました。
ESファイルエクスプローラーではちゃんと実装されていました！

# AndroidにおけるSDカードのアクセス

ご存知の通り4.3以前では[WRITE_EXTERNAL_STORAGE](http://developer.android.com/reference/android/Manifest.permission.html#WRITE_EXTERNAL_STORAGE)を宣言すれば自由にSDカードだろうと何だろうとアクセスすることができました。
（SDカードがどこにあるかなんていう罠もありましたが。。今もかな？）

Googleの大方針としてはSDカード自体は推奨しないようで、External Storageにアクセスしても
しかし4.4 KitkatでついにSDカードへのアクセスは制限されてしまいました。

* [KitKat端末では今後アプリからMicro SDカードへの書き込みができくなる模様](http://juggly.cn/archives/106598.html)

ただし、[Storage Access Framework](https://developer.android.com/guide/topics/providers/document-provider.html)というものを使ってユーザに明示的に許可をもらえば、そのセッションの間で書き込みや変更が可能になりました。

詳しくはdocumentやこの記事がわかりやすいです。

* [Storage Access Framework を使用してみよう！](http://www.gaprot.jp/pickup/android-4-4/vol3/)
* [Android4.4 Storage Access Framework解説](http://blog.tomorrowkey.jp/2013-11-10/detail-of-storage-access-framework-on-android4-4/)

# KitkatにおけるStorage Access Framework(SAF)の使い方概要

Storage Access Frameworkは上記の記事を読んでいただければわかるのですが、

* ファイルへのアクセスをピッカー(標準ストレージであれば「ドキュメント」アプリ)を通してユーザに許可してもらう。
* 自分のアプリが管理できるストレージ(クラウドストレージなど)へのアクセスを許可するDocumentProviderを実装して、他のアプリからContentProviderのように編集できるようにする。

という二つの機能があります。
SDカードにアクセスするには一つ目のピッカーを介して許可を得て編集する、という方法をとります。

```java:MyFragment.java
// IntentでProviderを実装している呼びます。
public void performFileSearch() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    // Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
    // 適宜ピッカー表示の際のフィルターを追加
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("image/*");
    startActivityForResult(intent, READ_REQUEST_CODE);
}

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {
    // ピッカーで受け取った結果で色々行います。
    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            Log.i(TAG, "Uri: " + uri.toString());
            showImage(uri);
        }
    }
}
```

この例だとMediaStoreを介したアクセスとあまり変わりませんが、メディアに限らずアクセスできるのがいいところでしょう。。

# Lollipopでできるようになったこと

SAFが拡張されただけで、実は中身はKitkatの頃とほとんど変わりません。
ドキュメントアプリを呼び出すためのintentに[ACTION_OPEN_DOCUMENT_TREE](https://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT_TREE)が追加されました。

しかし、これによって許可されるのが、Directory以下の書き込み権限なので、ユーザにSDカード以下のアクセス権限を付与してもらえば、Android4.3以下と同様なアクセスが可能になる、というわけです。

また、恒常的にアクセス権を付与してもらうこともできるので、一度SDカード以下へのアクセスを許可されればそれ以降確認のためのピッカー表示が入らなくなります。

Kitkatでも恒常的なアクセス権限は保持できましたが、ファイルごとに権限が異なるため、結局毎回ピッカー表示が必要だったりしたので、この点が大きいかと思います。

## 実装例

実装方法はKitkatと変わらないです。
上記の例からだと、IntentのActionがOPEN_DOCUMENT_TREEに変わり、onActivityResultで許可された後に、Permissionを保持しています。
(保持は上の例でも可能です。)


```java:MyFragment.java
public void requestSdcardAccessPermission() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, REQUEST_CODE);
}
```
Actionの内容をOPEN_DOCUMENT_TREEに変更します。


```java:MyFragment.java

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    if (resultCode == Activity.RESULT_OK) {
        // IntentからtreeのURIを取得します。
        Uri treeUri = resultData.getData();
    
        DocumentFile pickedDir = DocumentFile.fromTreeUri(getActivity(), treeUri);
        // この間は自由にアクセスできるので、treeのリストなどを表示できます。
        // List all existing files inside picked directory
        for (DocumentFile file : pickedDir.listFiles()) {
            LogUtil.d(TAG, "Found file " + file.getName() + " with size " + file.length());
        }
```
先ほどのようにIntentからtreeのURIを取得します。
このUriが有効な間は自由にアクセスできるので、treeのリストなどを表示できます。

```java:MyFragment.java
        
        // 恒常的にPermissionを取得するにはContentResolverでtakePersistableUriPermissionを呼び出します。
        getActivity().getContentResolver().takePersistableUriPermission(treeUri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                
```
もしも恒常的にURIのPermissionを取得するにはContentResolverでtakePersistableUriPermissionを呼び出します。
また、後でアクセスするためにこの時のURIを覚えておく必要があります。


```java:MyFragment.java
        // JavaのFileを扱うように自由に書き込みもできます。
        // ただしSAFのAPIを介する必要があります。
        // また、一度付与されたuriは保持しておく必要があります。
        // Create a new file and write into it
        DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel");
        try {
            OutputStream out =　getActivity().getContentResolver().openOutputStream(newFile.getUri());
            out.write("A long time ago...".getBytes());
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```
SAFのAPIを介することで、IOStremなどJavaのAPIに変換できるので、自由に書き込みもできます。


SAFのAPI（[DocumentsContract](https://developer.android.com/reference/android/provider/DocumentsContract.html)など）を使いますが、ここではSupportLibraryの[DocumentFile](https://developer.android.com/reference/android/support/v4/provider/DocumentFile.html)を使ってアクセスしています。


```:java
// preferenceなどに保存しておいたURiを取り出します。
Uri treeUri = PreferenceUtil.getSharedPreferenceUri();

// SDカードのdocumentを作成します。
DocumentFile sdcard = DocumentFile.fromTreeUri(mContext, treeUri);
// そこからファイルをたどっていく必要があります。
DocumentFile nextDocument = document.findFile("test");
:

// APIを通じてIOStreamを作りたいときはDocumentFileを使うと便利です。
DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
    getContentResolver().openOutputStream(targetDocument.getUri());

```

# まとめ・注意点

注意点をまとめると、

* 一度ピッカーで特定ディレクトリ(全体ならSD全体)のアクセスをユーザにUIで指定・許可してもらう必要がある。（ハードルがとても高いと思います）
* URIを記録しておく必要が有る
* 通常のFileアクセスとは違うAPIを介する必要がある。

などでしょうか。。
まあ、できるようになったけど面倒には変わりない、という認識でいいかと思います。

ESファイルエクスプローラーはインストラクションでわかりやすく表現しており、大変参考になるかと思います。

![2.png](https://qiita-image-store.s3.amazonaws.com/0/42594/9744f41e-219a-67c5-c057-6ad3ac036edb.png "2.png")