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


118

Фон

На Android 4.4 (KitKat) Google зробив доступ до SD-карти досить обмеженим.

Станом на Android Lollipop (5.0), розробники можуть використовувати новий API, який просить користувача підтвердити, щоб він мав доступ до певних папок, як це написано в цій публікації Google-груп .

Проблема

Публікація спрямовує вас відвідувати два веб-сайти:

Це виглядає як внутрішній приклад (можливо, він буде показаний на демонстраційних версіях API пізніше), але досить важко зрозуміти, що відбувається.

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

Ось що це говорить вам:

Якщо вам дійсно потрібен повний доступ до цілого піддерева документів, почніть з запуску ACTION_OPEN_DOCUMENT_TREE, щоб користувач міг вибрати каталог. Потім перенесіть отриманий getData () вTreeUri (контекст, Uri), щоб розпочати роботу з обраним користувачем деревом.

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

Щоб спростити свій код на пристроях, на яких запущено KITKAT або новіші версії, ви можете використовувати fromFile (Файл), який імітує поведінку DocumentsProvider.

Питання

У мене є кілька запитань щодо нового API:

  1. Як ти насправді ним користуєшся?
  2. Відповідно до публікації, ОС пам’ятатиме, що програма отримала дозвіл на доступ до файлів / папок. Як перевірити, чи можете ви отримати доступ до файлів / папок? Чи є функція, яка повертає мені список файлів / папок, до яких я можу отримати доступ?
  3. Як ви вирішуєте цю проблему на Kitkat? Це частина бібліотеки підтримки?
  4. Чи є екран налаштувань на ОС, який показує, які програми мають доступ до яких файлів / папок?
  5. Що станеться, якщо додаток встановлено для декількох користувачів на одному пристрої?
  6. Чи є інша документація / посібник щодо цього нового API?
  7. Чи можна скасувати дозволи? Якщо так, чи є намір, який надсилається в додаток?
  8. Попросили б дозвіл працювати рекурсивно у вибраній папці?
  9. Чи дозволить використання дозволу надати користувачеві можливість багаторазового вибору за вибором користувача? Або додатку потрібно конкретно вказати намір, які файли / папки дозволити?
  10. Чи є можливість на емуляторі спробувати новий API? Я маю на увазі, у нього є розділ SD-картки, але він працює як первинний зовнішній накопичувач, тому весь доступ до нього вже надається (за допомогою простого дозволу).
  11. Що відбувається, коли користувач замінить SD-карту на іншу?

FWIW, в AndroidPolice була невеличка стаття про це сьогодні.
фініш

@fattire Дякую але вони показують про те, що я вже читав. Хоча це гарна новина.
андроїд розробник

32
Щоразу, коли виходить нова ОС, вони ускладнюють наше життя ...
Phantômaxx

@Funkystein правда. Хотілося б, щоб вони зробили це на Кіткаті. Зараз є 3 типи поведінки, з якими можна впоратися.
андроїд розробник

1
@Funkystein я не знаю. Я ним користувався давно. Дуже не так погано робити цю перевірку. Ви повинні пам’ятати, що вони теж люди, щоб вони могли робити помилки та час від часу
міркувати

Відповіді:


143

Багато хороших запитань, давайте розберемося. :)

Як ти ним користуєшся?

Ось чудовий підручник для взаємодії з рамкою доступу до зберігання в KitKat:

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

Взаємодія з новими 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();
    }
}

Повернений Uri DocumentFile.getUri()досить гнучкий для використання з різними API платформ. Наприклад, ви можете поділитися ним за Intent.setData()допомогою Intent.FLAG_GRANT_READ_URI_PERMISSION.

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

Як перевірити, чи можете ви отримати доступ до файлів / папок?

За замовчуванням Uris, що повернувся через рамки доступу до зберігання, не зберігається через перезавантаження. Платформа "пропонує" можливість зберігати дозвіл, але вам потрібно "взяти" дозвіл, якщо ви цього хочете. У нашому прикладі вище, ви зателефонуєте:

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

Ви завжди можете розібратися, що надає доступ до вашої програми через ContentResolver.getPersistedUriPermissions()API. Якщо вам більше не потрібен доступ до збереженого Урі, ви можете звільнити його ContentResolver.releasePersistableUriPermission().

Це доступно на KitKat?

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

Чи можу я побачити, які програми мають доступ до файлів / папок?

Наразі немає інтерфейсу, який би це показував, але ви можете знайти деталі у розділі "Випущені дозволи на Uri" adb shell dumpsys activity providers.

