Android 5.0(Lollipop)用に提示された新しいSDカードアクセスAPIの使用方法


118

バックグラウンド

のAndroid 4.4(キットカット)、Googleがかなり制限されたSDカードにアクセスしてきました。

Android Lollipop(5.0)以降、デベロッパーは、このGoogleグループの投稿に記載されているように、特定のフォルダへのアクセスを許可することをユーザーに確認する新しいAPIを使用できます。

問題

この投稿では、2つのWebサイトにアクセスするように指示されています。

これは内部の例のように見えますが(おそらくAPIデモで後で示される可能性があります)、何が起こっているのかを理解するのは非常に困難です。

これは新しいAPIの公式ドキュメントですが、それを使用する方法についての十分な詳細は記載されていません。

これはあなたに言っていることです:

ドキュメントのサブツリー全体への完全なアクセスが本当に必要な場合は、まずACTION_OPEN_DOCUMENT_TREEを起動して、ユーザーがディレクトリを選択できるようにします。次に、結果のgetData()をfromTreeUri(Context、Uri)に渡して、ユーザーが選択したツリーの操作を開始します。

DocumentFileインスタンスのツリーをナビゲートするときは、常にgetUri()を使用して、そのオブジェクトの基になるドキュメントを表すUriを取得したり、openInputStream(Uri)などで使用したりできます。

KITKAT以前を実行しているデバイスでコードを簡略化するには、DocumentsProviderの動作をエミュレートするfromFile(File)を使用できます。

質問

新しいAPIについていくつか質問があります。

  1. 実際にどのように使用しますか?
  2. 投稿によると、OSはアプリにファイル/フォルダーへのアクセス許可が与えられたことを記憶します。ファイル/フォルダにアクセスできるかどうかをどのように確認しますか?アクセスできるファイル/フォルダのリストを返す関数はありますか?
  3. キットカットでこの問題をどのように処理しますか?サポートライブラリの一部ですか?
  4. OSに、どのアプリがどのファイル/フォルダーにアクセスできるかを示す設定画面はありますか?
  5. 同じデバイスに複数のユーザー向けにアプリをインストールするとどうなりますか?
  6. この新しいAPIに関する他のドキュメント/チュートリアルはありますか?
  7. 権限を取り消すことはできますか?もしそうなら、アプリに送信されているインテントはありますか?
  8. 選択したフォルダに対して再帰的にアクセス許可を要求しますか?
  9. アクセス許可を使用すると、ユーザーがユーザーの選択による複数の選択の機会をユーザーに与えることもできますか?それともアプリは、どのファイル/フォルダーを許可するかを具体的に伝える必要がありますか?
  10. エミュレータで新しいAPIを試す方法はありますか?つまり、SDカードパーティションがありますが、プライマリ外部ストレージとして機能するため、(単純なアクセス許可を使用して)そこへのすべてのアクセスが既に与えられています。
  11. ユーザーがSDカードを別のカードに交換するとどうなりますか?

FWIW、AndroidPoliceは今日これについて少し記事を書いています。
fattire 2014年

@fattireありがとうございます。しかし、彼らは私がすでに読んだことについて示しています。それは良いニュースです。
Android開発者

32
新しいOSが出てくるたびに、彼らは...私たちの生活をより複雑にする
Phantômaxx

@Funkystein true。彼らがキットカットでそれをしたかったのに。現在、処理する動作には3つのタイプがあります。
Android開発者

1
@Funkysteinわからない。ずっと前から使っていました。このチェックを行うのはそれほど悪くはないと思います。彼らも人間であることを覚えておかなければならないので、彼らは時々間違いを犯し、彼らの考えを変えることができます...
Android開発者

回答:


143

良い質問がたくさんあるので、掘り下げてみましょう。:)

どんなふうに使うの?

次に、KitKatのストレージアクセスフレームワークを操作するための優れたチュートリアルを示します。

https://developer.android.com/guide/topics/providers/document-provider.html#client

Lollipopの新しいAPIの操作は非常に似ています。ユーザーにディレクトリツリーを選択するように促すには、次のようなインテントを起動します。

    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, 42);

