阅读 160

Android R 新特性分析及适配指南

Android R(Android 11 API 30)于2020年9月9日正式发布,随国内各终端厂商在售Android设备的版本更新升级,应用软件对Android R 版本的兼容适配已迫在眉睫。

对于Android R的新特性,这里按照以下几个方面进行了归纳:分区存储、权限、隐私、性能、安全

官方文档描述:developer.android.google.cn/about/versi…

一、分区存储

从Android 10(API 29)开始,Android默认开启分区存储功能,不过Android 10 可通过增加android:requestLegacyExternalStorage="true"配置停用分区存储; 从Android 11(API 30)开始,强制执行分区存储,对于Android 11及以上设备,android:requestLegacyExternalStorage="true"配置将不再有效。

Android 11 分区存储官方描述: developer.android.google.cn/training/da… Android 10 默认开启分区存储: xiaxl.blog.csdn.net/article/det…

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_STORAGEWRITE_EXTERNAL_STORAGE 权限;

文件路径的访问权限

Android 11以上设备中,如果您的应用再次请求READ_EXTERNAL_STORAGE权限时,动态权限申请弹窗将变化为“您的应用正在请求访问照片和媒体”

您的应用正在请求访问照片和媒体

文件媒体访问 官方描述: developer.android.google.cn/training/da…

1.3、共享文件

如果需要与其他应用共享单个文件或应用数据,可以使用API:

  • FileProvider(分享自己的一个或多个文件)

如果应用需要将自己的一个或多个文件提供给其他应用,安全的做法是向接收方应用发送文件的内容 URI,并授予对该 URI 的临时访问权限。 Android FileProvider 组件提供了 getUriForFile() 方法,用于生成文件的内容 URI

  • ContentProvider(获取替他应用提供的数据)

如果您需要向其他应用提供数据,可以使用ContentProviderContentProvider是一种标准接口,可将一个进程中的数据与另一个进程中运行的代码进行连。

ContentProvider管理存储空间

Android 11 共享文件官方描述: developer.android.google.cn/training/da…

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相关官方描述: developer.android.google.cn/training/da…

二、权限

Android 11 中对权限进行了如下更改:

  • 新增 READ_PHONE_NUMBERS权限,获取手机号码;
  • 后台访问位置权限调整;
  • 用户多次针对某项特定的权限请求拒绝,表示用户希望不再询问
  • 应用长时间未使用,系统会自动重置用户已授予敏感权限
  • 针对位置、麦克风、摄像头授权弹窗新增仅限这一次授权按钮;
  • SYSTEM_ALERT_WINDOW 权限授权方式改变为系统自动授权;

参考 Android 11 权限更新官方文档: developer.android.google.cn/about/versi…

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信息;

相关信息参考 xiaxl.blog.csdn.net/article/det…

  • Android 11 开始获取手机号相关API更换为READ_PHONE_NUMBERS权限;

READ_PHONE_NUMBERS权限官方API描述: developer.android.google.cn/reference/a…

2.2、后台访问位置权限调整

  • 在Android10设备上,同时申请前台、后台位置权限时,并在用户选择始终允许后,才能获得后台位置权限。
  • 在Android11设备上,对于targetSdkVersion<=29(Android 10)的应用,同时申请前台、后台位置权限时,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要用户在设置页面选择始终允许才能获得后台位置权限。
  • 在Android11设备上,对于targetSdkVersion=30(Android 11)的应用,同时申请前台、后台位置权限时,系统会忽略该请求,无任何响应(需首先获取前台位置权限,再次申请后台位置权限)。
  • 在Android11设备上,对于targetSdkVersion=30(Android 11)的应用,先申请前台位置权限,后申请后台位置权限

后台访问位置权限 官方描述: developer.android.google.cn/training/lo…

a、Android10设备

在Android10设备上,同时申请前台、后台位置权限时,并在用户选择始终允许后,才能获得后台位置权限。

// 在Android10设备上,同时 申请前台、后台位置权限
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
复制代码

在Android10设备上,同时 申请前台、后台位置权限

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);
复制代码

