100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > Android Q 存储新特性适配脑壳疼?指南来了!

Android Q 存储新特性适配脑壳疼?指南来了!

时间:2021-12-30 17:54:09

相关推荐

Android Q 存储新特性适配脑壳疼?指南来了!

码个蛋(codeegg)第 692次推文

原文:

https://mp./s/aiDMyAfAZvaYIHuIMLAlcg

简单回顾下:Android Q 适配 之 存储新特性

接下来看看存储新特性的适配啦~

继续第二章,且看第二回~

2. 存储空间限制

2.3 适配指导

Android Q Scoped Storage 新特性谷歌官方适配文档:

https://developer./preview/privacy/scoped-storage

OPPO 适配指导如下,分为:访问 APP 自身 App-specific 目录文件、使用 MediaStore 访问公共目录、使用 SAF 访问指定文件和目录、分享 App-specific 目录下文件和其他细节适配。

2.3.1 访问 APP 自身 App-specific 目录文件

无需任何权限,APP 即可直接使用文件路径来读写自身 App-specific 目录下的文件。获取 App-specific 目录路径的接口如下表所示。

如下,以新建并写入文件为例。

//set"Documents"assubDirfinalFile[]dirs=getExternalFilesDirs("Documents");FileprimaryDir=null;if(dirs!=null&&dirs.length>0){primaryDir=dirs[0];}if(primaryDir==null){return;}FilenewFile=newFile(primaryDir.getAbsolutePath(),"MyTestDocument");OutputStreamfileOS=null;try{fileOS=newFileOutputStream(newFile);if(fileOS!=null){fileOS.write("fileiscreated".getBytes(StandardCharsets.UTF_8));fileOS.flush();}}catch(IOExceptione){LogUtil.log("createfilefail");}finally{try{if(fileOS!=null){fileOS.close();}}catch(IOExceptione1){LogUtil.log("closestreamfail");}}

2.3.2 使用 MediaStore 访问公共目录

APP 无法直接访问公共目录下的文件。MediaStore 为 APP 提供了访问公共目录下媒体文件的接口。APP 在有适当权限时,可以通过 MediaStore 查询到公共目录文件的 Uri,然后通过 Uri 读写文件。

MediaStore 相关的 Google 官方文档:

https://developer./reference/android/provider/MediaStore

2.3.2.1 MediaStore 的 Uri 和路径对照表

MediaStore 提供了下列几种类型的访问 Uri,通过查询对应 Uri 数据(在 MediaProvider 中),达到访问的目的。

下列每种类型又分为三种 Uri:Internal、External、可移动存储。

在 Android Q 上,所有的外部存储设备,包括内置卡、SD 卡等,都会被命名,即设备的 Volume Name。MediaStore 可以通过 Volume Name 获取对应存储设备的 Uri。

for(StringvolumeName:MediaStore.getExternalVolumeNames(this)){MediaStore.Images.Media.getContentUri(volumeName);}

MediaProvider 对于 APP 新建到公共目录的文件,通过 ContentResolver.insert 方法中的 Uri 来确定具体存放目录。其中下表中

content://media//>

2.3.2.2 APP 通过 MediaStore 访问文件所需要的权限

通过 MediaStore 提供的 Uri,使用 ContentResolver 的 insert 接口,将文件保存到公共目录下。不同的 Uri,可以保存到不同的公共目录中,详见 2.3.2.1。

