在内部闪存中使用mkdir()函数可以正常工作,但在SD卡上却不行?

32

我正在构建一个文件管理应用,允许用户浏览其设备的文件系统。用户从设备的根目录/开始,但可以浏览到任何他们想要的位置,例如内部闪存存储或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卡(至少对于我正在测试的设备来说是这样)。

如何在非root Android设备上创建外部存储路径以外的文件夹和文件?


清单:

...
<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

你不能假设 /storage/sdcard 或 /storage/emulated 映射到任何内容。OEM 可以将其重命名为任何他们想要的名称。 - Gabe Sechan
2
SD卡已挂载或只读? - DominicEU
@Gabe Sechan 我知道这一点。我的应用程序实际上并不假设SD卡和内部闪存驱动器的位置,但它会加载“/storage/”目录,以便用户可以选择他们想要的挂载点。 - zxgear
@DominicEU 不确定。我正在一台运行Lollipop 5.0的物理设备上测试代码,插入了一个16GB的SD卡。 - zxgear
2
这可能与编程有关:http://www.androidcentral.com/lollipop-brings-changes-way-your-sd-card-works-kind-youll - Khaled.K
显示剩余2条评论
4个回答

24
首先,您应该注意到file.mkdir()file.mkdirs()如果目录已经存在,则返回false。如果您想在返回时知道目录是否存在,请使用(file.mkdir() || file.isDirectory())或者简单地忽略返回值并调用file.isDirectory()(请参阅文档)。
话虽如此,您真正的问题是需要在Android 5.0+上获得在可移动存储中创建目录的权限。在Android上使用可移动SD卡是非常困难的。
在Android 4.4(KitKat)中,Google限制了对SD卡的访问(参见这里这里这里)。如果需要在Android 4.4(KitKat)上创建可移动SD卡上的目录,请参阅此StackOverflow答案,该答案将导向此XDA帖子
在Android 5.0(Lollipop)中,Google引入了新的SD卡访问API。有关示例用法,请参阅此stackoverflow答案
基本上,你需要使用DocumentFile#createDirectory(String displayName)来创建你的目录。在创建此目录之前,你需要要求用户授予你的应用程序权限。

注意: 这是针对可移动存储的。如果您拥有权限android.permission.WRITE_EXTERNAL_STORAGE,则使用File#mkdirs()将适用于内部存储(在Android上经常与外部存储混淆)。


下面我将发布一些示例代码:

检查是否需要请求权限:

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

下一步(如果needPermissionstrue),您可以显示一个对话框,向用户解释他们需要选择“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
}

您现在需要在 onActivityResult 中检查 resultCoderequestCode

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

这应该为您在 Android 5.0+ 上使用 DocumentFile 和可移动 SD 卡提供了一个良好的起点。但这可能有点麻烦。


此外,没有公共API可以获取可移动SD卡的路径(如果存在)。您不应该依赖硬编码“/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;
}

感谢提供有用的链接,我已经全部阅读了。由于所需权限具有递归性质(据我理解),是否可以显示一个对话框,显式地要求许可来处理“/”而无需用户指定路径? - zxgear
这个解决方案有些凌乱,但是它能够在我的SD卡上创建一个新文件夹。如果可能的话,最好不要让用户被发送到一个新的活动中。 - zxgear
2
您只需要对每个存储设备请求一次权限。由于明显的安全原因,它将无法使用根目录("/"),但应用于任何可移动存储。通过一些工作,您可以将逻辑从活动类中解耦。但是,仍需检查 "onActivityResult"。 - Jared Rummler
没问题,我只需要SD卡访问权就很满意了。 - zxgear
在Android上使用可移动SD卡是可怕的。- 同意,+1。本质上现在我必须复制我的代码,一个用于File,一个用于DocumentFile - LWChris
显示剩余2条评论

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

2
在KitKat中,Google限制了对外部SD卡的访问,因此您将无法在KitKat上写入外部存储。
在Lollipop中,Google创建了一个新的框架来写入数据到外部存储,您必须使用新的DocumentFile类,该类向后兼容。
基本上,您可以在应用程序启动时请求权限到应用程序的根目录,然后可以创建目录。

1

用这个试试。对我来说很好用。

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。
完成了!!!
参考自 this 问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接