版本说明
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) {
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) {
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保存到配置中方便后期直接调用
public static String getUriTree(){
return sharedPreferences.getString("uriTree", "");
}
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();
}
}
}
取得文件列表
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);
}
|