ContentValuesvalues=newContentValues();values.put(MediaStore.Images.Media.DESCRIPTION,"Thisisanimage");values.put(MediaStore.Images.Media.DISPLAY_NAME,"Image.png");values.put(MediaStore.Images.Media.MIME_TYPE,"image/png");values.put(MediaStore.Images.Media.TITLE,"Image.png");values.put(MediaStore.Images.Media.RELATIVE_PATH,"Pictures/test");Uriexternal=MediaStore.Images.Media.EXTERNAL_CONTENT_URI;ContentResolverresolver=context.getContentResolver();UriinsertUri=resolver.insert(external,values);LogUtil.log("insertUri:"+insertUri);OutputStreamos=null;try{if(insertUri!=null){os=resolver.openOutputStream(insertUri);}if(os!=null){finalBitmapbitmap=Bitmap.createBitmap(32,32,Bitmap.Config.ARGB_8888);press(pressFormat.PNG,90,os);//writewhatyouwant}}catch(IOExceptione){LogUtil.log("fail:"+e.getCause());}finally{try{if(os!=null){os.close();}}catch(IOExceptione){LogUtil.log("failinclose:"+e.getCause());}}

2.3.2.4 使用 MediaStore 查询文件

用 MediaStore 提供的 Uri 指定设备,selection 参数指定过滤条件,通过 ContentResolver.query 接口查询文件 Uri。

Uriexternal=MediaStore.Images.Media.EXTERNAL_CONTENT_URI;ContentResolverresolver=context.getContentResolver();Stringselection=MediaStore.Images.Media.TITLE+"=?";String[]args=newString[]{"Image"};String[]projection=newString[]{MediaStore.Images.Media._ID};Cursorcursor=resolver.query(external,projection,selection,args,null);UriimageUri=null;if(cursor!=null&&cursor.moveToFirst()){imageUri=ContentUris.withAppendedId(external,cursor.getLong(0));cursor.close();}

2.3.2.5 使用 MediaStore 读取文件

通过以上查询方式得到 Uri 之后,通过以下方式读取文件:

1)通过 ContentResolver openFileDescriptor 接口,选择对应的打开方式。例如”r” 表示读,”w” 表示写,返回 ParcelFileDescriptor 类型的文件描述符。

ParcelFileDescriptorpfd=null;if(imageUri!=null){try{pfd=context.getContentResolver().openFileDescriptor(imageUri,"r");if(pfd!=null){Bitmapbitmap=BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());//showthebitmap,ordosomethingelse.}}catch(IOExceptione){LogUtil.log("fail:"+e.getCause());}finally{try{if(pfd!=null){pfd.close();}}catch(IOExceptione){LogUtil.log("failinclose:"+e.getCause());}}}

2)访问 Thumbnail,使用 ContentResolver.loadThumbnail 接口。通过传入 size 参数,MediaProvider 返回指定大小的 Thumbnail。

3)Native 代码访问文件

如果 Native 代码需要访问文件,可以参考下面方式:

通过 openFileDescriptor 返回 ParcelFileDescriptor

通过 ParcelFileDescriptor.detachFd() 读取 FD

将 FD 传递给 Native 层代码

通过 close 接口关闭 FD

StringfileOpenMode="r";ParcelFileDescriptorparcelFd=resolver.openFileDescriptor(uri,fileOpenMode);if(parcelFd!=null){intfd=parcelFd.detachFd();//Passtheintegervalue"fd"intoyournativecode.Remembertocall//close(2)onthefiledescriptorwhenyou'redoneusingit.}

2.3.2.6 使用 MediaStore 修改文件

根据查询得到的文件 Uri,使用 MediaStore 修改其他 APP 新建的多媒体文件,需要 catch RecoverableSecurityException ,由 MediaProvider 弹出弹框给用户选择是否允许 APP 修改或删除图片 / 视频 / 音频文件。用户操作的结果,将通过 onActivityResult 回调返回到 APP。如果用户允许,APP 将获得该 Uri 的修改权限,直到设备下一次重启。

根据文件 Uri,通过下列接口,获取需要修改文件的 FD 或者 OutputStream:

1)getContentResolver().openOutputStream(contentUri)

获取对应文件的 OutputStream。

2)getContentResolver().openFile 或者 getContentResolver().openFileDescriptor

通过 openFile 或者 openFileDescriptor 打开文件,需要选择 Mode 为”w”,表示写权限。这些接口返回一个 ParcelFileDescriptor。

