Posted at

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

More than 3 years have passed since last update.

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

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

権限についての言及が正しくないし、使ってみたかったので使ってみました。

ESファイルエクスプローラーではちゃんと実装されていました!


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

ご存知の通り4.3以前ではWRITE_EXTERNAL_STORAGEを宣言すれば自由にSDカードだろうと何だろうとアクセスすることができました。

(SDカードがどこにあるかなんていう罠もありましたが。。今もかな?)

Googleの大方針としてはSDカード自体は推奨しないようで、External Storageにアクセスしても

しかし4.4 KitkatでついにSDカードへのアクセスは制限されてしまいました。

ただし、Storage Access Frameworkというものを使ってユーザに明示的に許可をもらえば、そのセッションの間で書き込みや変更が可能になりました。

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


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

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


  • ファイルへのアクセスをピッカー(標準ストレージであれば「ドキュメント」アプリ)を通してユーザに許可してもらう。

  • 自分のアプリが管理できるストレージ(クラウドストレージなど)へのアクセスを許可するDocumentProviderを実装して、他のアプリからContentProviderのように編集できるようにする。

という二つの機能があります。

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


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が追加されました。

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

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

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


実装例

実装方法はKitkatと変わらないです。

上記の例からだと、IntentのActionがOPEN_DOCUMENT_TREEに変わり、onActivityResultで許可された後に、Permissionを保持しています。

(保持は上の例でも可能です。)


MyFragment.java

public void requestSdcardAccessPermission() {

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE);
}

Actionの内容をOPEN_DOCUMENT_TREEに変更します。


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のリストなどを表示できます。


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を覚えておく必要があります。


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など)を使いますが、ここではSupportLibraryのDocumentFileを使ってアクセスしています。


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