次に、onActivityResult()で、ユーザーが選択したUriを新しいDocumentFileヘルパークラスに渡すことができます。次に、選択したディレクトリ内のファイルを一覧表示し、新しいファイルを作成する簡単な例を示します。

public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    if (resultCode == RESULT_OK) {
        Uri treeUri = resultData.getData();
        DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);

        // List all existing files inside picked directory
        for (DocumentFile file : pickedDir.listFiles()) {
            Log.d(TAG, "Found file " + file.getName() + " with size " + file.length());
        }

        // Create a new file and write into it
        DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel");
        OutputStream out = getContentResolver().openOutputStream(newFile.getUri());
        out.write("A long time ago...".getBytes());
        out.close();
    }
}

によって返されるUriは、DocumentFile.getUri()さまざまなプラットフォームAPIで使用できるほど柔軟です。たとえば、を使用Intent.setData()して共有できますIntent.FLAG_GRANT_READ_URI_PERMISSION

ネイティブコードからそのUriにアクセスする場合は、またはをContentResolver.openFileDescriptor()使用して、従来のPOSIXファイル記述子整数を取得できます。ParcelFileDescriptor.getFd()detachFd()

ファイル/フォルダにアクセスできるかどうかをどのように確認しますか?

デフォルトでは、Storage Access Frameworksインテントを通じて返されたUrisは、再起動後は保持されません。プラットフォームは、パーミッションを永続化する機能を「提供」しますが、必要な場合はパーミッションを「取得」する必要があります。上記の例では、次のように呼び出します。

    getContentResolver().takePersistableUriPermission(treeUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION |
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

ContentResolver.getPersistedUriPermissions()API を介してアプリがアクセスできる永続的な付与をいつでも把握できます。永続化されたUriにアクセスする必要がなくなった場合は、を使用して解放できますContentResolver.releasePersistableUriPermission()

これはKitKatで利用できますか?

いいえ、古いバージョンのプラットフォームに新しい機能をさかのぼって追加することはできません。

ファイル/フォルダにアクセスできるアプリを確認できますか?

現在、これを示すUIはありませんが、adb shell dumpsys activity providers出力の「Granted Uri Permissions」セクションで詳細を確認できます。

同じデバイスに複数のユーザー向けにアプリをインストールするとどうなりますか?

Uri権限の付与は、他のすべてのマルチユーザープラットフォーム機能と同様に、ユーザーごとに分離されます。つまり、2人の異なるユーザーのもとで実行されている同じアプリには、重複したり共有されたUriパーミッションはありません。

権限を取り消すことはできますか?

バッキングDocumentProviderは、クラウドベースのドキュメントが削除されたときなど、いつでも権限を取り消すことができます。これらの取り消されたアクセス許可を検出する最も一般的な方法は、ContentResolver.getPersistedUriPermissions()上記の説明から消えたときです。

権限に関与しているいずれかのアプリのアプリデータがクリアされると、アクセス許可も取り消されます。

選択したフォルダに対して再帰的にアクセス許可を要求しますか?

ACTION_OPEN_DOCUMENT_TREEはい、このインテントにより、既存のファイルと新しく作成されたファイルとディレクトリの両方に再帰的にアクセスできます。

これは複数選択を許可しますか?

はい、複数選択はKitKatからサポートされておりEXTRA_ALLOW_MULTIPLEACTION_OPEN_DOCUMENTインテントを開始するときに設定することで許可できます。Intent.setType()またはEXTRA_MIME_TYPESを使用して、選択できるファイルのタイプを絞り込むことができます。

http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT

エミュレータで新しいAPIを試す方法はありますか?

はい、プライマリ共有ストレージデバイスは、エミュレータ上でもピッカーに表示されます。アプリが共有ストレージへのアクセスにストレージアクセスフレームワークのみを使用している場合は、アクセスREAD/WRITE_EXTERNAL_STORAGE許可はまったく必要なく、削除したり、android:maxSdkVersion機能を使用して古いプラットフォームバージョンでのみ要求したりできます。

ユーザーがSDカードを別のカードに交換するとどうなりますか?

物理メディアが関係する場合、元のメディアのUUID(FATシリアル番号など)は常に返されたUriに書き込まれます。システムはこれを使用して、ユーザーが複数のスロット間でメディアを交換しても、ユーザーが最初に選択したメディアに接続します。

ユーザーが2枚目のカードを入れ替えた場合は、新しいカードにアクセスするように求めるメッセージが表示されます。システムは許可をUUIDごとに記憶するため、ユーザーが後でカードを再挿入した場合でも、以前に許可されていた元のカードへのアクセスが継続されます。

http://en.wikipedia.org/wiki/Volume_serial_number


2
そうですか。そのため、既知のAPI(ファイル)にさらに追加するのではなく、別のAPIを使用することが決定されました。OK。(最初のコメントに書かれている)他の質問に答えてもらえますか?ところで、このすべてに答えてくれてありがとう。
Android開発者

7
@JeffSharkey OPEN_DOCUMENT_TREEに開始位置のヒントを提供する方法はありますか?ユーザーは、アプリケーションがアクセスする必要があるものにナビゲートするのがいつも得意であるとは限りません。
Justin

2
Last Modified Timestamp(FileクラスのsetLastModified()メソッドを変更する方法はありますか?アーカイバのようなアプリケーションにとっては本当に基本的なことです。
Quark、2014

1
保存されたフォルダドキュメントUriがあり、デバイスの再起動後のある時点でファイルをリストしたいとします。DocumentFile.fromTreeUriは、指定したUri(子Uriも含む)に関係なく、常にルートファイルを一覧表示します。DocumentFileを作成するには、どのようにしてlistfilesを呼び出すことができますか?
AndersC、2015年

1
@JeffSharkey文字列を出力ファイルパスとして受け入れるため、このURIをMediaMuxerでどのように使用できますか? MediaMuxer(java.lang.String、int
Petrakeas

45

以下にリンクされているGithubの私のAndroidプロジェクトには、Android 5のextSdCardに書き込むことができる実際のコードがあります。ユーザーがSDカード全体へのアクセスを許可し、このカードのすべての場所に書き込むことができると想定しています。(単一のファイルのみにアクセスしたい場合は、物事が簡単になります。)

メインコードスニペット

ストレージアクセスフレームワークのトリガー:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}

ストレージアクセスフレームワークからの応答の処理:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
    if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) {
        Uri treeUri = null;
        if (resultCode == Activity.RESULT_OK) {
            // Get Uri from Storage Access Framework.
            treeUri = resultData.getData();

            // Persist URI in shared preference so that you can use it later.
            // Use your own framework here instead of PreferenceUtil.
            PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri);

            // Persist access permissions.
            final int takeFlags = resultData.getFlags()
                & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
        }
    }
}