OutputStreamos=null;try{if(imageUri!=null){os=resolver.openOutputStream(imageUri);}}catch(IOExceptione){LogUtil.log("openimagefail");}catch(RecoverableSecurityExceptione1){LogUtil.log("getRecoverableSecurityException");try{((Activity)context).startIntentSenderForResult(e1.getUserAction().getActionIntent().getIntentSender(),100,null,0,0,0);}catch(IntentSender.SendIntentExceptione2){LogUtil.log("startIntentSenderfail");}}

2.3.2.7 使用 MediaStore 删除文件

删除其他 APP 新建的媒体文件,与修改类似,需要用户授权。删除文件使用 ContentResolver.delete 接口。

getContentResolver().delete(imageUri,null,null);

2.3.3 使用 SAF 访问指定文件和目录

SAF,即 Storage Access Framework。根据当前系统中存在的 DocumentsProvider,让用户选择特定的文件或文件夹,使调用 SAF 的 APP 获取它们的读写权限。APP 通过 SAF 获得文件或目录的读写权限,无需申请任何存储相关的运行时权限。

SAF 相关的 Google 官方文档:

/guide/topics/providers/document-provider

使用 SAF 获取文件或目录权限的过程:

APP 通过特定 Intent 调起 DocumentUI -> 用户在 DocumentUI 界面上选择要授权的文件或目录 -> APP 在回调中解析文件或目录的 Uri,最后根据这一 Uri 可进行读写删操作。

2.3.3.1 使用 SAF 选择单个文件

使用 Intent.ACTION_OPEN_DOCUMENT 调起 DocumentUI 的文件选择页面,用户可以选择一个文件,将它的读写权限授予 APP。

Intentintent=newIntent(Intent.ACTION_OPEN_DOCUMENT);//youcansettypetofilterfilestoshowintent.setType("*/*");startActivityForResult(intent,REQUEST_CODE_FOR_SINGLE_FILE);

2.3.3.2 使用 SAF 修改文件

通过 2.3.3.1 的方式,用户选择文件授权给 APP 后,在 APP 的 onActivityResult 回调中收到返回结果,解析出对应文件的 Uri。然后使用该 Uri,用户可以获取可写的 ParcelFileDescriptor 或者打开 OutputStream 进行修改。

if(requestCode==REQUEST_CODE_FOR_SINGLE_FILE&&resultCode==Activity.RESULT_OK){UrifileUri=null;if(data!=null){fileUri=data.getData();}if(fileUri!=null){OutputStreamos=null;try{os=getContentResolver().openOutputStream(fileUri);os.write("something".getBytes(StandardCharsets.UTF_8));}catch(IOExceptione){LogUtil.log("modifydocumentfail");}finally{if(os!=null){try{os.close();}catch(IOExceptione1){LogUtil.log("closefail");}}}}}

2.3.3.3 使用 SAF 删除文件

类似修改文件,在回调中解析出文件 Uri,然后使用 DocumentsContract.deleteDocument 接口进行删除操作。

if(requestCode==REQUEST_CODE_FOR_SINGLE_FILE&&resultCode==Activity.RESULT_OK){UrifileUri=null;if(data!=null){fileUri=data.getData();}if(fileUri!=null){try{DocumentsContract.deleteDocument(getContentResolver(),fileUri);}catch(FileNotFoundExceptione){LogUtil.log("deletedocumentfail");}}}

2.3.3.4 使用 SAF 新建文件

APP 通过 Intent.ACTION_CREATE_DOCUMENT 调起 DocumentUI 界面,由用户决定文件命名,以及存放位置。

Intentintent=newIntent(Intent.ACTION_CREATE_DOCUMENT);intent.addCategory(Intent.CATEGORY_OPENABLE);//youcansetfilemimetypeintent.setType("*/*");//defaultfilenameintent.putExtra(Intent.EXTRA_TITLE,"myFileName");startActivityForResult(intent,REQUEST_CODE_FOR_CREATE_FILE);

在用户确定后,操作结果将返回到 APP 的 onActivityResult 回调中,APP 解析出文件 Uri,之后就可以利用这一 Uri 对文件进行读写删操作。

