web-dev-qa-db-ja.com

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

バックグラウンド

Android 4.4 (KitKat)では、GoogleはSDカードへのアクセスをかなり制限しました。

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

問題

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

たくさんの良い質問、掘り下げましょう:)

それをどうやって使いますか?

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();
    }
}

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

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

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

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

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

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

これはキットカットで利用できますか?

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

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

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

同じデバイス上の複数のユーザーにアプリがインストールされた場合はどうなりますか?

Uriの許可付与は、他のすべてのマルチユーザープラットフォーム機能と同様に、ユーザーごとに分離されます。つまり、2人の異なるユーザーの下で実行されている同じアプリには、重複または共有されたUriアクセス許可が付与されません。

許可を取り消すことはできますか?

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

許可に関係するいずれかのアプリのアプリデータが消去されるたびに、権限も取り消されます。

選択したフォルダで許可を再帰的に要求しますか?

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

これにより複数選択が可能ですか?

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

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

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

はい、エミュレータでも、プライマリ共有ストレージデバイスがピッカーに表示されます。アプリが共有ストレージへのアクセスにStorage Access Frameworkのみを使用する場合、READ/WRITE_EXTERNAL_STORAGE権限はまったく必要ありません。それらを削除するか、Android:maxSdkVersion機能を使用できます。古いプラットフォームバージョンでのみリクエストする。

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

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

ユーザーが2枚目のカードを交換した場合、新しいカードへのアクセスを求めるプロンプトを表示する必要があります。システムはUUID単位で許可を記憶しているため、ユーザーが後で再挿入した場合、元のカードへの以前に許可されたアクセス権を引き続き保持します。

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

135
Jeff Sharkey

以下にリンクされている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);
}

Storage Access Frameworkからの応答の処理:

@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

40
Jörg Eisfeld

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

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

/**
 * 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();
    }
}
0
Anggrayudi H