56
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

2.png

56
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?