if(requestCode==REQUEST_CODE_FOR_CREATE_FILE&&resultCode==Activity.RESULT_OK){UrifileUri=null;if(data!=null){fileUri=data.getData();}//read/update/deletebytheurigothere.LogUtil.log("uri:"+fileUri);}

2.3.3.5 使用 SAF 选择目录

通过 Intent.ACTION_OPEN_DOCUMENT_TREE 调起 DocumentUI 界面,用户可以选择任意文件夹,将它及其子文件夹的读写权限授予 APP。

Intentintent=newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE);startActivityForResult(intent,REQUEST_CODE_FOR_DIR);

在右上角的菜单中选择 show internal storage,可以在左侧菜单中选择内置存储设备,接着用户可以选择内置存储设备中的任意文件夹。

在用户确定后,APP 的 onActivityResult 回调收到操作结果,解析出被选文件夹的 uriTree。根据这一 uriTree ,进一步可以生成表示被选文件夹的 DocumentFile,利用 DocumentFile 提供的 API 可以对目录下的文件进行各种操作。

if(requestCode==REQUEST_CODE_FOR_DIR&&resultCode==Activity.RESULT_OK){UriuriTree=null;if(data!=null){uriTree=data.getData();}if(uriTree!=null){//createDocumentFilewhichrepresentstheselecteddirectoryDocumentFileroot=DocumentFile.fromTreeUri(this,uriTree);//listallsubdirsofrootDocumentFile[]files=root.listFiles();//doanythingyouwantwithAPIsprovidedbyDocumentFile//...}}

2.3.3.6 永久保存获取的目录权限

在 2.3.3.5 中,通过 SAF 获取了用户指定目录的读写权限,直至设备下一次重启。APP 可以通过 takePersistableUriPermission 接口获取该 uriTree 的永久权限,并将 uriTree 以 SharedPreferences 等形式持久化保存,以备之后随时使用。

if(uriTree!=null){//getpersistableuripermissionfinalinttakeFlags=data.getFlags()&(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);getContentResolver().takePersistableUriPermission(uriTree,takeFlags);//saveuriTreetosharedPreferenceSharedPreferencessp=getSharedPreferences("DirPermission",Context.MODE_PRIVATE);SharedPreferences.Editoreditor=sp.edit();editor.putString("uriTree",uriTree.toString());mit();}

在使用保存的 uriTree 时,首先检查是否顺利从 SharedPreferences 中获取到 uriTree,然后通过 takePersistableUriPermission 接口是否抛异常来判断权限是否仍存在。如果权限不存在,则重新通过 SAF 申请权限。

SharedPreferencessp=getSharedPreferences("DirPermission",Context.MODE_PRIVATE);StringuriTree=sp.getString("uriTree","");if(TextUtils.isEmpty(uriTree)){startSafForDirPermission();}else{try{Uriuri=Uri.parse(uriTree);finalinttakeFlags=getIntent().getFlags()&(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);getContentResolver().takePersistableUriPermission(uri,takeFlags);//uritreepermissionisgranted,dowhatyouwantwiththisuriLogUtil.log("uriisgranted");DocumentFileroot=DocumentFile.fromTreeUri(this,uri);}catch(SecurityExceptione){LogUtil.log("uriisnotgranted");startSafForDirPermission();}}

APP 申请到目录的永久权限后,用户可以在该 APP 的设置页面取消目录的访问权限,即点击如下图的 “Clear access” 按钮。

2.3.4 分享 App-specific 目录下文件

APP 可以选择以下的方式,将自身 App-specific 目录下的文件分享给其他 APP 读写。

2.3.4.1 使用 FileProvider

APP 可以使用 FileProvider 将私有文件的读写权限赋给其他 APP。这种方式十分适用于 APP 主动发起事件的情况,例如从 APP 将某个私有文件分享给其他 APP。

FileProvider 相关的 Google 官方文档:

https://developer./reference/androidx/core/content/FileProvider

/training/secure-file-sharing/setup-sharing

自定义 FileProvider 及使用的基本步骤:

1)在 AndroidManifest.xml 中声明 App 的 FileProvider

android:authorities="com.oppo.whoops.fileprovider"android:android:grantUriPermissions="true"android:exported="false">android:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/filepaths"/>

2)根据 FileProvider 声明中的 meta data,在 res/xml 中新建 filepaths.xml ,用于定义分享的路径。

namerepresentswhatotherappsseeintheshareduriassubdir.-->

3)在 APP 逻辑代码中生成要分享的 uri,设置权限,然后发送 uri。

StringfilePath=getExternalFilesDir("Documents")+"/MyTestImage.PNG";Uriuri=FileProvider.getUriForFile(this,"com.oppo.whoops.fileprovider",newFile(filePath));Intentintent=newIntent(Intent.ACTION_SEND);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(uri,getContentResolver().getType(uri));startActivity(Intent.createChooser(intent,"FileProvidershare"));

4)接收方 APP 的组件设置对应的 intent-filter。

5)接收方 APP 的组件收到 intent,解析获得 uri,通过 uri 获取文件的 FD。

