Delphi处理Android 11分区存储Scoped Storage
1、何谓“分区存储”
为了让用户更好地控制自己的文件并减少系统文件夹和文件的混乱状态,Android 10 针对应用推出了一种新的存储范例,称为分区存储。分区存储改变了应用在设备的外部存储设备中存储和访问文件的方式,类似于Apple的“沙盒”机制。Android 10 是beta尝试,正式强制实施在Android 11。
2、“分区存储”的游戏规则
2.1、从Android 11开始,Apk默认为“分区存储”方式
即:android:requestLegacyExternalStorage="false",意思是说:为兼容旧版本请求外部存储,参数值才=true(Legacy兼容旧版本及Android 9[Sdk API 级别=28]及其以前的老版本,uses-sdk android:minSdkVersion="23" android:targetSdkVersion="28";API 级别=23代表Android 6;API 级别=19代表Android 4.4;API 级别=29代表Android 10;API 级别=30代表Android 11;API 级别=31代表Android 12)。有关Android版本号与API级别对应关系(需墙后访问国外资源):
https://developer.android.google.cn/guide/topics/manifest/uses-sdk-element?hl=en#ApiLevels
也就是说:你未在AndroidManifest.xml中显式的声明,目前平台API 30也默认Android 11的存储方式为分区存储(即帮你默认了这个参数数值为:android:requestLegacyExternalStorage="false")
2.2、Android 11起对媒体或文件访问的主要改变
2.2.1、应当区分API级别定义AndroidManifest.xml
可通过模板文件实现批量编译更新AndroidManifest.xml:AndroidManifest.template.xml
以前的老版本(Android 9 API 级别=28及其以前的版本):
从Android 10 API 级别=29以后的新版本:
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="28" />
<%uses-permission%>
<!-- API 级别在19~28才需要申明权限;API 29开始所有运行时的权限均在需要访问时动态申请-->
从Android 10 API 级别=29以后的新版本:
<!-- android:requestLegacyExternalStorage="true"我们需要将其屏蔽掉-->
上面这句是要关闭分区存储Scoped Storage。Android10及其以上版本默认值=false即使用分区存储。
为了Android11分区存储强制执行的过渡期,Android10仍允许兼容=true
Android11起强制执行分区存储,它支持读写应用本身创建的媒体,基于Intent意图而不管其存储位置在何处,
并将其存储在归因于该版本应用的内部存储的位置,可读但不允许批量打开共享文件夹及其文件,
但可以打开单独1个文件;否则需要使用MediaSto
<!-- android:requestLegacyExternalStorage="true" 加入到application部分,版本<=28时(Android9)的安卓sdk的话才需加入-->
<uses-feature android:glEsVersion="0x00020000" android:required="True"/>
<application android:persistent="%persistent%"
android:restoreAnyVersion="%restoreAnyVersion%"
android:label="%label%"
android:debuggable="%debuggable%"
android:largeHeap="%largeHeap%"
android:icon="%icon%"
android:theme="%theme%"
android:hardwareAccelerated="%hardwareAccelerated%"
android:usesCleartextTraffic="true"
<!--android:requestLegacyExternalStorage="true"-->
<!-- :此句关闭分区存储Scoped Storage:Android10及其以上版本默认值=false即使用分区存储。
为了Android11分区存储强制执行的过渡期,Android10仍允许兼容=true
Android11起强制执行分区存储,它支持读写应用本身创建的媒体,基于Intent意图而不管其存储位置在何处,
并将其存储在归因于该版本应用的内部存储的位置,可读但不允许批量打开共享文件夹及其文件,
但可以打开单独1个文件;否则需要使用MediaStore API接口用原生文件提供者或文件解析器
ContentResolver.Query方法批量读取文件-->
android:resizeableActivity="false">
<%provider%>
具体改变,以本文最后官方Google开发者大会上讲的为准:
Android 11分区存储学习视频Scoped Storage_pulledup的博客-CSDN博客
2.2.2、改变1:默认你的Apk是分区存储媒体和文件的
不同的Apk的能提供对外共享的文件的外部存储空间是不一样的。使用分区存储模型的应用只能访问自身的应用的专用缓存文件,默认不能访问其它应用的分区存储下的数据目录中的专有资源。
如果这些Apk对外发布了可以共享的资源,你可以通过申请访问外部存储的权限来读取:
<!-- 存储权限-读外部存储: -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
并通过FileProvider提供的应用对外共享的文件,提取复制(不能改写其它Apk的贡献的资源)到你的Apk下使用。只要是你的Apk自身产生的文件或媒体,你随时可以增删改查。
访问应用的专属文件(可读可写。但是:但Apk被卸载后,它们便不复存在啦):
访问持久性文件:如需从外部存储空间访问应用专属文件,请调用 getExternalFilesDir(),如以下代码段所示(详见2.2.4.3、File path access for media,使用直接文件路径和原生库访问文件:):
val appSpecificExternalDir = File(context.getExternalFilesDir(), filename)
创建缓存文件:如需将应用专属文件添加到外部存储空间中的缓存,请获取对 externalCacheDir 的引用(详见2.2.4.3、File path access for media,使用直接文件路径和原生库访问文件:):
val externalCacheFile = File(context.externalCacheDir, filename)
Delphi句法如下:
uses
System.IOUtils
{$IFDEF Android},Androidapi.IOUtils{$ENDIF Android}
;
//(可读可写。但是:但Apk被卸载后,它们便不复存在啦):
{$IFDEF Android}
ShowMessage('var appSpecificExternalDir:=GetExternalFilesDir;//即'+Androidapi.IOUtils.GetExternalFilesDir);
ShowMessage('var externalCacheFile:=GetExternalCacheDir;//即'+Androidapi.IOUtils.GetExternalCacheDir);
LogMe(self,'var openFileOutput:=GetFilesDir//即:'+Androidapi.IOUtils.GetFilesDir+',区别是不需申请写的权限');
LogMe(self,'var openFileOutput:=GetCacheDir//即:'+Androidapi.IOUtils.GetCacheDir+',区别是不需申请写的权限');
LogMe(self,'var getAppSpecificPicturesStorageDir:=GetExternalPicturesDir//即:'+Androidapi.IOUtils.GetExternalPicturesDir+',Result := GetJniCustomPath("getExternalFilesDir'', ''(Ljava/lang/String;)Ljava/io/File;", cpPICTURES);cpPICTURES是StrPathType数组元素');
{$ENDIF Android}
var appSpecificExternalDir:=GetExternalFilesDir//即:/storage/emulated/0/Android/data/com.CarveoutManage.CarveoutManage104/files,若要写入需申请写的权限
var externalCacheFile:=GetExternalCacheDir//即:/storage/emulated/0/Android/data/com.CarveoutManage.CarveoutManage104/cache,若要写入需申请写的权限
var openFileOutput:=GetFilesDir//即:/data/user/0/com.CarveoutManage.CarveoutManage104/files,区别是不需申请写的权限
var openFileOutput:=GetCacheDir//即:/data/user/0/com.CarveoutManage.CarveoutManage104/cache,区别是不需申请写的权限
var getAppSpecificPicturesStorageDir:=GetExternalPicturesDir
//即:/storage/emulated/0/Android/data/com.CarveoutManage.CarveoutManage104/files/Pictures,Result := GetJniCustomPath("getExternalFilesDir', '(Ljava/lang/String;)Ljava/io/File;", cpPICTURES);cpPICTURES是StrPathType数组元素
最终,建议使用此路径,作为可以在本地缓存的外部存储路径:
Androidapi.IOUtils.GetExternalDocumentsDir
2.2.3、改变2:Apk被人为删除后,存储在内部存储中的App文件和存储在外部存储中的App文件将不再保留。
2.2.4、改变3:Android 11起,无需授权,便可以读取所有的系统共享的媒体资源和文件资源,但需要使用系统文件挑选器来打开它们;系统的共享文件夹不再允许任何写入。
即:系统默认程序(比如相机)产生的共享媒体和文件,你均可在不申请权限的情况下读取;但:打开它们:
2.2.4.1、申请权限READ_EXTERNAL_STORAGE
2.2.4.2、打开非媒体类的文件资源,不再接受任何第三方定制的“文件挑选器”
2.2.4.3、打开非媒体的(比如文件列表或文件)的资源的话,只接受“系统默认的文件挑选器”
Non media requires the system picker
为媒体访问加入了“文件路径” File path access for media,使用直接文件路径和原生库访问文件:为了帮助您的应用更顺畅地使用第三方媒体库,Android 11 除允许您使用 MediaStore API 之外,还允许你通过直接文件路径访问共享存储空间中的媒体文件。其中包括:
- File API。
- 原生库,例如
fopen()
。
非媒体类的文件也允许存放到共享文件夹download
系统挑选器:既可以打开媒体资源,也可以打开非媒体类的其它任何文件。Delphi原有的TActionList呼叫系统媒体挑选器、呼叫系统相机、呼叫分享,仍然有效:
2.2.4.4、在未使用MediaStore API时,为了I/O性能,只允许打开单个文件,无论是共享文件夹download还是共享的媒体文件夹。
2.2.4.5、不再允许获取Root目录,不再允许在任何共享文件夹下创建apk的文件夹和文件,它们必须在归因的Apk的“分区存储”中建立,路径如下:
2.2.4.6、通过代码批量列表或指定特定路径去访问外部存储中的共享媒体只能使用MediaStore API
所需权限:READ_EXTERNAL_STORAGE
使用哪个MediaStore API呢?使用ContentResolver组件调用query函数(类似数据库中的查询select命令)。
最后,在通过游标(类似数据库中的查询结果集)列表出来:
我在去年2020年4月已经为大家介绍了这种Android原生的方法,这里就不再赘述了,其中有Delphi代码提供下载:
《delphi Android四大组件之ContentProvider:案例delphi 加载Android手机图片的效率》
事实上,不同APK之间共享文件和媒体的方法,就是这样类似实现的,一方提供文件ContentProvider,另一方申请并解析提取文件ContentResolver。它们是归集到分类的媒体集合中,因Intent意图的URI的资源类型而不依文件存储位置,来读取文件目录树的。
2.2.4.7、编辑或删除(2.2.4.6方法或经授权可以访问的)所获得的媒体或文件
所需权限:READ_EXTERNAL_STORAGE
只需请求一次权限即可,而无需以前那样,每删除1个文件就要申请一次权限(即:实现了批量权限申请):
MediaStore.createWriteRequest() 或 MediaStore.createTrashRequest() 为应用的写入或删除请求创建待定 intent,然后通过调用该 intent 提示用户授予修改一组文件的权限;Delphi 11下媒体库单元应当封装了,Delphi 10系列尚未封装:
若用Delphi实现的话,你只需使用Delphi动态申请权限的方法即可(运行时权限,无需设计时)。
共享文件夹下的共享文件(或经授权可以访问的其它应用的ContentProvider文件),你可以通过Copy到你自己的的分区文件夹下去,进行编辑和删除。
2.2.4.8、对媒体或文件的元数据访问、对照片的ExiF位置信息的访问,必须申请权限并拉起系统询问用户的方法,否则不能使用
所需权限:ACCESS_MEDIA_LOCATION
所需权限:READ_EXTERNAL_STORAGE
若用Delphi实现的话,你只需使用Delphi动态申请权限的方法即可(运行时权限,无需设计时)。
但:申请权限后,需要拉起一个可以捕获的异常信息后再次拉起系统提示框,将上图的java代码翻译成delphi的pascal语法即可。
2.2.4.9、若你的Apk是媒体或文件的提供者(即生产者),你可以不经申请权限就写入到你的“分区存储中”
Media can be contributed without permission,比如你写文件流TFileStream、你TBitmap.SavetoFile、你下载文件到你的apk的本机分区存储等,不再需要以前那样申请外部存储写权限:
WRITE_EXTERNAL_STORAGE
2.2.4.10、Android 11的分区存储模式,默认对媒体和文件的访问,不再需要设计时赋值授权(因为会让Apk首次安装时强制被执行1次而带来不好的用户体验),你只需要在需要调用响应权限的窗体或方法之前,用代码动态申请权限即可。
3、Android 11 中的隐私
https://developer.android.google.cn/about/versions/11/privacy?hl=en https://developer.android.google.cn/about/versions/11/privacy?hl=en
4、兼容性框架本机测试工具
https://developer.android.google.cn/guide/app-compatibility/test-debug
附本博文官方视频:
Android 11分区存储学习视频Scoped Storage_pulledup的博客-CSDN博客
分享网友写的较好的“分区存储”文章:
https://shoewann0402.github.io/2020/03/17/android-R-scoped-storage/