Android11设备上targetSdkVersion<=29的应用,申请前台、后台位置权限

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的应用,申请后台位置权限,直接跳转到设置页面。

// 在Android11设备上,targetSdkVersion=30的应用,申请后台位置权限
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
复制代码

Android11设备上,targetSdkVersion=30的应用,申请后台位置权限

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();
        }
    }
}
复制代码

源码参考: github.com/android/per…

2.6、SYSTEM_ALERT_WINDOW 权限授权方式

在 Android 11 中,SYSTEM_ALERT_WINDOW 权限授权方式更改为:根据请求自动向某些应用授予 SYSTEM_ALERT_WINDOW 权限

  • 系统会自动向具有 ROLE_CALL_SCREENING 且请求 SYSTEM_ALERT_WINDOW 的所有应用授予该权限。如果应用失去 ROLE_CALL_SCREENING,就会失去该权限。

ROLE_CALL_SCREENINGRoleManager中的常量类,多用于通知用户将我们的应用替换掉手机自带的预搭载应用(短信、电话拨号);

  • 系统会自动向通过 MediaProjection 截取屏幕且请求 SYSTEM_ALERT_WINDOW 的所有应用授予该权限,除非用户已明确拒绝向应用授予该权限。当应用停止截取屏幕时,就会失去该权限。此用例主要用于游戏直播应用。

SYSTEM_ALERT_WINDOW权限 官方描述: developer.android.google.cn/about/versi…

三、隐私保护

主要更改涉及以下几个方面:

  • 软件包可见性:获取其他应用信息需在AndroidManifest中增加<queries>标签;
  • 前台服务:访问位置信息、摄像头、麦克风限制;
  • 永久 SIM 卡标识符 ICCID 获取受限;
  • 新增AppOpsManager.OnOpNotedCallback监听危险权限的调用,从而保护用户的私密数据;

这样对于第三方依赖库的权限使用申请可以做一个监控

3.1、软件包可见性

  • 在 Android 11 及更高版本设备中,当应用的 targetSdkVersion>=30 时,如果应用希望获取其他应用的信息(比如:包名、软件名称),原有方式将无法获取到。
  • 如需获取其他应用信息,需要在AndroidManifest中增加<queries>元素标签,告知系统希望获取哪些应用的信息或者哪一类应用的信息。
  • 如果需要获取所有应用的信息(比如:Launcher应用、设备管理器应用):这种情况只需要在AndroidManifest中添加QUERY_ALL_PACKAGES权限即可。

QUERY_ALL_PACKAGES权限为普通权限,不需要进行动态申请。但提交应用市场后,应用市场可能会进行审核

软件包可见性 官方描述: developer.android.google.cn/about/versi…

 <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>
复制代码

前台服务 官方描述: developer.android.google.cn/about/versi…

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 相关官方描述: developer.android.google.cn/guide/topic…

四、性能

  • JobScheduler使用频率进行限制

4.1、JobScheduler使用频率进行限制

Android 11 为对JobScheduler使用频率进行一定限制。 对于 debuggable 清单属性设置为 true 的应用,过多的调用 JobScheduler API 将返回 RESULT_FAILURE

JobScheduler主要用于在未来某个时间下满足一定条件时触发执行某项任务,例如:当设备在空闲状态, 并且使用wifi时, 自动下载ApkJobScheduler典型的使用举例如下:

 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);
复制代码

官方描述参考: developer.android.google.cn/about/versi…

官方Demo参考: github.com/googlearchi…

五、安全

  • 非 SDK 接口限制

5.1、非 SDK 接口限制

官方从 Android 9(API 级别 28)开始,对应用使用的非 SDK 接口实施了限制。 如果你的APP通过引用非 SDK 接口或尝试使用反射或 JNI 来获取句柄,这些限制就会起作用。官方给出的解释是为了提升用户体验、降低应用崩溃风险

a、非SDK接口检测工具

官方给出了一个检测工具,下载地址:veridex

veridex使用方法:

appcompat.sh --dex-file=apk.apk
复制代码

veridex检测截图

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接口限制 官方描述: developer.android.google.cn/about/versi…

========== THE END ==========

文章分类
Android
文章标签