Scoped storage
文件存储介绍了内部存储和外部存储相关的内容。因为外部存储容易读写,所以在手机中经常看到很多“乱七八糟”的文件或文件夹,这些就是应用肆意创建的。
Android Q(10)开始添加了scoped storage的功能,更好的限制了应用访问外部存储。
先见个例子,下面代码运行在Android Q上会有什么现象呢:
AndroidManifest.xml中权限声明:
执行代码:
File[] externalFiles = context.getExternalFilesDirs( null ); for (File file : externalFiles) { try { File fileA = new File( file, "aaaa.txt" ); FileOutputStream fosA = new FileOutputStream( fileA ); fosA.close(); File fileB = new File( file.getParentFile().getParentFile().getParentFile().getParentFile(), "bbbb.txt" ); Log.d( TAG, "fileA="+fileA+";\nfileB="+fileB); FileOutputStream fosB = new FileOutputStream( fileB ); fosB.close(); } catch (IOException e) { Log.d( TAG, "exception: "+e.getMessage() ); e.printStackTrace(); } }
执行的结果:
log的结果如下,实际与log是符合的。上述代码在四个 位置各创建一个文件,2个创建成功了2个fail了。/storage/emulated/0/和/storage/3B80-111D/下创建失败,提示权限问题。
1 2 3 4 5 6 |
|
scoped storage在Android 10及更高版本默认开启。若之前的应用不满足这一功能,而运行在Android 10上 则需要将下面的属性设置成true, 关闭这一功能。
1 |
|
上述代码修改后就能执行完成了。
注:后续版本可能强制要求开启scoped storage功能,上述关闭属性方法可能只是一个过渡。
以前的读写外部存储的权限:READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE。只要设置了这个就能够很容易的读写外部存储上的文件。
当scoped storage功能添加后,对权限和路径 具体有如下表格:
File location | Permissions needed | Method of accessing (*) | Files removed when app uninstalled? |
---|---|---|---|
App-specific directory | None | getExternalFilesDir() |
Yes |
Media collections (photos, videos, audio) |
READ_EXTERNAL_STORAGE only when accessing other apps' files |
MediaStore |
No |
Downloads (documents and e-books) |
None | Storage Access Framework (loads system's file picker) |
No |
从上表中看到,只是访问其他应用的媒体文件 才需要READ_EXTERNAL_STORAGE权限,其他访问方式都不需要任何权限。(Storage Access Framework不了解可以点击链接 了解下)
开启scoped storage后,访问自身应用创建的文件都不需要任何权限(不管文件时创建在内部存储还是外部存储中)。而访问其他应用创建的文件,需要满足:
1.需要READ_EXTERNAL_STORAGE权限;
2.该文件需要在下列某个媒体集合中:
照片:存储在 MediaStore.Images 中。(image/*)
视频:存储在 MediaStore.Video 中。(video/*)
音乐文件:存储在 MediaStore.Audio 中。(audio/*)
为了访问另一应用创建的文件(包括“downloads”目录下的文件),您的应用必须使用存储访问框架(Storage Access Framework),用户可以通过该框架选择特定文件。
注意:使用scoped storage的应用无法直接访问类似 sdcard/DCIM/IMG1024.JPG 的路径。 要访问此类文件,必须使用MediaStore,并调用openFile()之类的方法。
scoped storage还添加了媒体相关数据限制:
除非您的应用已获得 ACCESS_MEDIA_LOCATION 权限,否则图片文件中的 Exif 元数据会被删除。
MediaStore.Files 表已经过滤,仅显示照片、视频和音频文件。例如,该表格不会再显示 PDF 文件。(下面媒体文件部分也说到的)
媒体文件
MediaStore提供api接口 来访问下面定义良好的的媒体文件:
照片:存储在 MediaStore.Images(image/*) 中。
视频:存储在 MediaStore.Video(video/*) 中。
音频:存储在 MediaStore.Audio(audio/*) 中。
MediaStore.Files包含了所有media类型的文件集合。如果使用了scoped storage,则 MediaStore.Files仅仅包含上面3个类型(Images,Video,Audio)。
访问媒体文件
加载媒体文件,调用ContentResolver的方法:
如下面一段代码 已Images为例,手机中只有拍摄的两张图片。
ContentResolver contentResolver = this.getContentResolver(); Uri imgUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; Uri firstImgUri = null; Cursor cursor = contentResolver.query( imgUri, null, null, null ); if (cursor != null && cursor.getCount() > 0) { cursor.moveToFirst(); firstImgUri = Uri.fromFile( new File( cursor.getString( cursor.getColumnIndex( MediaStore.Images.Media.DATA ) ) ) ); do { Log.d( TAG, "img cursor data=" + cursor.getString( cursor.getColumnIndex( MediaStore.Images.Media.DATA ) ) +";\nimg cursor type=" + cursor.getString( cursor.getColumnIndex( MediaStore.Images.Media.MIME_TYPE ) )); } while (cursor.moveToNext()); } Log.d( TAG, "firstImgUri="+firstImgUri ); try { ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor( firstImgUri, "r" ); if (Build.VERSION.SDK_INT >= 29) { Bitmap bitmap = contentResolver.loadThumbnail( firstImgUri, new Size( 200,200 ), null ); } } catch (Exception e) { Log.d( TAG, "exception: "+e.getMessage() ); }
执行后的结果:
1 2 3 4 5 |
|
IS_PENDING独占
Android 10以后,当写入磁盘时 应用可以通过IS_PENDING标志实现对媒体文件的独占访问。
如:
ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, "TEST.jpg"); values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); values.put(MediaStore.Images.Media.IS_PENDING, 1); Uri collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); Uri item = contentResolver.insert(collection, values); try (ParcelFileDescriptor pfd = contentResolver.openFileDescriptor(item, "w", null)) { Parcel out = Parcel.obtain(); pfd.writeToParcel( out, Parcelable.PARCELABLE_WRITE_RETURN_VALUE ); } catch (IOException e) { Log.d( TAG, "e:"+e.getMessage() ); } values.clear(); values.put(MediaStore.Images.Media.IS_PENDING, 0);//释放,使其他应用可以访问 contentResolver.update(item, values, null, null);
善始者实繁,克终者盖寡