版本说明

Android 6 SDK23

之前访问文件列表无需进行权限申请,或者只需在AndroidManifest.xml中添加相应权限即可进行
从23之后如果访问文件列表需要在Activity中动态申请访问权限
比较好的方案是和权限检查放在一起,即检查了权限,又相于做了动态权限申请

Android 7

在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。
通过FileProvider在应用间共享文件
FileProvider实际上是ContentProvider的一个子类,它的作用也比较明显了,file:///Uri不给用,那么换个Uri为content://来替代。

Android 10 SDK 29

Android 10增加了文件分区的功能,文件的访问特别是根目录下的访问受限,Android10不再允许直接读取文件根目录,即使动态申请也无效。
但为了过度,可以使用临时的方案,即在application节点下增加android:requestLegacyExternalStorage="true"可关闭文件分区功能,但可能后期被取消。

在Android29之后,不再允许访问根目录,此时调用 listFiles() 方法,将得到一个 null 值

AndroidQ 为每个应用程序在外部存储设备提供了一个独立的存储沙箱,应用直接通过文件路径保存的文件都会保存在应用的沙箱目录,应用卸载的时候默认所有数据都会被删除
如果不希望应用卸载删除的文件,需要应用通过 MdeiaProvider 或者 SAF 方式保存在公共共享集体目录如多媒体文件集合音视频图片等和下载文件集合等

Android 11 SDK 30

强制开启文件分区功能,即使加上了10上的关闭标识也会忽略掉
此时如果再想要访问文件目录,如文件管理器等,需要申请11新增的权限android.permission.MANAGE_EXTERNAL_STORAGE

权限检查与动态权限申请

AndroidManifest.xml

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

<application android:requestLegacyExternalStorage="true"></application>

应用启动时进行权限检查

@Override
protected void onCreateActivity() {
    //...
    verifyPermission();
}

private void verifyPermission() {
    try {
        //定义要检查的权限,如:读写目录权限等
        int permission = ActivityCompat.checkSelfPermission(this, "android.permission.READ_EXTERNAL_STORAGE");
        if (permission != PackageManager.PERMISSION_GRANTED) {
            //如果权限不足,则打开权限申请弹窗。参数中的100为onRequestPermissionsResult回调码,自定义即可。
            ActivityCompat.requestPermissions(this, new String[]{"android.permission.READ_EXTERNAL_STORAGE"}, 100);
        }
    } catch (Exception e) {
        showDialogError(e.getMessage());
    }
}

动态权限申请

Android26以后需要动态申请权限

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
}

在回调事件中添加权限通过后的相关功能

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    //super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == 100) { //判断回调码
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            //权限通过后执行的功能事件...
            //如:创建目录等
        }
    }
}

MediaStore

Android10开启分区存储后,将无法再使用File类来操作文件,只能通过MediaStore来进行多媒体文件的增删改查。

  • MediaStroe.Audio.Media.EXTERNAL_CONTENT_URI:存储在外部存储器上的音频文件内容;
  • MediaStroe.Audio.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的音频文件内容.
  • MediaStore.Images.Media.EXTERNAL_CONTENT_URI:存储在外部存储器上的图片文件内容.
  • MediaStore.Images.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的图片文件内容.
  • MediaStore.Video.Media.EXTERNAL_CONTENT_URI:存储在外部存储器上的视频文件内容.
  • MediaStore.Video.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的视频文件内容

取得图库相册