Storage Access Frameworkを介してファイルのoutputStreamを取得します(保存されているURLを使用します。これが外部SDカードのルートフォルダーのURLであると想定しています)

DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
    getContentResolver().openOutputStream(targetDocument.getUri());

これは、次のヘルパーメソッドを使用します。

public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) {
    String baseFolder = getExtSdCardFolder(file);

    if (baseFolder == null) {
        return null;
    }

    String relativePath = null;
    try {
        String fullPath = file.getCanonicalPath();
        relativePath = fullPath.substring(baseFolder.length() + 1);
    }
    catch (IOException e) {
        return null;
    }

    Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard);

    if (treeUri == null) {
        return null;
    }

    // start with root of SD card and then parse through document tree.
    DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri);

    String[] parts = relativePath.split("\\/");
    for (int i = 0; i < parts.length; i++) {
        DocumentFile nextDocument = document.findFile(parts[i]);

        if (nextDocument == null) {
            if ((i < parts.length - 1) || isDirectory) {
                nextDocument = document.createDirectory(parts[i]);
            }
            else {
                nextDocument = document.createFile("image", parts[i]);
            }
        }
        document = nextDocument;
    }

    return document;
}

public static String getExtSdCardFolder(final File file) {
    String[] extSdPaths = getExtSdCardPaths();
    try {
        for (int i = 0; i < extSdPaths.length; i++) {
            if (file.getCanonicalPath().startsWith(extSdPaths[i])) {
                return extSdPaths[i];
            }
        }
    }
    catch (IOException e) {
        return null;
    }
    return null;
}

