mkdir()在内部闪存存储器内工作,但不是SD卡吗?

时间:2016-01-31 01:28:07

标签: android file-permissions

我目前正在构建一个文件管理应用程序,允许用户浏览其设备的文件系统。用户可以在设备的根目录/中启动,但可以浏览到他们想要的任何位置,例如内部闪存或SD卡。

此应用程序的一个关键要求是允许用户在任何地方创建新文件夹。像这样的功能对应用程序非常有用。但是,File#mkdir()方法在SD卡目录中根本不起作用。

我为清单文件添加了适当的权限。我还写了一个测试,看看哪些目录(我的Lollipop 5.0设备上都存在)允许创建一个新文件夹。根据我的观察,File#mkdir()仅在内部闪存目录中有效。

注意:请不要将Environment#getExternalStorageDirectory()与SD卡位置混淆,如this article所述。另外在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

4 个答案:

答案 0 :(得分:23)

首先,您应该注意,如果目录已存在,file.mkdir()file.mkdirs()将返回false。如果您想知道返回时该目录是否存在,请使用(file.mkdir() || file.isDirectory())或者只是忽略返回值并调用file.isDirectory()(参见文档)。

那就是说,你真正的问题是你需要在Android 5.0+上的可移动存储上创建目录的权限。在Android上使用可移动SD卡是可怕的。

在Android 4.4(KitKat)上,Google限制访问SD卡(请参阅hereherehere)。如果您需要在Android 4.4(KitKat)上的可移动SD卡上创建目录,请参阅此StackOverflow answer,这将导致此XDA post

在Android 5.0(Lollipop)上,谷歌推出了新的SD卡访问API。有关样本用法,请参阅此stackoverflow answer

基本上,您需要使用DocumentFile#createDirectory(String displayName)来创建目录。在创建此目录之前,您需要让用户向您的应用授予权限。

注意:这适用于可移动存储。如果您拥有File#mkdirs()权限,则使用android.permission.WRITE_EXTERNAL_STORAGE可以处理内部存储(通常与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),您可以显示一个对话框,向用户解释他们需要选择&#34; SD卡&#34;为您的应用程序授予创建文件/目录的权限,然后启动以下活动:

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卡提供良好的开端。它可以是PITA。

此外,没有公共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;
}

答案 1 :(得分: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 :(得分:2)

在Kitkat谷歌限制访问外部SD卡,所以你将无法写入Kitkat上的外部存储。

在Lollipop中谷歌制作了一个新的FrameWork来将数据写入外部存储你必须使用新的DocumentFile

向后兼容的类。

基本上你可以在应用程序的onstart上向应用程序的根目录请求权限,然后你可以创建目录

答案 3 :(得分: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问题的参考。