Принудительный доступ к внешней съемной карте microSD

Я использую Samsung A3, Android 5.0.2. Я использую эту настройку для компиляции приложений, т.е. цель Android 4.1 Jelly Bean (API 16).

Я точно знаю путь к внешней съемной карте microSD, это /mnt/extSdCard/ (см. также примечание №7 ниже).

Проблема: я замечаю, что

File myDir = new File("/mnt/extSdCard/test");
myDir.mkdirs();

не работает: каталог не создается.

Также:

File file = new File("/mnt/extSdCard/books/test.txt");   // the folder "books" already exists on the external microSD card, has been created from computer with USB connection
FileOutputStream fos = new FileOutputStream(file);

выдает эту ошибку:

java.io.FileNotFoundException: /mnt/extSdCard/books/test.txt: ошибка открытия: EACCES (отказано в доступе) в libcore.io.IoBridge.open(...

Как обеспечить доступ для чтения и записи к внешней съемной карте microSD?

Примечания:

  1. Environment.getExternalStorageDirectory().toString() дает /storage/emulated/0, что является внутренней памятью моего телефона, т.е. не то, что я хочу.

  2. getExternalFilesDir(null) дает /storage/emulated/0/Android/data/com.blahblah.appname/files/ т.е. не то, что я хочу. Обратите внимание, что я не могу использовать getExternalFilesDirs с финальным s, потому что это недоступно в API16. Также разрешения во время выполнения недоступны в API16.

  3. У меня уже есть <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />, а также READ_EXTERNAL_STORAGE.

  4. Я прочитал много тем, таких как эта или этот, на самом деле, наверное, двадцать подобных вопросов, но наконец, это кажется очень сложным, и все, и наоборот, сказано. Это мое, я ищу решение, специфичное для этой ситуации.

  5. Мне не нужны ACTION_OPEN_DOCUMENT и ACTION_CREATE_DOCUMENT, на самом деле мне не нужно решение с графическим интерфейсом.

  6. Некоторые мои приложения (Sync Resilio) могут успешно изменять /mnt/extSdCard/music/, создавать там новые файлы и т. д.

  7. Кстати, ls -la /mnt/extSdCard/ дает

    drwxrwx--x root     sdcard_r          2017-10-15 01:21 Android
    drwxrwx--- root     sdcard_r          2017-10-14 00:59 LOST.DIR
    drwxrwx--- root     sdcard_r          2017-12-05 16:44 books
    drwxrwx--- root     sdcard_r          2017-11-21 22:55 music
    

person Basj    schedule 10.12.2017    source источник
comment
Вы забыли упомянуть разрешение во время выполнения.   -  person greenapps    schedule 10.12.2017
comment
Посмотрите на getExternalFilesDirs(). Если вам повезет, он вернет два пути. Второй будет доступен для записи на карте micro SD.   -  person greenapps    schedule 10.12.2017
comment
Вы можете использовать Google для разрешений во время выполнения. А вот для доступа к микро сд карте они бесполезны.   -  person greenapps    schedule 10.12.2017
comment
@greenapps В API16 getExternalFilesDirs во множественном числе (Dirs) недоступен. Также недоступно разрешение во время выполнения, см. примечание 2. Любая другая идея?   -  person Basj    schedule 11.12.2017
comment
Это кажется актуальным. stackoverflow.com/questions/40068984/   -  person Parth Sane    schedule 12.12.2017
comment
Отключите передачу файлов с устройства на компьютер. Если включено, приложение не сможет получить доступ к SD-карте. Может в этом и проблема, вроде все известные способы перепробовали   -  person Rainmaker    schedule 12.12.2017
comment
@Basj, но я не вижу, какой пункт может помочь, начните пробовать каждый из принятого ответа.   -  person petey    schedule 13.12.2017
comment
@petey Многие из пунктов не применимы, потому что API выше или потому, что я пытался, и они не сработали (см. Мои примечания 1, 2, 3 и т. Д.).   -  person Basj    schedule 13.12.2017
comment
Вам нужно использовать DocumentFile. Не существует общедоступного API для получения пути к съемной SD-карте (насколько я знаю). Это должно помочь: stackoverflow.com/a/35175460/1048340 Почему вы не можете использовать внутреннее хранилище вместо съемного SD карта?   -  person Jared Rummler    schedule 13.12.2017
comment
@JaredRummler У меня более 100 ГБ на карте microSD для музыкальной библиотеки, у меня нет этого места во внутренней памяти (вероятно, только 8 или 16 ГБ во внутренней памяти). Как вы думаете, может ли DocumentFile работать с target=API16, а также с Android 5.0.2?   -  person Basj    schedule 13.12.2017
comment
@Basj Я добавил ответ на этот вопрос.   -  person Jorgesys    schedule 18.12.2017
comment
Спасибо @JaredRummler. У вас есть минимальный пример кода, который создаст хотя бы файл <EXT_SD_CARD>/books/hello.txt (а не <EXT_SD_CARD>/Android/data/...), скомпилируемый с помощью API16 (Android 4.1) и работающий на нерутированном телефоне Android 5.0?   -  person Basj    schedule 18.12.2017


Ответы (3)


имейте в виду, что некоторые устройства Android будут иметь другой путь для SD-карты, а некоторые не имеют съемной SD-карты.

Вам не нужно указывать путь напрямую!

File myDir = new File("/mnt/extSdCard/test");
myDir.mkdirs();

Сначала вы можете проверить, установлена ​​ли на вашем устройстве съемная SD-карта:

public static boolean isSDCardAvailable(Context context) {
    File[] storages = ContextCompat.getExternalFilesDirs(context, null);
    if (storages.length > 1 && storages[0] != null && storages[1] != null)
        return true;
    else
        return false;
}

Зачем получать внешние каталоги> 1, потому что большинство устройств Android имеют внешнее хранилище в качестве основного каталога и съемную SD-карту в качестве второго каталога:

представить описание изображения aquí

Но вы можете использовать метод, чтобы получить реальный путь к вашей съемной карте microSD:

public static String getRemovableSDCardPath(Context context) {
    File[] storages = ContextCompat.getExternalFilesDirs(context, null);
    if (storages.length > 1 && storages[0] != null && storages[1] != null)
        return storages[1].toString();
    else
        return "";
}

Тогда просто сделайте это:

File myDir = new File(getRemovableSDCardPath(getApplicationContext()),"test");
if(myDir.mkdirs()){
  Log.i(TAG, "Directory was succesfully create!");
}else{
  Log.i(TAG, "Error creating directory!");
}

Например, используя метод:

   String pathSDCard = getRemovableSDCardPath(getApplicationContext());

В результате у меня есть путь к моей съемной SD-карте (если бы у меня не было съемной SD-карты, мой путь был бы "", поэтому вы можете выполнить проверку, чтобы избежать создания папки):

/storage/extSdCard/Android/data/com.jorgesys.myapplication/files

Теперь создаем новую папку внутри:

    File myDir = new File(getRemovableSDCardPath(getApplicationContext()),"test");
    if(myDir.mkdirs()){
        Log.i(TAG, "Directory was succesfully create!");
    }else{
        Log.i(TAG, "Error creating directory!");
    }

теперь у меня есть созданный каталог /test:

введите здесь описание изображения

person Jorgesys    schedule 12.12.2017
comment
Пожалуйста, для -1 добавьте комментарий по причинам, я готов улучшить свои ответы, чтобы лучше помочь ОП. - person Jorgesys; 18.12.2017
comment
Спасибо за ответ, но см. примечание 2: getExternalFilesDirs() равно недоступно при использовании целевого API 16 (добавлено только на уровне API 19). Также я не хочу получать доступ к /SDcard/Android/data/com.package.example/files/..., а скорее к корневой папке: /SDcard/mp3/ (т.е. я не хочу ограничиваться /SDCard/Android/data/... подпапками). Пример использования: представьте, что у вас есть библиотека MP3 объемом 50 ГБ, вы не хотите, чтобы она была ограничена приложением X, но вы также хотите использовать ее с приложением Y или приложением Z. - person Basj; 19.12.2017
comment
Вопрос был в получении разрешений - person Umair Iqbal; 13.04.2020

Поскольку я много боролся с той же проблемой, я поделюсь своей частью. Я получил помощь от этих ресурсов, большое спасибо им:

DocumentFile Android Docs и Отсюда

Я протестировал код на 5.1.1 и 6.0.1, а не на остальных устройствах. Я не тестировал его, но он должен работать нормально.

В 5.0.2 для записи на внешнее устройство вам потребуется разрешение пользователя.

Используя приведенный ниже код и прежде чем запрашивать это разрешение, вам необходимо указать пользователю выбрать корневую SD-карту, чтобы у вас был доступ ко всему внешнему хранилищу.

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

теперь в onActivityResult сохраните UriTree, возвращаемый API, так как он понадобится вам позже.

 @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == 25&&resultCode == RESULT_OK) {
            getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    } 
    super.onActivityResult(requestCode, resultCode, data);
}

