Android R(Android 11 API 30)于2020年9月9日正式发布,随国内各终端厂商在售Android设备的版本更新升级,应用软件对Android R 版本的兼容适配已迫在眉睫。
对于Android R的新特性,这里按照以下几个方面进行了归纳:分区存储、权限、隐私、性能、安全
。
官方文档描述:https://developer.android.google.cn/about/versions/11
## 一、分区存储
从Android 10(API 29)开始,Android默认开启分区存储
功能,不过Android 10 可通过增加android:requestLegacyExternalStorage="true"
配置停用分区存储
;
从Android 11(API 30)开始,强制执行分区存储
,对于Android 11及以上设备,android:requestLegacyExternalStorage="true"
配置将不再有效。
Android 11 分区存储官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage
Android 10 默认开启分区存储:
https://xiaxl.blog.csdn.net/article/details/103125117
1.1、访问目录
开启分区存储后,应用默认情况下只能访问应用专属目录(内部存储、外部存储应用专属目录)
,以及本应用所创建的特定类型的媒体文件
。
- 应用专属目录
包括内部存储
、外部存储专属目录
(若应用包名com.xiaxl.demo):/data/data/com.xiaxl.demo/files,
/sdcard/Android/data/com.xiaxl.demo/files
分别采用以下API进行访问:File appFile = new File(context.getFilesDir(), filename);
File appExternalFile = new File(context.getExternalFilesDir(), filename);
- 共享存储目录
包括媒体、文档和其他文件。例如DCIM、Pictures、Movies、Download等目录;
注:Android 10(Android Q)中共享存储目录使用MediaStore API访问;
Android 11(Android R)中共享存储目录支持MediaStore API与File API访问。
为保证应用在Android 10、Android 11设备中,使用File API对共享存储目录具有相同的文件访问权限
。建议在应用 AndroidManifest配置文件中,增加requestLegacyExternalStorage="true"
标识,以关闭Android 10设备上的分区存储功能
,使分区存储只对Android 11以上设备生效
:
1.2、访问所需权限
- 应用专属目录
应用专属目录(内部存储
、外部存储专属目录
)的读写,Android 4.4以上设备不需要任何权限;
- 共享存储目录
共享存储路径的读写,需要READ_EXTERNAL_STORAGE
与WRITE_EXTERNAL_STORAGE
权限;
Android 11以上设备中,如果您的应用再次请求READ_EXTERNAL_STORAGE
权限时,动态权限申请弹窗将变化为“您的应用正在请求访问照片和媒体”
。
文件媒体访问 官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage
1.3、共享文件
如果需要与其他应用共享单个文件或应用数据,可以使用API:
FileProvider
(分享自己的一个或多个文件)
如果应用需要将自己的一个或多个文件提供给其他应用,安全的做法是向接收方应用发送文件的内容 URI,并授予对该 URI 的临时访问权限。
Android FileProvider
组件提供了 getUriForFile()
方法,用于生成文件的内容 URI
。
ContentProvider
(获取替他应用提供的数据)
如果您需要向其他应用提供数据,可以使用ContentProvider
。ContentProvider
是一种标准接口,可将一个进程中的数据与另一个进程中运行的代码进行连。
Android 11 共享文件官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage
1.4、所有文件的访问权限
有一些应用需要获取所有文件的访问权限,例如:文件管理器软件。
获取所有文件的访问权限,可申请MANAGE_EXTERNAL_STORAGE
权限。
// 权限配置
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
// 是否拥有MANAGE_EXTERNAL_STORAGE权限判断
Environment.isExternalStorageManager();
// 跳转到设置页,请求用户授权
Intent intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
MANAGE_EXTERNAL_STORAGE
相关官方描述:
https://developer.android.google.cn/training/data-storage/manage-all-files
二、权限
Android 11 中对权限进行了如下更改:
- 新增
READ_PHONE_NUMBERS
权限,获取手机号码; 后台访问位置
权限调整;- 用户
多次针对某项特定的权限请求
点拒绝
,表示用户希望不再询问
; - 应用
长时间未使用
,系统会自动重置用户已授予敏感权限
; - 针对
位置、麦克风、摄像头
授权弹窗新增仅限这一次
授权按钮; SYSTEM_ALERT_WINDOW
权限授权方式改变为系统自动授权;
参考 Android 11 权限更新官方文档:
https://developer.android.google.cn/about/versions/11/privacy/permissions#one-time
2.1、新增 READ_PHONE_NUMBERS 权限
当应用的 targetSdkVersion>=30
时,使用以下API获取手机号码
时,需要申请READ_PHONE_NUMBERS
权限,而不再是READ_PHONE_STATE
权限。
TelephonyManager
类和TelecomManager
类中的getLine1Number()
方法。TelephonyManager
类中不受支持的getMsisdn()
方法。
在Android 10及之前的设备,可以继续使用READ_PHONE_STATE
获取手机号;
对Android11及以上设备,需获取READ_PHONE_NUMBERS
权限,才能获取手机号;
<manifest>
<!-- 仅在Android 10及以下设备获取READ_PHONE_STATE权限,以获取终端手机号码-->
<uses-permission android:name="READ_PHONE_STATE"
android:maxSdkVersion="29" />
<!-- Android 11及以上设备获取READ_PHONE_NUMBERS权限,以获取终端手机号码-->
<uses-permission android:name="READ_PHONE_NUMBERS" />
</manifest>
对于READ_PHONE_STATE
权限
- Android 10 开始
普通应用
已经不能再读取设备的硬件ID
信息;
相关信息参考 https://xiaxl.blog.csdn.net/article/details/103125117;
- Android 11 开始
获取手机号
相关API更换为READ_PHONE_NUMBERS
权限;
READ_PHONE_NUMBERS
权限官方API描述:
https://developer.android.google.cn/reference/android/Manifest.permission#READ_PHONE_NUMBERS
2.2、后台访问位置权限调整
- 在Android10设备上,同时
申请前台、后台位置权限
时,并在用户选择始终允许
后,才能获得后台位置权限。 - 在Android11设备上,对于
targetSdkVersion<=29(Android 10)
的应用,同时申请前台、后台位置权限
时,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要用户在设置页面选择始终允许
才能获得后台位置权限。 - 在Android11设备上,对于
targetSdkVersion=30(Android 11)
的应用,同时申请前台、后台位置权限
时,系统会忽略该请求,无任何响应(需首先获取前台位置权限,再次申请后台位置权限
)。 - 在Android11设备上,对于
targetSdkVersion=30(Android 11)
的应用,先申请前台位置权限,后申请后台位置权限
。
后台访问位置权限 官方描述:
https://developer.android.google.cn/training/location/background
a、Android10设备
在Android10设备上,同时申请前台、后台位置权限
时,并在用户选择始终允许
后,才能获得后台位置权限。
// 在Android10设备上,同时 申请前台、后台位置权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
b、Android11设备 targetSdkVersion<=29
在Android11设备上,对于targetSdkVersion<=29(Android 10)
的应用,同时申请前台、后台位置权限
时,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要用户在设置页面选择始终允许
才能获得后台位置权限。
// 在Android11设备上,targetSdkVersion<=29的应用,同时 申请前台、后台位置权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
c、Android11设备 targetSdkVersion=30 同时申请前台、后台位置权限
- 在Android11设备上,对于
targetSdkVersion=30(Android 11)
的应用,同时申请前台、后台位置权限
时,系统会忽略该请求,无任何响应(需首先获取前台位置权限,再次申请后台位置权限
)。
// 在Android11设备上,targetSdkVersion=30的应用,同时 申请前台、后台位置权限
// 请求无反应,此为错误写法
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
d、Android11设备 targetSdkVersion=30 依次申请前台、后台位置权限
在Android11设备上,对于targetSdkVersion=30(Android 11)
的应用,先申请前台位置权限,后申请后台位置权限
。
// 在Android11设备上,targetSdkVersion=30的应用,申请前台位置权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION}, 101);
Android11设备上,targetSdkVersion=30的应用,申请后台位置权限,直接跳转到设置页面。
// 在Android11设备上,targetSdkVersion=30的应用,申请后台位置权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
2.3、用户多次针对某项特定的权限请求
点拒绝
在 Android 11 中,用户多次针对某项特定的权限请求
点击了拒绝
,那么应用再次请求该项权限时,用户将不会看到系统权限弹窗,该操作表示用户希望不再询问
;
2.4、长时间未使用,自动重置已授予敏感权限
在 Android 11 中,当targetSdkVersion>=30时,应用在一段时间内未使用
,系统会通过自动重置用户已授予应用的运行时敏感权限
来保护用户数据;
2.5、新增“仅限这一次”授权按钮
从 Android 11(API 级别 30)开始,当应用请求与位置、麦克风、摄像头
相关权限时,面向用户的授权对话框会包含仅限这一次
选项;如果用户在对话框中选择仅限这一次
,系统会向应用授予临时的单次授权。
权限申请API使用方式不变:
private void showCameraPreview() {
// 判断是否拥有Camera权限
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
// 进入Camera页面
// startCamera();
} else {
// 请求Camera权限
requestCameraPermission();
}
}
private void requestCameraPermission() {
// 判断Camera权限,之前是否已被用户"拒绝"
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 弹窗告诉用户,为什么需要Camera权限
Snackbar.make(mLayout, R.string.camera_access_required,
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
// 请求Camera权限
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA},
PERMISSION_REQUEST_CAMERA);
}
}).show();
} else {
// 请求Camera权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CAMERA) {
// 用户授权Camera(用户选择"使用使用时允许"、"仅这一次允许")
if (grantResults.length == 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission has been granted. Start camera preview Activity.
Snackbar.make(mLayout, R.string.camera_permission_granted,
Snackbar.LENGTH_SHORT)
.show();
startCamera();
}
// 用户选择"拒绝"
else {
// Permission request was denied.
Snackbar.make(mLayout, R.string.camera_permission_denied,
Snackbar.LENGTH_SHORT)
.show();
}
}
}
源码参考:
https://github.com/android/permissions-samples/tree/main/RuntimePermissionsBasic;
2.6、SYSTEM_ALERT_WINDOW 权限授权方式
在 Android 11 中,SYSTEM_ALERT_WINDOW
权限授权方式更改为:根据请求自动向某些应用授予 SYSTEM_ALERT_WINDOW 权限
。
- 系统会自动向具有
ROLE_CALL_SCREENING
且请求SYSTEM_ALERT_WINDOW
的所有应用授予该权限。如果应用失去ROLE_CALL_SCREENING
,就会失去该权限。ROLE_CALL_SCREENING
为RoleManager
中的常量类,多用于通知用户将我们的应用替换掉手机自带的预搭载应用(短信、电话拨号); - 系统会自动向通过
MediaProjection
截取屏幕且请求SYSTEM_ALERT_WINDOW
的所有应用授予该权限,除非用户已明确拒绝向应用授予该权限。当应用停止截取屏幕时,就会失去该权限。此用例主要用于游戏直播应用。
SYSTEM_ALERT_WINDOW权限 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/permissions#system-alert
三、隐私保护
主要更改涉及以下几个方面:
- 软件包可见性:获取其他应用信息需在
AndroidManifest
中增加<queries>
标签; - 前台服务:访问位置信息、摄像头、麦克风限制;
- 永久 SIM 卡标识符 ICCID 获取受限;
- 新增
AppOpsManager.OnOpNotedCallback
监听危险权限的调用,从而保护用户的私密数据;
这样对于第三方依赖库的权限使用申请可以做一个监控
3.1、软件包可见性
- 在 Android 11 及更高版本设备中,当应用的
targetSdkVersion>=30
时,如果应用希望获取其他应用的信息(比如:包名、软件名称),原有方式将无法获取到。 - 如需获取其他应用信息,需要在
AndroidManifest
中增加<queries>
元素标签,告知系统希望获取哪些应用的信息或者哪一类应用的信息。 - 如果需要获取所有应用的信息(比如:Launcher应用、设备管理器应用):这种情况只需要在
AndroidManifest
中添加QUERY_ALL_PACKAGES
权限即可。QUERY_ALL_PACKAGES
权限为普通权限,不需要进行动态申请。但提交应用市场后,应用市场可能会进行审核软件包可见性 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/package-visibility
<manifest package="com.xiaxl.myapp">
// 1、若知道具体应用的包名
<queries>
<package android:name="com.xiaxl.otherapp01" />
<package android:name="com.xiaxl.otherapp01" />
</queries>
// 2、不知道包名,但想知道某一类App的应用信息
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/jpeg" />
</intent>
</queries>
</manifest>
3.2、前台服务:访问位置信息、摄像头、麦克风限制
当应用的 targetSdkVersion>=30
时,前台服务
访问位置信息、摄像头、麦克风
时,需添加foregroundServiceType
。
<manifest>
// 前台服务访问:位置信息、摄像头、麦克风
<service
android:foregroundServiceType="location|camera|microphone" />
</manifest>
前台服务 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/foreground-services
3.3、永久 SIM 卡标识符 ICCID 获取受限
在 Android 11 及更高版本中,使用 SubscriptionInfo.getIccId()
方法访问不可重置的 ICCID 受到限制。
SubscriptionInfo.getIccId()
方法会返回一个非null的空字符串
。
如需唯一标识设备上安装的 SIM 卡,请改用 getSubscriptionId()
方法。SubscriptionId
会提供一个索引值,用于唯一识别已安装的 SIM 卡(包括实体 SIM 卡和电子 SIM 卡),除非设备恢复出厂设置,否则此标识符的值对于给定 SIM 卡是保持不变的。
3.4、监听危险权限的调用
Android 11新增AppOpsManager.OnOpNotedCallback
为开发者提供对应用危险权限的使用监听,从而保护用户的私密数据
。
当应用以及应用的依赖包中,申请某项危险权限时,AppOpsManager.OnOpNotedCallback
的对应回调方法将会被调用,从而打印申请的权限
与对应的API调用栈
。
举例:使用位置权限获取位置信息
时,将会回调AppOpsManager.OnOpNotedCallback
中的onNoted
方法,并打印使用的权限
与对应的API调用栈
。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//
AppOpsManager.OnOpNotedCallback appOpsCallback =
new AppOpsManager.OnOpNotedCallback() {
private void logPrivateDataAccess(String opCode, String trace) {
Log.i("xiaxl: ", "opCode: " + opCode + "\n trace: " + trace);
}
@Override
public void onNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {
Log.i("xiaxl: ", "---onNoted---");
logPrivateDataAccess(syncNotedAppOp.getOp(),
Arrays.toString(new Throwable().getStackTrace()));
}
@Override
public void onSelfNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {
Log.i("xiaxl: ", "---onSelfNoted---");
logPrivateDataAccess(syncNotedAppOp.getOp(),
Arrays.toString(new Throwable().getStackTrace()));
}
@Override
public void onAsyncNoted(@NonNull AsyncNotedAppOp asyncNotedAppOp) {
Log.i("xiaxl: ", "---onAsyncNoted---");
logPrivateDataAccess(asyncNotedAppOp.getOp(),
asyncNotedAppOp.getMessage());
}
};
AppOpsManager appOpsManager = getSystemService(AppOpsManager.class);
if (appOpsManager != null) {
appOpsManager.setOnOpNotedCallback(getMainExecutor(), appOpsCallback);
}
}
public void getLocation() {
// 创建归因
Context attributionContext = createAttributionContext("shareLocation");
// 获取位置信息
LocationManager locationManager =
attributionContext.getSystemService(LocationManager.class);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return;
}
Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
}
打印日志如下:
---onNoted---
opCode: android:coarse_location
trace:
[com.xiaxl.android_test.MainActivity$1.onNoted(MainActivity.java:42),
android.app.AppOpsManager.readAndLogNotedAppops(AppOpsManager.java:8204),
android.os.Parcel.readExceptionCode(Parcel.java:2304),
android.os.Parcel.readException(Parcel.java:2279),
android.location.ILocationManager$Stub$Proxy.getLastLocation(ILocationManager.java:1225),
android.location.LocationManager.getLastKnownLocation(LocationManager.java:648),
com.xiaxl.android_test.MainActivity.getLocation(MainActivity.java:87),
com.xiaxl.android_test.MainActivity$2.onClick(MainActivity.java:70),
android.view.View.performClick(View.java:7448),
com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:967),
android.view.View.performClickInternal(View.java:7425),
android.view.View.access$3600(View.java:810),
android.view.View$PerformClick.run(View.java:28305),
android.os.Handler.handleCallback(Handler.java:938),
android.os.Handler.dispatchMessage(Handler.java:99),
android.os.Looper.loop(Looper.java:223),
android.app.ActivityThread.main(ActivityThread.java:7656),
java.lang.reflect.Method.invoke(Native Method),
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592),
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)]
从以上日志可以看出,当应用申请ACCESS_COARSE_LOCATION
权限并获取位置信息时
,打印了应用申请的权限
与对应的API调用栈
。
AppOpsManager 相关官方描述:
https://developer.android.google.cn/guide/topics/data/audit-access#audit-by-attribution-tag
四、性能
- JobScheduler使用频率进行限制
4.1、JobScheduler使用频率进行限制
Android 11 为对JobScheduler
使用频率进行一定限制。
对于 debuggable 清单属性设置为 true 的应用,过多的调用 JobScheduler
API 将返回 RESULT_FAILURE
。
JobScheduler
主要用于在未来某个时间下满足一定条件时触发执行某项任务,例如:当设备在空闲状态, 并且使用wifi时, 自动下载Apk
。JobScheduler
典型的使用举例如下:
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName jobService = new ComponentName(this, MyJobService.class);
//任务Id等于123
JobInfo jobInfo = new JobInfo.Builder(123, jobService)
// 任务最少延迟时间
.setMinimumLatency(5000)
// 任务deadline,当到期没达到指定条件也会开始执行
.setOverrideDeadline(60000)
// 网络条件,网络无需付费时执行
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
// 是否充电
.setRequiresCharging(true)
// 是否在空闲时执行
.setRequiresDeviceIdle(true)
// 设备重启后是否继续执行
.setPersisted(true)
// 设置退避/重试策略
.setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR)
.build();
scheduler.schedule(jobInfo);
官方描述参考:
https://developer.android.google.cn/about/versions/11/behavior-changes-all
官方Demo参考:
https://github.com/googlearchive/android-JobScheduler
## 五、安全
- 非 SDK 接口限制
5.1、非 SDK 接口限制
官方从 Android 9(API 级别 28)开始,对应用使用的非 SDK 接口实施了限制。
如果你的APP通过引用非 SDK 接口
或尝试使用反射或 JNI 来获取句柄
,这些限制就会起作用。官方给出的解释是为了提升用户体验、降低应用崩溃风险
。
a、非SDK接口检测工具
官方给出了一个检测工具,下载地址:veridex
veridex使用方法:
appcompat.sh --dex-file=apk.apk
b、blacklist、greylist、greylist-max-o、greylist-max-p含义
以上截图中,blacklist、greylist、greylist-max-o、greylist-max-p含义如下:
- blacklist 黑名单:禁止使用的非SDK接口,运行时直接Crash(因此必须解决)
- greylist 灰名单:即当前版本仍能使用的非SDK接口,但在下一版本中可能变成被限制的非SDK接口
- greylist-max-o: 在targetSDK<=O中能使用,但是在targetSDK>=P中被禁止使用的非SDK接口
- greylist-max-p: 在targetSDK<=P中能使用,但是在targetSDK>=Q中被禁止使用的非SDK接口
非SDK接口限制 官方描述:
https://developer.android.google.cn/about/versions/11/non-sdk-11
========== THE END ==========
文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,可关注我的公众号。
文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,可关注我的公众号。
文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,可关注我的公众号。