/**
 * Get a list of external SD card paths. (Kitkat or higher.)
 *
 * @return A list of external SD card paths.
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
private static String[] getExtSdCardPaths() {
    List<String> paths = new ArrayList<>();
    for (File file : Application.getAppContext().getExternalFilesDirs("external")) {
        if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) {
            int index = file.getAbsolutePath().lastIndexOf("/Android/data");
            if (index < 0) {
                Log.w(Application.TAG, "Unexpected external file dir: " + file.getAbsolutePath());
            }
            else {
                String path = file.getAbsolutePath().substring(0, index);
                try {
                    path = new File(path).getCanonicalPath();
                }
                catch (IOException e) {
                    // Keep non-canonical path.
                }
                paths.add(path);
            }
        }
    }
    return paths.toArray(new String[paths.size()]);
}

 /**
 * Retrieve the application context.
 *
 * @return The (statically stored) application context
 */
public static Context getAppContext() {
    return Application.mApplication.getApplicationContext();
}

完全なコードへの参照

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/SettingsFragment.java#L521

そして

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java


1
SDカードのみを処理する最小化されたプロジェクトに入れてもらえますか?また、利用可能なすべての外部ストレージにアクセスできるかどうかを確認して、何の許可も要求しないようにする方法を知っていますか?
Android開発者

1
これをもっと賛成できるといいのですが。私はこのソリューションの途中で、ドキュメントナビゲーションがひどいので、間違っていると思いました。これについていくつか確認しておくのは良いことです。Googleに感謝します。
アンソニー

1
はい、外部SDに書き込む場合は、通常のファイルアプローチを使用できなくなります。一方、古いAndroidバージョンとプライマリSDの場合、ファイルが依然として最も効率的なアプローチです。したがって、ファイルアクセスにはカスタムユーティリティクラスを使用する必要があります。
イェルクEisfeld

15
@JörgEisfeld:File254回使用するアプリがあります。あなたはそれを修正することを想像できますか?Androidは、下位互換性がまったくないため、開発者にとって悪夢になりつつあります。それでも、Googleが外部ストレージに関してこの愚かな決定をすべて行った理由を説明する場所はまだ見つかりませんでした。「セキュリティ」と主張する人もいますが、どのアプリも内部ストレージを台無しにする可能性があるため、もちろんナンセンスです。私の推測では、クラウドサービスを使用するように強制しようとしています。ありがたいことに、応援することで問題は解決します...少なくともAndroid <6の場合....
Luis A. Florit

1
OK。魔法のように、私は私の電話を再起動した後、それは動作します:)
sancho21

0

それは単なる補足的な答えです。

新しいファイルを作成した後、その場所をデータベースに保存して、明日読む必要がある場合があります。次のメソッドを使用して、それを再度読み取ることができます。

/**
 * Get {@link DocumentFile} object from SD card.
 * @param directory SD card ID followed by directory name, for example {@code 6881-2249:Download/Archive},
 *                 where ID for SD card is {@code 6881-2249}
 * @param fileName for example {@code intel_haxm.zip}
 * @return <code>null</code> if does not exist
 */
public static DocumentFile getExternalFile(Context context, String directory, String fileName){
    Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/" + directory);
    DocumentFile parent = DocumentFile.fromTreeUri(context, uri);
    return parent != null ? parent.findFile(fileName) : null;
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS && resultCode == RESULT_OK) {
        int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
        getContentResolver().takePersistableUriPermission(data.getData(), takeFlags);
        String sdcard = data.getDataString().replace("content://com.android.externalstorage.documents/tree/", "");
        try {
            sdcard = URLDecoder.decode(sdcard, "ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        // for example, sdcardId results "6312-2234"
        String sdcardId = sdcard.substring(0, sdcard.indexOf(':'));
        // save to preferences if you want to use it later
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        preferences.edit().putString("sdcard", sdcardId).apply();
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.