После того, как у вас есть корневой UriTree, вы можете создавать файлы или каталоги для удаления из внешнего хранилища, для чего у вас будет DocumentFile из UriTree.

Чтобы получить документ UriTree, используйте приведенный ниже код.

public static DocumentFile getDocumentFile(final File file) {
        String baseFolder = getExtSdCardFolder(file);
        String relativePath = null;

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

        try {
            String fullPath = file.getCanonicalPath();
            relativePath = fullPath.substring(baseFolder.length() + 1);
        } catch (IOException e) {
            Logger.log(e.getMessage());
            return null;
        }
        Uri treeUri = Common.getInstance().getContentResolver().getPersistedUriPermissions().get(0).getUri();

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

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

        String[] parts = relativePath.split("\\/");

        for (String part : parts) {
            DocumentFile nextDocument = document.findFile(part);
            if (nextDocument != null) {
                document = nextDocument;
            }
        }

        return document;
    }


    public static String getExtSdCardFolder(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;
    }


@TargetApi(Build.VERSION_CODES.KITKAT)
public static String[] getExtSdCardPaths() {
    List<String> paths = new ArrayList<>();
    for (File file : Common.getInstance().getExternalFilesDirs("external")) {

        if (file != null && !file.equals(Common.getInstance().getExternalFilesDir("external"))) {
            int index = file.getAbsolutePath().lastIndexOf("/Android/data");
            if (index < 0) {
                Log.w("asd", "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()]);
}

Приведенный выше код вернет вам версию DocumentFile любого файла, с помощью которой вы можете выполнить желаемую операцию.

Если вы хотите увидеть этот код в действии, посмотрите, как я использовал его в своем проект с открытым исходным кодом для изменения mp3-файлов, присутствующих во внешнем хранилище.

Надеюсь, это поможет в случае сомнений, дайте мне знать.

Забыл сказать, что недавно задавал тот же вопрос Вот этот вопрос.

Редактировать. Используя этот код, вы можете проверить, дал ли пользователь разрешение на

public static boolean hasPermission() {
    List<UriPermission> uriPermission = Common.getInstance().getContentResolver().getPersistedUriPermissions();
    return uriPermission != null && uriPermission.size() > 0;
}

если разрешение будет отозвано, UriTree не будет, поэтому вам придется снова запрашивать разрешение.

person Reyansh Mishra    schedule 18.12.2017
comment
Выглядит чудесно @ReyanshMishra, попробую как можно скорее! Два небольших вопроса: 1) как вы думаете, будет ли он скомпилирован с целевым API16 (Android 4.1) или ему обязательно понадобится более новая версия? - person Basj; 18.12.2017
comment
2) Придется ли пользователю каждый раз выбирать корневую SD-карту в диалоговом окне Intent.ACTION_OPEN_DOCUMENT_TREE? По одному после каждой перезагрузки телефона? Или раз и навсегда (при первом открытии приложения)? Еще раз спасибо! - person Basj; 18.12.2017
comment
да, я должен скомпилировать API 16, а на второй вопрос я обновлю ответ. - person Reyansh Mishra; 18.12.2017
comment
Еще раз спасибо @ReyanshMishra. Если пользователь ничего не делает, как долго будет действовать разрешение? Допустим, я даю разрешение сегодня, несколько раз перезагружаю телефон и снова открываю приложение в январе 2018 года. Нужно ли мне повторно давать разрешение или оно останется по умолчанию? - person Basj; 18.12.2017
comment
Только в двух случаях разрешение будет удалено 1. когда пользователь удаляет SD-карту 2. при использовании очищает кеш и данные приложения из настроек приложения. - person Reyansh Mishra; 19.12.2017
comment
Тот, кто голосует против без объяснения причин, на самом деле не помогает сообществу. Пожалуйста, будьте хорошим членом сообщества и объясните отрицательные голоса. Спасибо - person Reyansh Mishra; 19.12.2017
comment
Давайте продолжим обсуждение в чате. - person Reyansh Mishra; 19.12.2017
comment
Еще раз спасибо за ваш ответ, я проверю это подробно. (PS: я не минусовал, конечно, я проголосовал) - person Basj; 19.12.2017
comment
Поскольку вы дали мне баллы, я предполагаю, что это сработало и должно. Для получения более подробной информации вы всегда можете использовать мой код библиотеки с открытым исходным кодом, который я указал в ответе, и я говорю о людях, которые просто так голосуют без объяснения причин. - person Reyansh Mishra; 19.12.2017

На основе ответа Doomsknight и моего и Дэйв Смит и Сообщения в блоге Марка Мерфи: 1, 2, 3:

  1. В идеале используйте Storage Access Framework и DocumentFile, как указал Джаред Раммлер. Или:
  2. Используйте путь к вашему приложению/storage/extSdCard/Android/data/com.myapp.example/files.
  3. Добавьте разрешение на чтение/запись для манифеста для pre-KitKat, позже разрешение не требуется для этого пути.
  4. Попробуйте использовать каталог point2 и методы Doomsknight с учетом Кейс KitKat и Samsung.
  5. Отфильтруйте и используйте getStorageDirectories путь точки 2 и разрешения на чтение/запись до KitKat.
  6. ContextCompat.getExternalFilesDirs, поскольку KitKat запоминает Samsung сначала возвращает внутренние:

Каталог данных Android Write My Package подходит для основного и дополнительного хранилищ:

В KitKat полное владение каталогами данных для конкретного приложения предоставляется уникальному идентификатору пользователя приложения. Это означает, что в дальнейшем приложению не требуется разрешение на чтение/запись в определенные каталоги на внешнем хранилище...

Разрешение android.permission.WRITE_EXTERNAL_STORAGE теперь предоставляет членство в sdcard_r И sdcard_rw...

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

ПРИМЕЧАНИЕ. В списках ls -la /mnt/extSdCard/... группа sdcard_r имеет полные разрешения +rwx, что, очевидно, не соответствует действительности... поскольку демон FUSE является активным участником изменения разрешений, применяемых к приложениям во время выполнения.

Samsung: пример использования

В Android 4.3 Samsung эмулировала основной внешний объем хранилища на внутренней флэш-памяти устройства (НЕ на съемной SD-карте). SD-карта всегда внутренне помечалась как дополнительный внешний носитель информации...

В Android 4.4 основной внешний объем хранения ПО-ПРЕЖНЕМУ находится на внутренней флэш-памяти.

Samsung решила включить слот для SD-карты, но не отметить его как основной внешний носитель информации.


Обновление:

Как объяснено здесь и отвечает на ваш комментарий об использовании корневого пути для обмена файлами:

Вы можете до Kikat использовать метод Doomsknight 1 и if/else кода в зависимости от целевой версии или создание нескольких APK.

Начиная с KitKat, сторонние приложения просто не могут добавлять собственные файлы в случайные места...

Почему сейчас?

Ответ в акрониме: CTS.

Правила, гласящие, что вторичные тома хранилища не должны быть доступны для записи приложениям, также были в документе с версии 4.2.

Однако были добавлены новые тесты. в CTS для 4.4, которые проверяют, имеет ли вторичное хранилище надлежащие разрешения только для чтения в каталогах, не относящихся к приложениям, предположительно из-за новых API, которые наконец раскрывают эти пути разработчикам приложений. Как только CTS включит эти правила, OEM-производители должны будут поддерживать их, чтобы продолжать поставки устройств с GMS (Google Play и т. д.) на борту.

Как насчет обмена файлами?

Это правильный вопрос. Что делать, если приложению необходимо предоставить общий доступ к файлам, которые оно создало на дополнительном внешнем томе хранения? Ответ Google, по-видимому, заключается в том, что те приложения, которые активно решают выйти за рамки основного внешнего хранилища для записи контента, также должны предоставить безопасный способ обмена им, либо с помощью поставщик контента или новая платформа доступа к хранилищу.

Как я объяснил по вопросу, который вы запросили у меня за помощью, я никогда не пробовал это, но мое мнение таково, что теоретически вы можете использовать корневой путь до KitKat, используя метод 1, а эти альтернативы позже.

person albodelu    schedule 13.12.2017
comment
Большое спасибо @albodelu. В дополнение ко всем ссылкам (я немного потерялся, потому что не знаю, какую именно версию использовать), чтобы сделать его полным ответом, не могли бы вы привести минимальный пример кода, как заставить это работать: минимальный код что бы создать файл <EXT_SDCARD>/books/hello.txt? (это будет компилироваться с целевым API 16 = Android 4.1 и будет работать на нерутированном телефоне Android 5.0). Заранее спасибо миллиард раз (а может и больше, потому что столько времени потратил безуспешно, как и деньги других людей наверное :) ) - person Basj; 18.12.2017
comment
Добро пожаловать. Идея состоит в том, чтобы использовать этот код заменяет ifsection на ContextCompat.getExternalFilesDirsи else по методу Doomsknight1 или 5. Теоретически это будет работать на API 16. Мои старые устройства рутированы, чтобы обновить их до KitKat, я не могу попробовать. Если вы дадите ссылку на демонстрационный/тестовый проект в Github, я смогу помочь вам на следующей неделе. В любом случае, общие ссылки уже содержат соответствующий код. Что касается пути, я ожидаю, что другие разработчики будут уважать ограничения Kitkat. - person albodelu; 19.12.2017
comment
Причина замены раздела else заключается в том, что устройства Samsung возвращают неправильный путь, используя getExternalFilesDir. Вам действительно не нужно использовать корневой путь, это /storage/extSdCard/Android/data/com.myapp.example/filesрешает вашу проблему с пространством. - person albodelu; 19.12.2017
comment
Нет, @albodelu, /storage/extSdCard/Android/data/com.myapp.example/files не решает проблему: представьте себе библиотеку MP3 объемом 50 ГБ на вашей карте microSD. Вы хотите, чтобы MP3Player приложение имело к нему доступ, а также другое приложение MP3_DJ, чтобы оно также имело к нему доступ. Таким образом, папка не может быть привязана к приложению. Вот почему я хотел получить доступ (чтение/запись) к корню карты micro SD /sdcard/mp3/, например. Прежде чем я проведу множество тестов, относящихся ко всем предоставленным вами ссылкам, скажите, работает ли ваше решение для корневых папок или нет? - person Basj; 19.12.2017
comment
На этот вопрос уже есть два ответа с теорией и кодом об альтернативе SAF. - person albodelu; 19.12.2017
comment
ContextCompat.getExternalFilesDirs [...] Теоретически это будет работать на API 16: Кажется, он начинается с API 19 (добавлен на уровне API 19) - person Basj; 19.12.2017
comment
Я думал, что это сработает @albodelu, но, к сожалению, я вижу, что упомянутый вами ответ не работает в KitKat: Is this available on KitKat? No, we can't retroactively add new functionality to older versions of the platform. так что это вероятно, не будет работать и для API 16. - person Basj; 19.12.2017
comment
Давайте продолжим это обсуждение в чате. - person Basj; 19.12.2017