Uriuri=getIntent().getData();ParcelFileDescriptorpdf=null;try{if(uri!=null){LogUtil.log("Uri:"+uri);pdf=getContentResolver().openFileDescriptor(uri,"r");LogUtil.log("Pdf:"+pdf);}}catch(FileNotFoundExceptione){LogUtil.log("openfilefail");}finally{try{if(pdf!=null){pdf.close();}}catch(IOExceptione1){LogUtil.log("closefdfail");}}

2.3.4.2 使用 ContentProvider

APP 可以实现自定义 ContentProvider 来向外提供 APP 私有文件。这种方式十分适用于内部文件分享,不希望有 UI 交互的情况。

ContentProvider 相关的 Google 官方文档:

https://developer./guide/topics/providers/content-providers

2.3.4.3 使用 DocumentsProvider

Android 默认提供的 ExternalStorageProvider、DownloadStorageProivder 和 MediaDocumentsProvider 会显示在 SAF 调起的 DocumentUI 界面中。ExternalStorageProvider 展示了所有外部存储设备的所有目录及文件,包括 App-specific 目录,所以 App-specific 目录下的文件也可以通过 SAF 授权给其他 APP。

APP 也可以自定义 DocumentsProvider 来提供向外授权。自定义的 DocumentsProivder 将作为第三方 DocumentsProvider 展示在 SAF 调起的界面中。DocumentsProvider 的使用方法请参考官方文档。

DocumentsProvider 相关的 Google 官方文档:

https://developer./reference/kotlin/android/provider/DocumentsProvider

2.3.5 细节适配

2.3.5.1 图片的地理位置信息

Android Q 上,默认情况下 APP 不能获取图片的地理位置信息。如果 APP 需要访问图片上的 Exif Metadata,需要完成以下步骤:

1)申请 ACCESS_MEDIA_LOCATION 权限。

2)通过 MediaStore.setRequireOriginal 返回新 Uri。

UriphotoUri=Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,cursor.getString(idColumnIndex));finaldouble[]latLong;//GetlocationdatafromtheExifInterfaceclass.photoUri=MediaStore.setRequireOriginal(photoUri);InputStreamstream=getContentResolver().openInputStream(photoUri);if(stream!=null){ExifInterfaceexifInterface=newExifInterface(stream);double[]returnedLatLong=exifInterface.getLatLong();//Iflat/longisnull,fallbacktothecoordinates(0,0).latLong=returnedLatLong!=null?returnedLatLong:newdouble[2];//Don'treusethestreamassociatedwiththeinstanceof"ExifInterface".stream.close();}else{//Failedtoloadthestream,soreturnthecoordinates(0,0).latLong=newdouble[2];}

2.3.5.2 DATA 字段数据不再可靠

MediaStore 中,DATA(即_data)字段,在 Android Q 中开始废弃,不再表示文件的真实路径。读写文件或判断文件是否存在,不应该使用 DATA 字段,而要使用 openFileDescriptor。

同时也无法直接使用路径访问公共目录的文件。