String[] projection = new String[]{MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
String sortOrder = MediaStore.MediaColumns.DATE_ADDED + " desc";
Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = getContentResolver().query(imageUri, projection, null, null, sortOrder);
if (cursor != null && cursor.getCount() > 0) {
    lstAlbums.clear();
    while (cursor.moveToNext()) {
        String id = cursor.getString(cursor.getColumnIndexOrThrow(projection[0]));
        String name = cursor.getString(cursor.getColumnIndex(projection[1]));
        lstAlbums.add(new DirListData(id, name));
    }
    cursor.close();
}

SAF 存储访问框架

Android 4.4(API 级别 19)引入了存储访问框架 (SAF:Storage Access Framework)。
借助 SAF,用户可轻松浏览和打开各种文档、图片及其他文件,而不用管这些文件来自其首选文档存储提供程序中的哪一个。
用户可通过易用的标准界面,跨所有应用和提供程序以统一的方式浏览文件并访问最近用过的文件。

相较与上面的MediaStore这个可以针对文件目录结构进行操作,所以在具体的案例中,如果需要进行指定目录读取的建议使用这个SAF来替代之前的File.listFiles()
如果是获取最近图片文件或者视频等文件的,可以使用上面的MediaStore不用指定具体目录,因为正常媒体文件都是存放在公共共享目录中的。按需取用。

Uri

获取授权后得到的路径Uri
前面是根目录字串,后面通过%2F当作目录分隔符
但在实际开发中发现直接访写出的uri进行DocumentFile转化得到的是个null,最好的方案是取得根目录的访问权限后通过findFile方式查询子目录并进行操作

content://com.android.externalstorage.documents/tree/0E03-3E0B%3A 'SDCard根目录'
content://com.android.externalstorage.documents/tree/0E03-3E0B%3ADCIM%2FCamera 'DCIM/Camera/'
content://com.android.externalstorage.documents/tree/0E03-3E0B%3A0DOC%2FMyData%2Fdb '0DOC/MyData/db'

注意事项

  • File可以通过当前文件获取父文件,然后再获取同级文件。但是SAF就不能这样,只能获取到授权的文件夹下面的文件。获取父文件夹的话都是null
  • SAF没有经过授权的话,无法获取到该文文件夹。哪怕知道具体的uri
  • SAF授权过的文件夹、,即使把程序卸载再装上,哪怕没有再次授权依然可以通过uri获取到该文件
  • SAF在11.0及其以下可以获取到存储卡中的根目录,但是12.0的话无法获取到外置存储卡的根目录

OPEN_DOCUMENT_TREE

取得目录的访问权限
将获取到权限后的目录Uri保存到配置中方便后期直接调用

// 自定义的MyApplication中返回储存的Uri
public static String getUriTree(){
    return sharedPreferences.getString("uriTree", "");
}
//region 取得根目录权限
private void verifyPermission() {
    if (TextUtils.isEmpty(MyApplication.getUriTree())) {
        // 不存在则重新授权
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
        startActivityForResult(intent, 100);
    } else {
        // 权限通过执行相关查询等操作
        // ...
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == 100 && resultCode == RESULT_OK) {
        Uri uriTree = null;
        if (data != null)
            uriTree = data.getData();
        if (uriTree != null) {
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putString("uriTree", uriTree.toString());
            editor.apply();
            // 权限通过执行相关查询等操作
            // ...
        }
    }
}
//endregion 取得根目录权限

取得文件列表

public void getRecentFile(Activity activity, GetRecentFileCallback callback) {
	DocumentFile documentFile;
	int flags = activity.getIntent().getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
	try {
	    activity.getContentResolver().takePersistableUriPermission(MyApplication.getUriTree(), flags);
	    documentFile = DocumentFile.fromTreeUri(activity, MyApplication.getUriTree());
	} catch (SecurityException e) {
	    
	}
	if(documentFile == null) return;
    List<RecentFileBean> lst = new ArrayList<>();
    DocumentFile camera = documentFile.findFile("DCIM").findFile("Camera");
    if (camera == null) {
        callback.onError("路径访问失败", 1);
        return;
    }
    DocumentFile[] subFiles = camera.listFiles();
    if (subFiles.length > 0) {
        for (DocumentFile file : subFiles) {
            if (file.isFile()) {
                String fileType = PubUtil.EXPLORER_IMAGE;
                String minetype = file.getType();
                if (minetype != null) {
                    if (minetype.contains("video")) fileType = PubUtil.EXPLORER_VIDEO;
                    else if (minetype.contains("audio")) fileType = PubUtil.EXPLORER_AUDIO;
                }
                lst.add(new RecentFileBean(file.getName(), file, file.lastModified(), fileType));
            }
        }
        Collections.sort(lst);
    }
    callback.onSuccess(lst);
}