Как использовать новый API доступа к SD-карте, представленный для Android 5.0 (Lollipop)?

Фон

На Android 4.4 (KitKat) Google сделал доступ к SD-карте довольно ограниченным.

Начиная с Android Lollipop (5.0), разработчики могут использовать новый API, который запрашивает у пользователя подтверждение доступа к определенным папкам, как написано в этом посте Google-Groups.

Эта проблема

Пост направляет вас посетить два сайта:

Это похоже на внутренний пример (возможно, позже будет показан на демоверсиях API), но довольно сложно понять, что происходит.

Это официальная документация нового API, но в ней недостаточно подробно описано, как его использовать.

Вот что это говорит вам:

Если вам действительно нужен полный доступ ко всему поддереву документов, начните с запуска ACTION_OPEN_DOCUMENT_TREE, чтобы позволить пользователю выбрать каталог. Затем передайте полученный getData() в fromTreeUri(Context, Uri), чтобы начать работу с выбранным пользователем деревом.

При навигации по дереву экземпляров DocumentFile вы всегда можете использовать getUri(), чтобы получить Uri, представляющий базовый документ для этого объекта, для использования с openInputStream(Uri) и т. Д.

Чтобы упростить ваш код на устройствах с KITKAT или более ранними версиями, вы можете использовать fromFile(File), который эмулирует поведение DocumentsProvider.

Вопросы

У меня есть несколько вопросов о новом API:

  1. Как вы на самом деле используете это?
  2. Согласно сообщению, ОС запомнит, что приложению было дано разрешение на доступ к файлам / папкам. Как проверить, есть ли у вас доступ к файлам / папкам? Есть ли функция, которая возвращает мне список файлов / папок, к которым я могу получить доступ?
  3. Как вы решаете эту проблему на Kitkat? Это часть библиотеки поддержки?
  4. Есть ли в ОС экран настроек, который показывает, какие приложения имеют доступ к каким файлам / папкам?
  5. Что произойдет, если приложение установлено для нескольких пользователей на одном устройстве?
  6. Есть ли другая документация / руководство по этому новому API?
  7. Можно ли отозвать разрешения? Если да, есть ли намерение, которое отправляется в приложение?
  8. Будет ли запрашивать разрешение рекурсивно работать на выбранной папке?
  9. Позволит ли использование разрешения также дать пользователю возможность множественного выбора по выбору пользователя? Или приложение должно специально указать намерение, какие файлы / папки разрешить?
  10. Есть ли на эмуляторе способ попробовать новый API? Я имею в виду, что у него есть раздел SD-карты, но он работает как основное внешнее хранилище, поэтому весь доступ к нему уже предоставлен (с помощью простого разрешения).
  11. Что происходит, когда пользователь заменяет SD-карту на другую?

3 ответа

Много хороших вопросов, давайте копаться.:)

Как вы используете его?

Вот отличное руководство по взаимодействию с Storage Access Framework в KitKat:

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

Взаимодействие с новыми API в Lollipop очень похоже. Чтобы предложить пользователю выбрать дерево каталогов, вы можете запустить намерение, подобное этому:

    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() является достаточно гибким для использования с различными API-интерфейсами платформы. Например, вы можете поделиться им, используя Intent.setData() с Intent.FLAG_GRANT_READ_URI_PERMISSION,

Если вы хотите получить доступ к этому Uri из собственного кода, вы можете позвонить ContentResolver.openFileDescriptor() а затем использовать ParcelFileDescriptor.getFd() или же detachFd() получить традиционное целое число дескриптора файла POSIX.

Как проверить, есть ли у вас доступ к файлам / папкам?

По умолчанию Uris, возвращенный через намерения Storage Access Frameworks, не сохраняется при перезагрузке. Платформа "предлагает" возможность сохранить разрешение, но вам все равно нужно "получить" разрешение, если вы этого хотите. В нашем примере выше вы бы позвонили:

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

Вы всегда можете выяснить, какие постоянные гранты имеет доступ к вашему приложению через ContentResolver.getPersistedUriPermissions() API. Если вам больше не нужен доступ к сохраненному Uri, вы можете освободить его с помощью ContentResolver.releasePersistableUriPermission(),

Это доступно на KitKat?

Нет, мы не можем задним числом добавить новые функциональные возможности в более старые версии платформы.

Могу ли я увидеть, какие приложения имеют доступ к файлам / папкам?

В настоящее время нет пользовательского интерфейса, который показывает это, но вы можете найти подробности в разделе "Предоставленные разрешения Uri" adb shell dumpsys activity providers выход.

Что произойдет, если приложение установлено для нескольких пользователей на одном устройстве?

Разрешения Uri изолированы для каждого пользователя, как и все другие функции многопользовательской платформы. То есть одно и то же приложение, работающее под двумя разными пользователями, не имеет перекрывающихся или общих разрешений Uri.

Можно ли отозвать разрешения?

Поддерживающий DocumentProvider может отозвать разрешение в любое время, например, при удалении облачного документа. Наиболее распространенный способ обнаружить эти отозванные разрешения - это когда они исчезают из ContentResolver.getPersistedUriPermissions() упомянутое выше.

Разрешения также аннулируются всякий раз, когда данные приложения очищаются для любого приложения, участвующего в предоставлении.

Будет ли запрашивать разрешение рекурсивно работать на выбранной папке?

Да, ACTION_OPEN_DOCUMENT_TREE intent предоставляет рекурсивный доступ как к существующим, так и к вновь созданным файлам и каталогам.

Это позволяет множественный выбор?

Да, множественный выбор поддерживается с KitKat, и вы можете разрешить его, установив EXTRA_ALLOW_MULTIPLE при запуске вашего ACTION_OPEN_DOCUMENT намерение. Ты можешь использовать Intent.setType() или же EXTRA_MIME_TYPES сузить типы файлов, которые можно выбрать:

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

Есть ли на эмуляторе способ попробовать новый API?

Да, основное устройство общего хранения должно появляться в средстве выбора даже на эмуляторе. Если ваше приложение использует только Storage Access Framework для доступа к общему хранилищу, вам больше не нужно READ/WRITE_EXTERNAL_STORAGE разрешения на все и могут удалить их или использовать android:maxSdkVersion возможность запрашивать их только на старых версиях платформы.

Что происходит, когда пользователь заменяет SD-карту на другую?

Когда задействован физический носитель, UUID (такой как серийный номер FAT) базового носителя всегда записывается в возвращенный Uri. Система использует это, чтобы подключить вас к носителю, который первоначально выбрал пользователь, даже если пользователь меняет носитель между несколькими слотами.

Если пользователь поменяет вторую карту, вам нужно будет запросить доступ к новой карте. Поскольку система запоминает разрешения для каждого UUID, у вас останется ранее предоставленный доступ к исходной карте, если пользователь повторно вставит ее позже.

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

В моем Android-проекте на Github, указанном ниже, вы можете найти рабочий код, который позволяет писать на extSdCard в Android 5. Предполагается, что пользователь предоставляет доступ ко всей 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);
        }
    }
}

Получение outputStream для файла через Storage Access Framework (используя сохраненный URL-адрес, предполагая, что это URL-адрес корневой папки внешней SD-карты)

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

Это просто дополнительный ответ.

После создания нового файла вам может потребоваться сохранить его местоположение в базе данных и прочитать его завтра. Вы можете прочитать, восстановить его снова, используя этот метод:

/**
 * 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();
    }
}
Другие вопросы по тегам