欢迎大家来到IT世界,在知识的湖畔探索吧!
Android 11 应用兼容性适配指导
本文档基于谷歌Android 11 Developer Preview 4(DP4)版本的变更输出
一、兼容性调试工具
Android 11 引入了新的工具,用于针对最新版平台中的行为变更来测试和调试应用。这些工具属于新的兼容性框架的一部分,可让应用开发者单独开启和关闭各项变更。有了这种灵活性,您可以关闭单项变更,然后继续针对平台中的其他变更测试应用;也可以每次单独针对一项行为变更测试应用。 不管是影响所有应用的行为变更还是只影响以 Android 11 为目标平台的应用的行为变更,您都可以随意开启或关闭。 您可以使用开发者选项、logcat 或 ADB 命令来查看当前已启用的行为变更。具体使用方法参考: https://developer.android.google.cn/preview/test-changes
使用过程中需要注意的点:
1 对于每项变更,每个进程最多只会记录一次。为确保看到所有相关的 logcat 消息,请强行停止应用进程,然后再重启该进程。 2 每次您使用开发者选项或 ADB 命令为应用开启或关闭变更时,应用都会终止,以确保您的替换操作立即生效。 3 切换变更的开关限制 android:debuggable: 如果可以调试,则设为 “true”;如果无法调试,则设为 “false”。默认值为 “false”。
4 兼容性框架中包含的变更列表:Android 11 中的某些行为变更可能尚未包含在兼容性框架中。最新包含的变更可查看:
https://developer.android.google.cn/preview/test-changes#list
二、隐私更新
2.1 存储 2.1.1 分区存储 1.1. 背景
Android 11 进一步增强了平台功能,为外部存储设备上的应用和用户数据提供了更好的保护。作为这项工作的一部分,平台引入了进一步的改进,以简化向分区存储的转换。 为了让用户更好地控制自己的文件,保护用户隐私数据,并限制文件混乱情况,Android 11在分区存储基础上限制了应用访问其他应用的文件。
分区存储将存储空间分为两部分:
● 公共目录:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等
■ 公共目录的文件在App卸载后,不会删除 ■ 可以通过SAF、MediaStore接口访问 ■ 拥有权限,也能通过路径直接访问 ● 应用专属目录 ■ 应用专属目录只能自己直接访问 ■ App卸载,数据会清除。
1.2. 兼容影响
当您将应用更新为以 Android 11 为目标平台后,您将无法使用requestLegacyExternalStorage,而且也没有其他标记可以提供停用分区存储。 分区存储对于App访问存储方式、App数据存放以及App间数据共享,都产生很大影响。 而Environment.getExternalStorageDirectory() 在 API Level 29 开始已被弃用,开发者应迁移至 Context#getExternalFilesDir(String), MediaStore, 或Intent#ACTION_OPEN_DOCUMENT。
1.3. 适配
1应用targetSdkVersion 应用targetSdkVersion >= 30,都会强制打开分区存储,同时requestLegacyExternalStorage将会无效。 如果您需要对已安装的应用进行适配分区存储的数据迁移,则可以在应用更新到目标平台为Android 11版本后仍暂时保留原有的存储模式。请在应用的manifest中设置preserveLegacyExternalStorage属性为true,应用更新到android 11可以保留存储继承模式。 2应用私有目录访问 对于运行在Android 11的应用,无论targetSdkVersion是什么都无法访问Emulated存储中的其他应用私有目录(Android/data)。SAF(Storage Access Framework)同样也禁止访问应用私有目录。 某些应用的核心用例需要访问大量的文件,如文件管理操作或备份和恢复操作。这些应用可通过执行以下操作获取“所有文件访问权限”:
● 声明 MANAGE_EXTERNAL_STORAGE 权限。 ● 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。
● 注意:获得此权限的应用仍然无法访问属于其他应用的应用专用目录。这些目录在存储卷上显示为 Android/data/ 的子目录。 3 直接路径访问 注意:使用直接路径和原生库保存媒体文件时,应用的性能会略有下降。请尽可能改用MediaStore API。 具体适配参考: https://developer.android.google.cn/training/data-storage#scoped-storage https://developer.android.google.cn/preview/privacy/storage 1.3.1. 运行模式 1.3.1.1. App运行模式 在Android 11版本上,系统会根据App targetSdkVersion决定运行模式: ● App targetSdkVersion >= 30,默认为分区存储,并且无法取消。
● App targetSdkVersion < 29,默认为分区存储,可通过requestLegacyExternalStorage更改
应用可以通过AndroidManifest.xml设置requestLegacyExternalStorage, 选择对应的方式:
● App targetSdkVersion < 29,声明了READ_EXTERNAL_STORAGE,默认Legacy Mode ● App在下列条件都成立时 ■ 声明 MANAGE_EXTERNAL_STORAGE 权限。 ■ 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。 App拥有外置存储空间Read、Write权限。但是通过Environment.isExternalStorageLegacy接口判断,返回不一定是Legacy Mode。
1.3.1.2. 判断当前App运行模式 判断当前App运行什么模式,可以通过这个API判断: Environment.isExternalStorageLegacy() (added in api 29);
1.3.2. 读写公共目录 App启动分区存储后,只能直接访问自身专属目录,所以Android 11,提供了两种访问公共目录的方法(特殊直接路径访问参考1.3.8. 直接路径访问):
1.3.2.1. 通过MediaStore定义的Uri MediaStore提供了下列几种类型的访问Uri,通过查找对应Uri数据,达到访问的目的。 下列每种类型又分为三种Uri,Internal、External、可移动存储:
●Audio ■ Internal: MediaStore.Audio.Media.INTERNAL_CONTENT_URI
content://media/internal/audio/media。
■ External: MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
content://media/external/audio/media。
■ 可移动存储: MediaStore.Audio.Media.getContentUri
content://media/<volumeName>/audio/media。 ● Video ■ Internal: MediaStore.Video.Media.INTERNAL_CONTENT_URI content://media/internal/video/media。
■ External: MediaStore.Video.Media.EXTERNAL_CONTENT_URI content://media/external/video/media。
■ 可移动存储: MediaStore.Video.Media.getContentUri content://media/<volumeName>/video/media。 ● Image ■ Internal: MediaStore.Images.Media.INTERNAL_CONTENT_URI content://media/internal/images/media。
■ External: MediaStore.Images.Media.EXTERNAL_CONTENT_URI content://media/external/images/media。
■ 可移动存储: MediaStore.Images.Media.getContentUri content://media/<volumeName>/images/media。
● File
■ MediaStore. Files.Media.getContentUri content://media/<volumeName>/file。 ● Downloads
■ Internal: MediaStore.Downloads.INTERNAL_CONTENT_URI content://media/internal/downloads。
■ External: MediaStore.Downloads.EXTERNAL_CONTENT_URI content://media/external/downloads。
■ 可移动存储: MediaStore.Downloads.getContentUri content://media/<volumeName>/downloads。
1.3.2.1.1. 获取所有的Volume 对于前面描述的Uri中,getContentUri如何获取所有<volumeName>,可以通过下述方式:
1.3.2.1.2.Uri跟公共目录关系 MediaProvider对于App存放到公共目录文件,通过ContentResolver insert方法中Uri来确定,其中下表中<Uri路径>为相对路径,完整为: content://media/<volumeName>/<Uri路径>。
Mine Type |
Uri路径 |
一级目录 |
audio/* |
images/mediaimages/media/# |
Environment.DIRECTORY_ALARMS |
Environment.DIRECTORY_MUSIC |
||
Environment.DIRECTORY_NOTIFICATIONS |
||
Environment.DIRECTORY_PODCASTS |
||
Environment.DIRECTORY_RINGTONES |
||
image/* |
audio/albumartaudio/albumart/# |
Environment.DIRECTORY_MUSIC |
audio/playlistsaudio/playlists/# |
Environment.DIRECTORY_MUSIC |
|
video/* |
video/mediavideo/media/# |
Environment.DIRECTORY_DCIM |
Environment.DIRECTORY_MOVIES |
||
image/* |
images/mediaimages/media/# |
Environment.DIRECTORY_DCIM |
Environment.DIRECTORY_PICTURES |
||
image/* |
video/thumbnailsvideo/thumbnails/# |
Environment.DIRECTORY_MOVIES |
image/* |
images/thumbnailsimages/thumbnails/# |
Environment.DIRECTORY_PICTURES |
downloadsdownloads/# |
Environment.DIRECTORY_DOWNLOADS |
|
filefile/# |
Environment.DIRECTORY_DOWNLOADS |
|
Environment.DIRECTORY_DOCUMENTS |
1.3.2.1.3. 权限 MediaStore通过不同Uri,为用户提供了增、删、改。 App对应的权限如下:
Audio |
Image |
Video |
File |
Downloads |
|
WRITE_EXTERNAL_STORAGE |
no-op |
||||
READ_EXTERNAL_STORAGE |
能读取所有App的多媒体文件 |
不能读取非多媒体文件 |
|||
无 |
只能读取、修改自己新建的文件 |
1.3.2.1.4. 查询文件
通过ContentResolver,根据不同的Uri查询不同的内容:
1.3.2.1.5.读取文件 通过ContentResolver query接口,查找出来文件后如何读取,可以通过下面的方式: ● 通过ContentResolver openFileDescriptor接口,选择对应的打开方式
例如”r”表示读,”w”表示写,返回ParcelFileDescriptor类型FD。
● 访问Thumbnail,通过ContentResolver loadThumbnail接口
通过传递大小,MediaProvider返回指定大小的Thumbnail。 ● Native代码访问文件 如果Native代码需要访问文件,可以参考下面方式:
■ 通过openFileDescriptor返回ParcelFileDescriptor
■ 通过ParcelFileDescriptor.detachFd()读取FD ■ 将FD传递给Native层代码 ■ App需要负责通过close接口关闭FD
1.3.2.1.6.新建文件 如果需要新建文件存放到公共目录,需要通过ContentResolver insert接口,使用不同的Uri,选择存储到不同的目录。
Uri uri = null;
if (token[0].equals("audio")) {
uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,contentValues);
System.out.println(token[0]+"11111111");
} else if (token[0].equals("video")) {
uri = contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,contentValues);
System.out.println(token[0]+"222222222");
} else if (token[0].equals("image")) {
uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues);
System.out.println(token[0]+"33333333");
} else if (token[0].equals("application")) {
uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI,contentValues);
System.out.println(token[0]+"4444444");
} else {
uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI,contentValues);
System.out.println(token[0]+"5555555");
}
try {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.a55709);
OutputStream outputStream = contentResolver.openOutputStream(uri);
System.out.println(uri.toString());
System.out.println(outputStream.toString());
bitmap.compress(Bitmap.CompressFormat.JPEG,100,outputStream);
outputStream.close();
System.out.println("111111111111111");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
欢迎大家来到IT世界,在知识的湖畔探索吧!
- 下载文件
欢迎大家来到IT世界,在知识的湖畔探索吧!String aa = System.currentTimeMillis()+".zip";
FileNameMap fileNameMap = URLConnection.getFileNameMap();
String type = fileNameMap.getContentTypeFor(aa);
String[] token = type.split("/");
System.out.println(token[0]);
ContentResolver contentResolver = getContentResolver();
ContentValues contentValues = new ContentValues();
if (token[0].equals("audio")) {
contentValues.put(MediaStore.Audio.AudioColumns.DISPLAY_NAME, aa);
contentValues.put(MediaStore.Audio.AudioColumns.MIME_TYPE, type);
contentValues.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_MUSIC);
System.out.println(token[0] + "11111111");
} else if (token[0].equals("video")) {
contentValues.put(MediaStore.Video.VideoColumns.DISPLAY_NAME, aa);
contentValues.put(MediaStore.Video.VideoColumns.MIME_TYPE, type);
contentValues.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_MOVIES);
System.out.println(token[0] + "222222222");
} else if (token[0].equals("image")) {
contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, aa);
contentValues.put(MediaStore.Images.ImageColumns.MIME_TYPE, type);
contentValues.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
System.out.println(token[0] + "33333333");
} else if (token[0].equals("application")) {
contentValues.put(MediaStore.Downloads.DISPLAY_NAME, aa);
contentValues.put(MediaStore.Downloads.MIME_TYPE, type);
contentValues.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
System.out.println(token[0] + "4444444");
} else {
contentValues.put(MediaStore.Downloads.DISPLAY_NAME, aa);
contentValues.put(MediaStore.Downloads.MIME_TYPE, type);
contentValues.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
System.out.println(token[0] + "4444444");
}
Uri uri = null;
if (token[0].equals("audio")) {
uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues);
System.out.println(token[0] + "11111111");
} else if (token[0].equals("video")) {
uri = contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
System.out.println(token[0] + "222222222");
} else if (token[0].equals("image")) {
uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
System.out.println(token[0] + "33333333");
} else if (token[0].equals("application")) {
uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
System.out.println(token[0] + "4444444");
} else {
uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
System.out.println(token[0] + "4444444");
}
Uri xx = uri;
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL("");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5000);//设置超时时间
connection.setDoInput(true);//开启输入流
connection.connect();//打开连接
try {
int code = connection.getResponseCode();
if (code==HttpURLConnection.HTTP_OK){
InputStream fis = connection.getInputStream();
OutputStream fos = getContentResolver().openOutputStream(xx);
int count = 0;
//设置缓冲大小
byte[] b=new byte[1024 * 8];
//用来存储接收到的数据
int a;
//原文件的大小
long len=connection.getContentLength();
//已复制的大小
double temp=0;
//用来格式化数据的
DecimalFormat df=new DecimalFormat("##.##%");
//read返回值为-1表示内容已经读完了
while((a=fis.read(b))!=-1){
fos.write(b, 0, a);
temp+=a;
//计算出进度百分比
double t=temp/len;
System.out.println("正在下载中"+df.format(t));
}
System.out.println("下载完毕");
}
} catch (IOException e) {
e.printStackTrace();
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
- onActivityResult选择图片返回路经
新建RealPathFromUriUtils.java的文件,对路经解密,代码如下:
public class RealPathFromUriUtils {
/**
* 根据Uri获取图片的绝对路径
*
* @param context 上下文对象
* @param uri 图片的Uri
* @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
*/
public static String getRealPathFromUri(Context context, Uri uri) {
int sdkVersion = Build.VERSION.SDK_INT;
if (sdkVersion >= 19) { // api >= 19
return getRealPathFromUriAboveApi19(context, uri);
} else { // api < 19
return getRealPathFromUriBelowAPI19(context, uri);
}
}
/**
* 适配api19以下(不包括api19),根据uri获取图片的绝对路径
*
* @param context 上下文对象
* @param uri 图片的Uri
* @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
*/
private static String getRealPathFromUriBelowAPI19(Context context, Uri uri) {
return getDataColumn(context, uri, null, null);
}
/**
* 适配api19及以上,根据uri获取图片的绝对路径
*
* @param context 上下文对象
* @param uri 图片的Uri
* @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
*/
@SuppressLint("NewApi")
private static String getRealPathFromUriAboveApi19(Context context, Uri uri) {
String filePath = null;
if (DocumentsContract.isDocumentUri(context, uri)) {
// 如果是document类型的 uri, 则通过document id来进行处理
String documentId = DocumentsContract.getDocumentId(uri);
if (isMediaDocument(uri)) { // MediaProvider
// 使用':'分割
String id = documentId.split(":")[1];
String selection = MediaStore.Images.Media._ID + "=?";
String[] selectionArgs = {id};
filePath = getDataColumn(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, selectionArgs);
} else if (isDownloadsDocument(uri)) { // DownloadsProvider
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(documentId));
filePath = getDataColumn(context, contentUri, null, null);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// 如果是 content 类型的 Uri
filePath = getDataColumn(context, uri, null, null);
} else if ("file".equals(uri.getScheme())) {
// 如果是 file 类型的 Uri,直接获取图片对应的路径
filePath = uri.getPath();
}
return filePath;
}
/**
* 获取数据库表中的 _data 列,即返回Uri对应的文件路径
*
* @return
*/
private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
String path = null;
String[] projection = new String[]{MediaStore.Images.Media.DATA};
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndexOrThrow(projection[0]);
path = cursor.getString(columnIndex);
}
} catch (Exception e) {
if (cursor != null) {
cursor.close();
}
}
return path;
}
/**
* @param uri the Uri to check
* @return Whether the Uri authority is MediaProvider
*/
private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
/**
* @param uri the Uri to check
* @return Whether the Uri authority is DownloadsProvider
*/
private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
}
欢迎大家来到IT世界,在知识的湖畔探索吧!}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
得到相册里的图片进行裁剪
if (requestCode == 1 && resultCode == RESULT_OK) {
得到相册图片
2018.08.14 12:34完工,共耗时2小时,找了好久获得真实路径的方法
String aa = RealPathFromUriUtils.getRealPathFromUri(this, data.getData());
Bitmap bitmap = BitmapFactory.decodeFile(aa);
Uri uri = Uri.fromFile(new File(aa));
cc.setImageBitmap(bitmap);
System.out.println(aa);
}
1.3.2.1.7.修改文件
如果需要修改多媒体文件,需要通过ContentResolver query接口查找出来对应文件的Uri。 如果不是自己新建的文件,需要注意1.3.2.1.3. 权限中描述,需要catch RecoverableSecurityException,弹框给用户选择。
通过下列接口,获取需要修改文件的FD或者OutputStream: ● getContentResolver().openOutputStream(contentUri) 获取对应文件的OutputStream。 ● getContentResolver().openFile或者getContentResolver().openFileDescriptor 通过openFile或者openFileDescriptor打开文件,需要选择Mode为”w”,表示写权限。这些接口返回一个ParcelFileDescriptor。 getContentResolver().openFileDescriptor(contentUri,”w”); getContentResolver().openFile(contentUri,”w”,null);
1.3.2.1.8.删除文件 通过ContentResolver接口删除文件,Uri为query出来的Uri: getContentResolver().delete(contentUri,null,null);
1.3.2.1.9.文件批量申请 Android 11提供了批量文件授权申请: ● createWriteRequest ● createFavoriteRequest ● createTrashRequest ● createDeleteRequest 具体可以参考https://developer.android.google.cn/reference/android/provider/MediaStore#createDeleteRequest(android.content.ContentResolver,%20java.util.Collection%3Candroid.net.Uri%3E)
1.3.2.2.通过SAF接口 SAF,即Storage Access Framework,通过选择不同的DocumentsProvider,提供给用户打开、浏览文件。
Android默认提供了下列DocumentsProvider: MediaDocumentsProvider、ExternalStorageProvider、 DownloadStorageProvider。 他们之间差异是:
MediaDocumentsProvider |
ExternalStorageProvider |
DownloadStorageProvider |
|
读 |
只能读取视频、音频、图片 |
全部内置、外置存储 |
读取Download目录 |
删除 |
可以删除 |
||
修改 |
无法修改 |
可以修改 |
这个图片上,有三个区域,分别是: ● MediaDocumentsProvide,DownloadStorageProvider
● ExternalStorageProvider
● 第三方DocumentsProvider 如何使用,具体参考: https://developer.android.google.cn/guide/topics/providers/document-provider 大致方法如下:
● 选择单个文件
● 选择目录
文件管理程序,清理程序,可以通过这个方法获取对应目录以及子目录的全部管理权限。
● 新建文件
● 删除 DocumentsContract.deleteDocument(getContentResolver(),uri);
● 修改
■ 获取OutputStream getContentResolver().openOutputStream(uri);
■ 获取可写ParcelFileDescriptor getContentResolver().openFileDescriptor(contentUri,”w”);
getContentResolver().openFile (contentUri,”w”,null);
具体Demo参考:https://github.com/android/storage 在Android 11上,无法通过SAF选择External Storage根目录、Downloads目录以及App专属目录(Android/data、Android/obb)。
1.3.3.访问应用的专属目录 访问应用专属目录分为两种情况,第一是访问App自身专属目录,第二是访问其他App的专属目录。
1.3.3.1.
App自身专属目录 Android 11获取应用专属目录
■ 获取Media接口:getExternalMediaDirs
■ 获取Cache接口:getExternalCacheDirs
■ 获取Obb接口:getObbDirs
■ 获取Data接口:getExternalFilesDirs 应用专属目录App本身可以直接访问。
1.3.3.2.
其他App的专属目录 Android 11,App无法访问其他App的专属目录(Android/data)。如果需要访问其他应用专属目录数据,需要被访问者按照下列方法来提供:
1.3.3.2.
1.通过SAF文件
● 共享App自定义DocumentsProvider App自定义DocumentsProvider需要做以下步骤:
a)指定DocumentsProvider
b)DocumentsProvider实现基本接口:
● 访问App通过ACTION_OPEN_DOCUMENT,启动浏览
1.3.3.2.2.共享App实现FileProvider
FileProvider具体使用参考: https://developer.android.google.cn/training/secure-file-sharing/setup-sharing 这边总结一下大概步骤:
● 指定App FileProvider
● 指定文件路径,配置文件必须要放到res/xml中
● 获取分享Uri
● 设置权限,并且发送Uri
● 接收App,设置接受的inter-filter
● 接收并处理Uri
1.3.3.2.3.
App自定义私有Provider App可以实现自定义ContentProvider,尤其是内部文件共享,但是不希望UI交互。
1.3.4.MediaStore文件Pending状态
MediaStore中添加了一个IS_PENDING Flag,用于标记当前文件时Pending状态。其他App通过MediaStore查询文件,如果没有设置setIncludePending接口,查询不到设置为Pending状态的文件,这就给App专享访问此文件。在一些情况下使用,例如在下载的时候:下载中,文件是Pending状态→下载完成,文件Pending状态置为0。
1.3.5. MediaColumns.RELATIVE_PATH设置存储路径 Android Q上,通过MediaStore存储到公共目录的文件,除了1.3.2.1.2节Uri跟公共目录关系中规定的每一个存储空间的一级目录外,可以通过MediaColumns.RELATIVE_PATH来指定存储的次级目录,这个目录可以使多级,具体代码如下:
● ContentResolver insert方法 通过values.put(Media.RELATIVE_PATH,”Pictures/album/family “)指定存储目录。其中,Pictures是一级目录,album/family是子目录。
● ContentResolver update方法 通过values.put(Media.RELATIVE_PATH,”Pictures/album/family “)指定存储目录。通过update方法,可以移动存储地方。
1.3.6.访问图片Exif Metadata Android Q上, App如果需要访问图片上的Exif Metadata,需要做下列事情:
● 申请ACCESS_MEDIA_LOCATION权限 ● 通过MediaStore.setRequireOriginal返回新Uri Demo Code如下:
1.3.7.App Scopted Storage,访问权限总结 App访问不同目录的权限总结如下:
文件位置 |
需要权限 |
访问方式 |
App卸载是否保存 |
应用专属目录 |
无 |
getExternalFilesDir() |
不保留 |
Media文件(photos, videos, audio) |
访问其他app文件,需要READ_EXTERNAL_STORAGE修改其他App,需要弹框用户确认 |
MediaStore |
保留 |
Downloads |
无 |
SAF |
保留 |
1.3.8.直接路径访问 Android 11上,App可以直接通过路径访问拥有权限的文件。例如,可以通过路径访问自己通过MediaStore新建的Images。 因为现在分区存储公共区域,是基于FUSE来实现,通过直接路径访问会经过下列路程:访问者 → FUSE → KERNEL—->MediaProvider(得到真实数据)—>KERNEL → FUSE→访问者,比之前SDCARDFS多了几个步骤,所以会导致一些性能问题。建议通过MediaStore访问。
1.3.9.宽泛权限 Android 11,提供了两种宽泛权限,需要注意的是这两种宽泛权限是无法访问其他应用的专属目录:
● MANAGE_EXTERNAL_STORAGE App拥有此权限,能够读写公共区域内所有文件,并且可以访问MediaStore.Files里面的所有文件。此权限能够满足清理、手机搬家、杀毒、文件管理这些类型应用需求。 App可以通过下列方式申请:
● System Gallery Role Gallery Role只能是预装的系统应用,通过系统配置才能成为Gallery Role。拥有Gallery Role,通过MediaStore读写多媒体文件不用弹框用户交互。
1.3.10.应用卸载
● 如果App在AndroidManifest.xml中声明:android:hasFragileUserData=”true” 卸载应用会有提示是否保留App数据:
● App存放到公共目录下的文件,卸载后,如果需要修改,需要用户重新授予权限
1.3.11.App数据迁移 App打开分区存储,会涉及到数据的迁移,不然会导致旧数据无法使用。可以从下面几方面着手数据迁移:
● App对于可以存放到公共目录的文件,可以通过MediaStore接口存放到对应类型的公共目录中。 ● 对于私有数据,可以存放到App私有目录。
● 迁移后数据的共享访问
■ 对于存放到公共目录的文件,其他App可以通过MediaStore访问。
■ 对于无法存放在公共目录文件,可以放置在私有目录,通过Uri共享给其他App访问。
1.3.12.MediaStore Queries 在使用MediaStore进行query动作的时候,使用Projection时,Column Name要在MediaStore中定义好的。
1.3.13.新建测试使用可移动存储 如果一个设备没有可移动的存储,可以使用下面的方法新建虚拟存储设备:
● adb shell sm set-virtual-disk true
● 在设置 -> 存储 -> Virtual SD,进行初始化
1.4.规范愿景 我们希望三方应用,尤其是TOP应用,能够按照分区存储的规范,将用户数据(例如图片、视频、音频等)保存在公共目录,把应用数据保存在SDCARD私有目录,以更好地保护外部存储上的应用和用户数据。而Google正在更新 Google Play 政策,以确保应用只在其真正需要获取位置信息时才请求授权。
2.1.2应用缓存
1 背景 在Android 11上,应用默认不能删除其他应用的缓存文件,即使申请了MANAGE_EXTERNAL_STORAGE权限。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/storage#manage-device-storage 2 兼容性影响 文件管理类,清理类或其他具有缓存清理功能应用,清除其他应用缓存功能失效。 3 适配指导 1 使用intent action – ACTION_MANAGE_STORAGE 检查可用存储空间大小。 2 如果可用的存储空间不足,使用 intent action —ACTION_CLEAR_APP_CACHE 呈现UI界面让用户确认后,触发所有应用的缓存清理。 注意:执行 ACTION_CLEAR_APP_CACHE 触发的缓存清理,会清理所有应用的缓存,同时大量的IO操作也会加剧电量消耗,如非必要,请不要使用。
2.1.3文件访问限制 1 背景 如果您的应用以 Android 11 为目标平台并使用存储访问框架 (SAF),则您无法再使用ACTION_OPEN_DOCUMENT和ACTION_OPEN_DOCUMENT_TREE操作访问某些目录,具体限制如下:
1 访问目录 您无法再使用ACTION_OPEN_DOCUMENT_TREE 操作来请求访问以下目录: Downloads根目录。 设备制造商认为可靠的各个 SD 卡根目录,无论该卡是模拟卡还是可移除的卡。 内部存储根目录
2 访问文件 您无法再使用 ACTION_OPEN_DOCUMENT_TREE 或 ACTION_OPEN_DOCUMENT操作来请求用户从以下目录中选择单独的文件: Android/data/ 目录及其所有子目录。 Android/obb/ 目录及其所有子目录。
2 兼容性影响 如果应用指定AndroidR为运行平台,则不再能使用SAF访问上述指出的目录,可能导致您的业务逻辑异常。
3 适配指导 执行以下操作来确认行为变更是否已对应用生效: 1 将targetSdkVersion指向Android 11 2 确保已经打开RESTRICT_STORAGE_ACCESS_FRAMEWORK 兼容性开关(使用方法见文档兼容性调试工具部分) 。
3 使用 intent action – ACTION_OPEN_DOCUMENT_TREE ,检查Downloads目录是否显示并呈灰显状态。 4 使用intent action – ACTION_OPEN_DOCUMENT检查Android/data/和Android/obb/目录是否都不显示。
2.1.4存储权限变更 1 背景 Android 11 引入了与存储权限相关的以下变更。 1 不管应用的目标 SDK 版本是什么,以下变更均会在 Android 11 中生效: ● 存储运行时权限已重命名为文件和媒体。 ● 如果应用未选择停用分区存储,并且请求 READ_EXTERNAL_STORAGE 权限,则用户会看到不同于 Android 10 的对话框。该对话框会指示应用正在请求访问相册和多媒体。如下图所示:
在系统设置的设置 > 隐私 > 权限管理器 > 文件和媒体 页面中,用户可以查看已授予权限READ_EXTERNAL_STORAGE应用,应用会列在允许存储所有文件下。
注意:如果您的应用以Android 11 为目标运行平台,上述允许存储所有文件代表的是对文件的只读权限。
2 以 Android 11 为目标平台 如果应用以 Android 11 为目标平台,则WRITE_EXTERNAL_STORAGE 权限和 WRITE_MEDIA_STORAGE 特许权限将不再提供任何其他访问权限。 2 兼容性影响 1 存储运行时权限UI发生变更。
2 WRITE_EXTERNAL_STORAGE 权限和 WRITE_MEDIA_STORAGE 在targetSdkVersion 指定为30时,发生变更。
2.1.5所有文件访问 1背景 有些应用主要功能就是访问手机存储文件,例如文件管理器、备份&恢复出厂操作。在Android 11 版本上,需要通过声明MANAGE_EXTERNAL_STORAGE权限来获取“Allowed for all files(允许存储所有文件)”权限,进行功能实现。 此权限被授予后,拥有以下权限:
1.“共享存储”上的所有文件的读写权限 共享存储说明: https://developer.android.google.cn/training/data-storage/shared
2. MediaStore.Files表内容 注意:即便授予了所有文件访问权限,应用也不能获取其他app的应用专属的文件。 应用专属目录: https://developer.android.google.cn/training/data-storage/app-specific 2兼容性影响 文件管理类应用或其他需要对较多存储文件进行扫描和处理的应用,可能会功能失效。
3 适配指导 Google适配指导: https://developer.android.google.cn/preview/privacy/storage#all-files-access
1 AndroidManifest.xml中声明MANAGE_EXTERNAL_STORAGE权限。
2 使用intent action – ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 跳转到系统设置页面,引导用户开启“Allowed for all files(允许存储所有文件)”权限。
2.2 权限
2.2.1单次权限
1.1 背景 对于最敏感的数据类型,包括位置信息、设备的麦克风和摄像头,在 Android 11中,用户可以授予单次的临时访问权限。
如右图所示。
如果用户选择了该选项,应用将获得临时的一次性权限。应用至少需要满足以下条件中一条时才能访问相关的数据:
(1)应用的Activity在用户授予一次性权限之后一直可见。
(2)应用在用户授予权限时可见,并且从那之后一直运行着前台服务,即使用户将应用切到后台,应用也会保留权限。
(3)应用短时间退至后台。 如果以上三个条件都不满足,无论应用的targetSdkVersion是什么,都需要再次请求该权限,才能访问相关数据。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/permissions#dialog-visibility
1.2兼容性影响 对于应用请求位置信息、麦克风或摄像头相关的权限时,用户可能授予“仅限这一次”权限,当应用被切换到后台(既无前台Activity,又无前台服务),该权限会被系统收回,并且应用进程会被杀掉。
1.3适配指导 应用申请位置信息、麦克风或摄像头相关的权限后,如在后台访问相关的敏感数据,需要确保权限不会被系统撤销的情况下执行相应的功能。 如果您的应用已经遵循Google权限申请最佳实践则不受变更影响,否则请按照最佳实践进行适配: https://developer.android.google.cn/privacy/best-practices#permissions
2.2.2权限对话框可见性 1背景 Android 11 不建议重复请求特定权限组中的权限。在应用安装到设备上后,如果用户在使用过程中对某个特定权限拒绝了两次,则表示其希望“不再询问”相应权限组的权限。 系统对于是否算作“拒绝”选项,做出了如下两个定义:
(1)如果用户按返回按钮关闭权限对话框,此操作不算“拒绝”操作。
(2)应用使用requestPermissions()转到系统设置,然后点返回按钮,此操作就算是“拒绝”操作。
2兼容性影响 应用若对运行时权限使用不规范,可能出现权限被关闭后无法正确引导用户打开权限、闪退的现象。
3适配指导
1 应用尽量不要申请与功能不相关的权限。
2 如果功能必须使用到被用户拒绝的权限,应用可以在权限拒绝的回调中弹窗提示用户,说明申请该权限的意图,引导用户跳转到应用权限设置页面,授予该权限。 请按照Google权限申请最佳实践适配: https://developer.android.google.cn/privacy/best-practices#permissions
2.2.3读取手机号码权限 1 背景 Android 11 更新了对手机号码读取的权限管理。如果您的应用targetSdkVersion为30,通过TelephonyManager和 TelecomManager的getLine1Number()方法,或者TelephonyManager的getMsisdn()方法读取号码时,需要申请READ_PHONE_NUMBERS权限,即便申请了READ_PHONE_STATE权限。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/permissions#phone-numbers 2 兼容性影响 如果您的应用升级到targetSdkVersion到30,并且应用通过TelephonyManager和TelecomManager的getLine1Number()方法,或TelephonyManager的getMsisdn()方法获取电话号码,若没有申请READ_PHONE_NUMBERS权限,则无法获取电话号码。
3 适配指导 1 如果您的应用使用READ_PHONE_STATE权限读取电话号码,但是没有使用上文中所提到的getLine1Number()或getMsisdn()方法,则不受影响。
2 如果您的应用升级到R,配置了READ_PHONE_STATE权限同时使用了getLine1Number()或getMsisdn()方法,则按照如下方法进行适配: a.调整READ_PHONE_STATE权限申明的最大Sdk版本 b.增加READ_PHONE_NUMBERS权限。 代码如下:
2.2.4 闲置应用权限自动重置 1 背景 如果您的应用以Android11为目标平台并且数月未使用,系统会通过自动重置用户已授予应用的敏感权限来保护用户数据。此操作与用户在系统设置中查看权限并将应用的访问权限级别更改为拒绝的做法效果一样。 2 兼容性影响 如果您的应用以Android11为目标平台,若用户长时间不使用,当用户再次使用时,若应用没有权限校验逻辑则会导致与回收权限相关的业务失效。
3 适配指导 Goolge适配指导:https://developer.android.google.cn/preview/privacy/permissions#auto-reset 如果您的应用遵循有关在运行时请求权限的最佳实践,那么您不必对应用进行任何更改。这是因为,当用户与应用中的功能互动时,您应该会验证相关功能是否具有所需权限。 权限申请的最佳实践:https://developer.android.google.cn/training/permissions/requesting
2.3后台位置访问
1背景 Android 11移除了来自应用程序中提示允许运行后台定位访问。 如下图所示
上述应用内的权限弹窗中没有了“Allow all the time(一直允许)”的选项。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/location#background-location 2 兼容性影响 1 获取定位权限的应用程序对话框不再包括“一直允许”的选项 2 用户deny掉应用定位访问请求两次,任何进一步请求相同权限都会被系统忽略掉。 3 如果尝试请求ACCESS_BACKGROUND_LOCATION,同时请求任何其他权限,系统会抛出一个异常。(只针对sdk为android 11),具体表现为闪退。 4 调用requestPermissions()请求后台定位权限会跳转至系统设置界面。
3 适配指导 (1)Target Android 11 使用自定义的UI界面向用户展示申请ACCESS_BACKGROUND_LOCATION权限原因和过程。 自定义UI界面的方法,请参考Google适配指导 https://developer.android.google.cn/preview/privacy/location#create-custom-ui (2) Target Android 10 or lower 使用系统提供的UI界面向用户展示申请ACCESS_BACKGROUND_LOCATION权限原因和过程。
系统UI界面使用方法,请参考Google适配指导 https://developer.android.google.cn/preview/privacy/location#use-system-provided-ui
2.4应用包可见性 1 背景 应用包可见性(Package Visibility),是Android 11上提升系统隐私安全性的一个新特性。它的作用是限制app随意获取其他app的信息和安装状态。 此前,Android系统提供了多种SDK接口(主要在PMS中,如getInstalledPackages(0)等),使app能轻易获取其他app的信息。这些接口容易被病毒软件、间谍软件利用,引发网络钓鱼、用户安装信息泄露等安全事件,而同时此类接口的合法应用场景也很多,仅靠软件商店的扫描检测监控很难识别这类接口的滥用。
因此,Android 11上引入了Package Visibility新特性。它改造了获取app信息的接口,且封堵了SDK接口以外变相获取app安装状态的途径。如果app一定需要获取某些app的信息,必须在清单文件中声明“需要交互”的app,或者声明特定的权限以越过拦截。 Package Visibility特性符合“最低优先级原则”:仅允许app获取“它需要交互”的app的信息。
Google官网特性介绍: https://developer.android.google.cn/preview/privacy/package-visibility
1.2 兼容性影响 1 SDK接口行为变更 app默认不再能通过SDK接口获取其他app的信息,例如: getPackageInfo(getPackageName(), 0) 仍能正常返回应用自身的PackageInfo,但getPackageInfo(“com.another.app”,0)将抛出NameNotFoundException,即使com.another.app已经被安装到设备上。getInstalledPackages(0),只能返回应用自身,以及少数的核心AOSP应用的信息。
2 其他变更 app不能感知/data/data/com.another.app等目录的存在。即使com.another.app已经被安装到设备上,app尝试访问/data/data/com.another.app也将提示“File not found”,而不是“Permission denied”。 类似的目录还有/data/misc/profiles/cur/${userId}/com.another.app。此变更,可以防止app通过访问目录文件返回的错误不同,来判断特定app是否已被安装。
3 适配指导 如果仍想要通过SDK接口获取其他app的信息,需要在清单文件中声明自己“需要交互的app”。有三种方式:
● 声明要交互特定的app <manifest> …… <queries> <package android:name=”com.another.app”/> </queries> …… </manifest>
● 声明要交互能响应特定intent的apps <manifest> …… <queries> <intent> <action android:name=”android.intent.action.SEND” /> <data android:type=”image/jpeg” /> </intent> </queries> …… </manifest>
● 声明要交互所有的app 申请新权限:android.permission.QUERY_ALL_PACKAGES。 QUERY_ALL_PACKAGES权限等级为normal,app申请即可获得。但是,Google Play上架应用时会检测并限制该权限的使用。同理,在queries中声明“宽泛的intent”也将受到Google Play的检测和限制。 Google适配指导: https://developer.android.google.cn/preview/privacy/package-visibility 2.5前台服务类型 1 背景 从Android 9 开始,应用被限制只有在前台时才能使用camara和microphone。Android 11 为了进一步保护用户隐私权限,通过引入变更来限制前台服务访问camara和microphone相关的数据。如果你的应用targetSdkVersion指定为Android 11,并且在前台服务中访问这些数据,你需要在Manifest中注册前台服务组件时,指定foregroundServiceType为camara和microphone。
2兼容性影响 1 targetSdkVersion为Android 11的应用在前台服务中访问camera或microphone相关的数据,不指定相应的foregroundServiceType,将无法正常访问到相关数据。
2 targetSdkVersion为Android 11的应用,在后台启动前台服务,即使应用声明了对应的foregroundServiceType,也无法正常访问相关数据。
3适配指导 Google适配指导: https://developer.android.google.cn/preview/privacy/foreground-service-types
1 在AndroidManifest.xml文件中配置foregroundServiceType 如果你的应用运行的前台服务需要访问location和camera相关的数据,
需要在Manifest中申明服务时按照如下方式指定foregroundServiceType: <manifest> … <service … android:foregroundServiceType=”location|camera” /> </manifest>
如果您的应用运行的前台服务需要访问location,camera和microphone相关的数据,需要在Manifest中声明服务时按照如下方式指定foregroundServiceType:
<manifest> … <service … android:foregroundServiceType=”location|camera|microphone”/> </manifest>
(2)需要在前台服务中获取location、camera、microphone相关数据时,应用必须在前台时启动前台服务。
三、行为变更
3.1 Firebase JobDispatcher 和 GCMNetworkManager 停用 1 背景 如果您应用的目标API级别是R或者更高,运行在Android 6.0 或更高版本上,Firebase JobDispatcher和GcmNetworkManager API已经失效。 2 兼容性影响 如果应用的目标API级别是R或者更高,以Firebase JobDispatcher和GcmNetworkManager实现的功能将在Android6.0及其后续版本将失效。 3 适配指导 将Firebase JobDispatcher和GcmNetworkManager迁移成WorkManager进行代替。 Firebase JobDispatcher迁移指南: https://developer.android.google.cn/topic/libraries/architecture/workmanager/migrating-fb GcmNetworkManager迁移指南: https://developer.android.google.cn/topic/libraries/architecture/workmanager/migrating-gcm
3.2自定义view的Toast屏蔽
1 背景 出于安全方面的考虑,同时也为了保持良好的用户体验,如果包含自定义视图的toast消息是以 Android 11 为目标平台的应用从后台发送的,则系统会屏蔽这些消息框。请注意,仍允许使用文本消息框;此类消息是使用Toast.makeText()创建的,并不调用setView()。 如果您的应用仍尝试从后台发布包含自定义视图的toast消息,系统会在 logcat 中记录以下消息: W/NotificationService: Blocking custom toast from package <package> due to package not in the foreground。
2 兼容性影响 如果您的应用以Android 11为目标运行平台时,后台使用自定义view的toast消息将不能显示,可能会影响用户交互的完整性。
3 适配指导 Google适配指导: https://developer.android.google.cn/preview/features/toasts 1 使用兼容性框架测试变更 adb shell am compat enable (128611929|CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK) PACKAGE_NAME adb shell am compat disable (128611929|CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK) PACKAGE_NAME 兼容性框架的相关介绍,请参考 https://developer.android.google.cn/preview/behavior-changes-11
2 使用snackbar替换toast信息提示,使用方法请参考: https://developer.android.google.cn/training/snackbar/showing#display 3 如果snackbar不适用业务场景,仍然需要在后台显示toast,可以使用纯文本的toast,即不设置自定义的view使用系统提供的toast默认样式即可,不调用setView()方法。
4 以Android 11 为目标平台时,调用如下接口将发生变更: getView()方法将返回 null getHorizontalMargin(),getVerticalMargin(),getGravity() ,getXOffset(),getYOffset() 方法不能返回实际值,不要在业务逻辑中依赖接口的返回值。 setMargin(),setGravity()方法将会失效。 上述接口变更的具体说明,请查看 https://developer.android.google.cn/reference/android/widget/Toast 3.3堆指针标记 1背景 Android 11上,堆指针在最高有效字节 (MSB) 中有一个非零标记。错误地使用指针的应用(包括修改 MSB 的应用)会崩溃或遇到其他问题。这是支持未来启用了ARM内存标记扩展 (MTE) 的硬件所必需的变更。
2 兼容性影响 如果您的应用目标Sdk为R则堆指针标记默认开启,目标SDK低于R时,默认关闭。 使用如下命令开启或关闭此特性,查看您的应用是否有错误的使用指针场景。
adb shell am compat enable (135754954|NATIVE_HEAP_POINTER_TAGGING) PACKAGE_NAME adb shell am compat disable (135754954|NATIVE_HEAP_POINTER_TAGGING) PACKAGE_NAME
3 适配指导 应用可以在AndroidManifest.xml文件中进行如下配置,显式关闭此特性。
<application android:allowNativeHeapPointerTagging=”false”> … </application>
不过若您的应用有指针使用上的问题,关闭并不能帮助解决问题,建议如果有相关问题,直接解决。同时以上的规避方案在后续的Android版本中,将会移除。
Google特性介绍和适配指导: https://source.android.google.cn/devices/tech/debug/tagged-pointers
以上为转载文章
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/35848.html