Mkdir() работает во внутренней памяти флеш-памяти, но не SD-карта?

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

Одним из важнейших требований этого приложения является предоставление пользователю возможности создавать новые папки в любом месте. Функция, подобная этой, была бы очень полезной для приложения. Однако метод File#mkdir() вообще не работает в каталоге SD-карты.

Я добавил соответствующие разрешения для файла манифеста. Я также написал тест, чтобы узнать, какие каталоги (все из которых существуют на моем устройстве Lollipop 5.0) позволяют создать новую папку. Из моих наблюдений File#mkdir() работает только во внутреннем каталоге флэш-памяти.

Примечание: не путайте Environment#getExternalStorageDirectory() с местоположением SD-карты, как объясняется в этой статье. Также на Lollipop 5.0 я считаю, что /storage/emulated/0/ и /storage/sdcard0/ относятся к внутренней флэш-памяти, а /storage/emulated/1/ и /storage/sdcard1/ относятся к SD-карте (что, по крайней мере, верно для устройства, с которым я тестирую).

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


манифеста:

...
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...

Тест:

...
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final String NEW_FOLDER_NAME = "TestFolder";
        testPath(new File(Environment.getExternalStorageDirectory(), NEW_FOLDER_NAME));
        testPath(new File("/storage/emulated/0/", NEW_FOLDER_NAME));
        testPath(new File("/storage/emulated/1/", NEW_FOLDER_NAME));
        testPath(new File("/storage/sdcard0/Download/", NEW_FOLDER_NAME));
        testPath(new File("/storage/sdcard1/Pictures/", NEW_FOLDER_NAME));
    }

    private void testPath(File path) {
        String TAG = "Debug.MainActivity.java";
        String FOLDER_CREATION_SUCCESS = " mkdir() success: ";

        boolean success = path.mkdir();
        Log.d(TAG, path.getAbsolutePath() + FOLDER_CREATION_SUCCESS + success);
        path.delete();
    }
}

Вывод:

/storage/emulated/0/TestFolder mkdir() success: true
/storage/emulated/0/TestFolder mkdir() success: true
/storage/emulated/1/TestFolder mkdir() success: false
/storage/sdcard0/Download/TestFolder mkdir() success: true
/storage/sdcard1/Pictures/TestFolder mkdir() success: false

Ответ 1

Во-первых, вы должны заметить, что file.mkdir() и file.mkdirs() возвращает false, если каталог уже существует. Если вы хотите узнать, существует ли каталог при возврате, используйте (file.mkdir() || file.isDirectory()) или просто игнорируйте возвращаемое значение и вызывайте file.isDirectory() (см. Документацию).

Тем не менее, ваша настоящая проблема в том, что вам нужно разрешение на создание каталога на съемном носителе на Android 5.0+. Работа со съемными SD-картами на Android ужасна.

В Android 4.4 (KitKat) ограниченный доступ к SD-картам Google (см. здесь, здесь, и здесь). См. Этот fooobar.com/questions/59969/..., который приводит к этому сообщению XDA, если вам нужно создать на съемной SD-карте на Android 4.4 (KitKat).

В Android 5.0 (Lollipop) Google представил новые API доступа к карте SD. Для использования примера см. Этот fooobar.com/questions/59969/....

В принципе, для создания каталога вам нужно использовать DocumentFile#createDirectory(String displayName). Перед созданием этого каталога вам необходимо попросить пользователя предоставить разрешения для вашего приложения.


ПРИМЕЧАНИЕ.. Это относится к съемному хранилищу. Использование File#mkdirs() будет работать на внутреннем хранилище (которое часто путают с внешним хранилищем на Android), если у вас есть разрешение android.permission.WRITE_EXTERNAL_STORAGE.


Я отправлю код примера ниже:

Проверьте, нужно ли запрашивать разрешение:

File sdcard = ... // the removable SD card
List<UriPermission> permissions = context.getContentResolver().getPersistedUriPermissions();
DocumentFile documentFile = null;
boolean needPermissions = true;

for (UriPermission permission : permissions) {
  if (permission.isWritePermission()) {
    documentFile = DocumentFile.fromTreeUri(context, permission.getUri());
    if (documentFile != null) {
      if (documentFile.lastModified() == sdcard.lastModified()) {
        needPermissions = false;
        break;
      }
    }
  }
}

Далее (если needPermissions - true), вы можете отобразить диалоговое окно, чтобы объяснить пользователю, что им нужно выбрать "SD-карту", ​​чтобы дать вашим приложениям права создавать файлы/каталоги, а затем запустить следующее активность:

if (needPermissions) {
  // show a dialog explaining that you need permission to create the directory
  // here, we will just launch to chooser (what you need to do after showing the dialog)
  startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), STORAGE_REQUEST_CODE);
} else {
  // we already have permission to write to the removable SD card
  // use DocumentFile#createDirectory
}