Що станеться, якщо додаток встановлено для декількох користувачів на одному пристрої?

Дотації на дозвіл Uri виділяються на основі кожного користувача, як і всі інші функції багатокористувацької платформи. Тобто, той самий додаток, який працює під двома різними користувачами, не має накладених дозволів на дозвільне використання 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?

Так, основний пристрій спільного зберігання повинен з’являтися в засобі вибору, навіть на емуляторі. Якщо ваш додаток використовує тільки Framework Access Storage для доступу до спільної пам'яті, вам більше не потрібні READ/WRITE_EXTERNAL_STORAGEдозволи на всі і може видалити їх або використовувати android:maxSdkVersionфункцію тільки запросити їх на більш старих версіях платформи.

Що відбувається, коли користувач замінить SD-карту на іншу?

Якщо задіяні фізичні носії, UUID (наприклад, серійний номер FAT) базових носіїв інформації завжди спалюється у повернутий Uri. Система використовує це для підключення вас до медіа, який користувач спочатку обрав, навіть якщо користувач обмінюється носієм між кількома слотами.

Якщо користувач обміняється другою карткою, вам потрібно буде запропонувати отримати доступ до нової картки. Оскільки система запам'ятовує дотації на основі UUID, вам надалі надаватиметься доступ до оригінальної картки, якщо користувач повторно вставить її пізніше.

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


2
Я бачу. тож рішенням було замість того, щоб додавати більше до відомого API (File), використовувати інший. ГАРАЗД. Чи можете ви відповісти на інші запитання (написані в першому коментарі)? До речі, дякую, що відповіли на все це.
андроїд розробник

7
@JeffSharkey Будь-який спосіб надати OPEN_DOCUMENT_TREE підказку про початкове місцезнаходження? Користувачі не завжди найкраще орієнтуються на те, до чого потрібно отримати доступ.
Джастін

2
Чи існує спосіб, як змінити метод Last Modified Timestamp ( setLastModified () у класі File)? Це дійсно принципово для таких програм, як архіватори.
Кварк

1
Скажімо, у вас є збережений документ папки Uri, і ви хочете перерахувати файли пізніше в якийсь момент після перезавантаження пристрою. DocumentFile.fromTreeUri завжди перераховує кореневі файли, незалежно від Uri, який ви їм надаєте (навіть дочірній Урі), тож як створити DocumentFile, ви можете викликати список файлів, де DocumentFile не представляє ні кореня, ні SingleDocument.
AndersC

1
@JeffSharkey Як можна використовувати цей URI в MediaMuxer, оскільки він приймає рядок як шлях до вихідного файлу? MediaMuxer (java.lang.String, int
Petrakeas

45

У моєму 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);
}

Поводження з відповіддю за рамками доступу до пам’яті:

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

Отримання вихідного потоку для файлу через рамку доступу до зберігання (використання збереженої 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


1
Чи можете ви помістити його в мінімізований проект, який обробляє тільки SD-карту? Крім того, чи знаєте ви, як я можу перевірити, чи доступні також усі зовнішні сховища, щоб я нічого не просив їх дозволу?
андроїд розробник

1
Бажаю, щоб я міг більше підтримати це. Я був на півдорозі до цього рішення і вважав навігацію Документом такою жахливою, що думав, що я роблю це неправильно. Добре мати певне підтвердження щодо цього. Дякую Google ... ні за що.
Ентоні

1
Так, для запису на зовнішній SD ви більше не можете використовувати звичайний файловий підхід. З іншого боку, для старих версій Android та для первинної SD-файлу File все ще є найбільш ефективним підходом. Тому для доступу до файлів слід використовувати спеціальний клас утиліти.
Йорг Ейсфельд

15
@ JörgEisfeld: У мене є додаток, який використовує File254 рази. Ви можете уявити, як це виправити ?? Android стає кошмаром для розробників із його цілковитою відсутністю відсталої сумісності. Я досі не знайшов місця, де вони пояснюють, чому Google прийняв усі ці дурні рішення щодо зовнішнього сховища. Деякі заявляють про "безпеку", але, звичайно, це нісенітниця, оскільки будь-яка програма може зіпсувати внутрішню пам’ять. Моя здогадка - спробувати змусити нас використовувати їх хмарні сервіси. На щастя, вкорінення вирішує проблеми ... принаймні для Android <6 ....
Луїс А. Флорит

1
ГАРАЗД. Магічно після перезавантаження телефону він працює :)
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();
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.