この記事は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カードにアクセスするには一つ目のピッカーを介して許可を得て編集する、という方法をとります。
// 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を保持しています。
(保持は上の例でも可能です。)
public void requestSdcardAccessPermission() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE);
}
Actionの内容をOPEN_DOCUMENT_TREEに変更します。
@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のリストなどを表示できます。
// 恒常的に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の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を使ってアクセスしています。
// 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ファイルエクスプローラーはインストラクションでわかりやすく表現しており、大変参考になるかと思います。