2.3.5.3 MediaStore.Files 接口自过滤

通过 MediaStore.Files 接口访问文件时,只展示多媒体文件(图片、视频、音频)。其他文件,例如 PDF 文件,无法访问到。

2.3.5.4 文件的 Pending 状态

Android Q 上,MediaStore 中添加了一个 IS_PENDING Flag,用于标记当前文件是 Pending 状态。

其他 APP 通过 MediaStore 查询文件,如果没有设置 setIncludePending 接口,就查询不到设置为 Pending 状态的文件,这就能使 APP 专享此文件。

这个 flag 在一些应用场景下可以使用,例如在下载文件的时候:下载中,文件设置为 Pending 状态;下载完成,把文件 Pending 状态置为 0。

ContentValuesvalues=newContentValues();values.put(MediaStore.Images.Media.DISPLAY_NAME,"myImage.PNG");values.put(MediaStore.Images.Media.MIME_TYPE,"image/png");values.put(MediaStore.Images.Media.IS_PENDING,1);ContentResolverresolver=context.getContentResolver();Uriuri=MediaStore.Images.Media.EXTERNAL_CONTENT_URI;Uriitem=resolver.insert(uri,values);try{ParcelFileDescriptorpfd=resolver.openFileDescriptor(item,"w",null);//writedataintothependingimage.}catch(IOExceptione){LogUtil.log("writeimagefail");}//clearIS_PENDINGflagafterwritingfinished.values.clear();values.put(MediaStore.Images.Media.IS_PENDING,0);resolver.update(item,values,null,null);

2.3.5.5 使用 MediaStore 接口定义好的 Columns

在使用 MediaStore 接口时,如果用到 Projection,Column Name 要使用在 MediaStore 中定义好的。如果 APP 引用的库使用了其他 Column Name,需要由 APP 做好 Column Name 映射。

2.3.5.6 设置相对路径

Android Q 上,通过 MediaStore 存储到公共目录的文件,除了 Uri 跟公共目录关系中规定的每一个存储空间的一级目录外,可以通过 MediaColumns.RELATIVE_PATH 来指定存储的次级目录,这个目录可以使多级,具体方法如下:

1)ContentResolver insert 方法

通过 values.put(Media.RELATIVE_PATH, "Pictures/album/family") 指定存储目录。其中,Pictures 是一级目录,album/family 是子目录。

2)ContentResolver update 方法

通过 values.put(Media.RELATIVE_PATH, "Pictures/album/family") 指定存储目录。通过 update 方法,可以移动文件的存储地方。

2.3.5.7 卸载应用

如果 APP 在 AndroidManifest.xml 中声明:android:hasFragileUserData="true",卸载应用会有提示是否保留 APP 数据。默认应用卸载时 App-specific 目录下的数据被删除,但用户可以选择保留。

2.3.5.8 新建虚拟可移动存储

APP 适配时,如果一个设备没有可移动存储,可以使用下面的方法新建虚拟存储设备:

1)命令行

adb shell smset-virtual-disktrue

2)在设置 -> 存储 -> Virtual SD,进行初始化

另外,关于存储权限的(如何启用)影响范围

模拟器

在Android Q Beat1中,谷歌暂未开放存储权限的改动。我们需要使用adb命令

adb shell sm set-isolated-storage on

来开启模拟器对于存储权限的变更来进行适配。

真机

当满足以下每个条件时,将开启兼容模式,即不开启Q设备中的存储权限改动:

应用targetSDK<=P。应用安装在从 Android P 升级到 Android Q 的设备上。

但是当应用重新安装(更新)时,不会重新开启兼容模式,存储权限改动将生效。

所以按官方文档所说,无论targetSDK是否为Q,必须对应用进行存储权限改动的适配。

近期文章:

产品要页面72变,x满足她

用Kotlin实现抖音爆红的文字时钟,征服产品小姐姐

Android Q 适配 之 存储新特性

日问题:

除了适配,还有什么问题脑壳疼?

专属升级社区:《这件事情,我终于想明白了》

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。