Теперь вам нужно проверить resultCode и requestCode на onActivityResult:

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  if (requestCode == STORAGE_REQUEST_CODE && resultCode == RESULT_OK) {
    File sdcard = ... // get the removable SD card

    boolean needPermissions = true;
    DocumentFile documentFile = DocumentFile.fromTreeUri(MainActivity.this, data.getData());
    if (documentFile != null) {
      if (documentFile.lastModified() == sdcard.lastModified()) {
        needPermissions = false;
      }
    }

    if (needPermissions) {
      // The user didn't select the "SD Card".
      // You should try the process over again or do something else.
    } else {
      // remember this permission grant so we don't need to ask again.
      getContentResolver().takePersistableUriPermission(data.getData(),
          Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
      // Now we can work with DocumentFile and create our directory
      DocumentFile doc = DocumentFile.fromTreeUri(this, data.getData());
      // do stuff...
    }
    return;
  }
  super.onActivityResult(requestCode, resultCode, data);
}

Это должно дать вам хорошее начало работы с DocumentFile и съемными SD-картами на Android 5.0+. Это может быть PITA.


Кроме того, нет никакого открытого API, чтобы получить путь к съемной SD-карте (если она существует). Вы не должны полагаться на hardcoding "/storage/sdcard1"! В StackOverflow есть несколько сообщений об этом. Во многих решениях используется переменная среды SECONDARY_STORAGE. Ниже приведены два метода, которые можно использовать для поиска съемных устройств хранения данных:

public static List<File> getRemovabeStorages(Context context) throws Exception {
  List<File> storages = new ArrayList<>();

  Method getService = Class.forName("android.os.ServiceManager")
      .getDeclaredMethod("getService", String.class);
  if (!getService.isAccessible()) getService.setAccessible(true);
  IBinder service = (IBinder) getService.invoke(null, "mount");

  Method asInterface = Class.forName("android.os.storage.IMountService$Stub")
      .getDeclaredMethod("asInterface", IBinder.class);
  if (!asInterface.isAccessible()) asInterface.setAccessible(true);
  Object mountService = asInterface.invoke(null, service);

  Object[] storageVolumes;
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    String packageName = context.getPackageName();
    int uid = context.getPackageManager().getPackageInfo(packageName, 0).applicationInfo.uid;
    Method getVolumeList = mountService.getClass().getDeclaredMethod(
        "getVolumeList", int.class, String.class, int.class);
    if (!getVolumeList.isAccessible()) getVolumeList.setAccessible(true);
    storageVolumes = (Object[]) getVolumeList.invoke(mountService, uid, packageName, 0);
  } else {
    Method getVolumeList = mountService.getClass().getDeclaredMethod("getVolumeList");
    if (!getVolumeList.isAccessible()) getVolumeList.setAccessible(true);
    storageVolumes = (Object[]) getVolumeList.invoke(mountService, (Object[]) null);
  }

  for (Object storageVolume : storageVolumes) {
    Class<?> cls = storageVolume.getClass();
    Method isRemovable = cls.getDeclaredMethod("isRemovable");
    if (!isRemovable.isAccessible()) isRemovable.setAccessible(true);
    if ((boolean) isRemovable.invoke(storageVolume, (Object[]) null)) {
      Method getState = cls.getDeclaredMethod("getState");
      if (!getState.isAccessible()) getState.setAccessible(true);
      String state = (String) getState.invoke(storageVolume, (Object[]) null);
      if (state.equals("mounted")) {
        Method getPath = cls.getDeclaredMethod("getPath");
        if (!getPath.isAccessible()) getPath.setAccessible(true);
        String path = (String) getPath.invoke(storageVolume, (Object[]) null);
        storages.add(new File(path));
      }
    }
  }

  return storages;
}

public static File getRemovabeStorageDir(Context context) {
  try {
    List<File> storages = getRemovabeStorages(context);
    if (!storages.isEmpty()) {
      return storages.get(0);
    }
  } catch (Exception ignored) {
  }
  final String SECONDARY_STORAGE = System.getenv("SECONDARY_STORAGE");
  if (SECONDARY_STORAGE != null) {
    return new File(SECONDARY_STORAGE.split(":")[0]);
  }
  return null;
}

Ответ 2

path.mkdir() также не работает, когда каталог уже существует. Вы можете сначала добавить чек:

if (!path.exists()) {
   boolean success = path.mkdir();
   Log.d(TAG, path.getAbsolutePath() + FOLDER_CREATION_SUCCESS + success);
   path.delete();
} else {
   Log.d(TAG, path.getAbsolutePath() + "already exists");
}

Ответ 3

на Kitkat google ограниченный доступ к внешней SD-карте, поэтому вы не сможете писать во внешнее хранилище на Kitkat.

В Lollipop Google создала новую FrameWork для записи данных на внешнее хранилище, и вам нужно использовать новый DocumentFile

который имеет обратную совместимость.

В принципе, вы можете запросить разрешение на onstart приложения в корневой каталог приложения, а затем вы можете создать каталог

Ответ 4

Попробуйте с этим. Это отлично работает для меня.

final String NEW_FOLDER_NAME = "TestFolder";

String extStore = System.getenv("EXTERNAL_STORAGE");
File f_exts = new File(extStore, NEW_FOLDER_NAME);

String secStore = System.getenv("SECONDARY_STORAGE");
File f_secs = new File(secStore, NEW_FOLDER_NAME);

testPath(f_exts);

textPath(f_secs);

и изменить логическое значение в функции testPath следующим образом

boolean success;
if(path.exists()) {
    // already created
    success = true;
} else {
    success = path.mkdir();
}

Если папка уже существует, метод path.mkdir() возвращает false.

и сделано.!!!

ссылку из этого вопроса.