Android Q(10) 重要更新与兼容适配

613 阅读9分钟

说明:本文将从三个角度讲解Android 10的重要更新与兼容问题

推出新的保护措施,可保护您将需要在应用中支持的用户隐私

可能会影响您在 Android 10 上运行的应用的系统更改

用于可折叠设备、深色主题、手势导航、连接、媒体、NNAPI 和生物识别等方面的 API

1. 隐私权变更

Android 10 引入了大量变更(如改进了系统界面、让权限授予更加严格以及对应用能够使用哪些数据实施了限制),目的是保护隐私权并赋予用户控制权。以下是重大变更:

隐私权变更受影响的应用缓解策略
分区存储访问和共享外部存储中的文件的应用使用特定于应用的目录和媒体集合目录
增强了用户对位置权限的控制力在后台时请求访问用户位置信息的应用确保在没有后台位置信息更新的情况下优雅降级使用 Android 10 中引入的权限在后台获取位置信息
针对从后台启动 Activity 的限制不需要用户互动就启动 Activity 的应用使用通知触发的 Activity
设备唯一标识符的变更访问设备序列号或 IMEI 的应用使用用户可以重置的标识符

分区存储

Android Q 在外部存储设备中为每个应用提供了一个“隔离存储沙盒”(例如 /sdcard)。任何其他应用都无法直接访问您应用的沙盒文件。由于文件是您应用的私有文件,因此您不再需要任何权限即可在外部存储设备中访问和保存自己的文件。对于以 Android 10 及更高版本为目标平台的应用,其访问权限范围限定为外部存储,即分区存储。 此类应用可以查看外部存储设备内以下类型的文件,无需请求任何与存储相关的用户权限:

  • 特定于应用的目录中的文件(使用 getExternalFilesDir() 访问,这是应用专属文件)
     public File getPrivateAlbumStorageDir(Context context, String albumName) {
          // Get the directory for the app's private pictures directory.
          File file = new File(context.getExternalFilesDir(
                  Environment.DIRECTORY_PICTURES), albumName);
          if (!file.mkdirs()) {
              Log.e(LOG_TAG, "Directory not created");
          }
          return file;
      }
    
- 应用创建的照片、视频和音频片段(通过媒体库访问,这是可共享的媒体文件)
 

  MediaStore 包括所有属于用户的、其他应用可见的媒体文件,所有应用可以在没任何权限下为MediaStore提供内容,但是要访问其他应用提供的内容则必须获取READ_EXTERNAL_STORAGE,以下是相关权限:
  ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b29524bf83c24bc18351a72b372dd8f3~tplv-k3u1fbpfcp-zoom-1.image)
 
 MediaStore API 提供访问以下类型的媒体文件的接口:
 
     照片:存储在 MediaStore.Images 中
     视频:存储在 MediaStore.Video 中
     音频文件:存储在 MediaStore.Audio 中
     MediaStore 还包含一个名为 MediaStore.Files 的集合,该集合提供访问所有类型的媒体文件的接口。
 
如果你要访问系统媒体文件:
 
      Android Q中引入了一个新定义媒体文件的共享集合,如果要访问沙盒外的媒体共享文件,比如照片,音乐,视频等,需要申请新的媒体权限:
      READ_MEDIA_IMAGES,READ_MEDIA_VIDEO,READ_MEDIA_AUDIO,申请方法同原来的存储权限。
这里举一个保存图片的例子:
 
 在Android 10之前我们是这样保存图片的

public static void saveImg (File originalFile,String fileName) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return null; } //创建项目图片公共缓存目录 File file = new File(Environment.getExternalStorageDirectory()+ File.separator + Environment.DIRECTORY_PICTURES + File.separator + "yourAppName" + File.separator + "images";); if (! file.exists()) { file.mkdirs(); } //创建对应图片的缓存路径 File newFile = new File(file + File.separator + fileName); FileOutputStream outStream = new FileOutputStream(newFile,true) //伪代码 // 将originalFile文件写入到输出流完成图片保存 }

在Android 10或者更高的版本我们不能通过File API去操作了,不然会报无权限访问异常。

private static void saveImg (File originalFile,String imageName, String imageType) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return null; } if (TextUtils.isEmpty(relativePath)) { return null; } Uri insertUri = null; ContentResolver resolver = context.getContentResolver(); //设置文件参数到ContentValues中 ContentValues values = new ContentValues(); //设置文件名 values.put(MediaStore.Images.Media.DISPLAY_NAME, imageName); //设置文件描述,这里以文件名代替 values.put(MediaStore.Images.Media.DESCRIPTION, imageName); //设置文件类型为image/* values.put(MediaStore.Images.Media.MIME_TYPE, "image/" + imageType); //注意:MediaStore.Images.Media.RELATIVE_PATH需要targetSdkVersion=29, //故该方法只可在Android10的手机上执行 values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM); //insertUri表示文件保存的uri路径 insertUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
//获取输出流 ContentResolver resolver = context.getContentResolver();
OutputStream os = resolver.openOutputStream(insertUri)
//伪代码 //将originalFile文件写入到输出流完成图片保存 }


** 在您的应用与分区存储完全兼容之前,您可以使用以下暂时停用分区存储 :**

 - 以 Android 9(API 级别 28)或更低版本为目标平台。
 - 如果您以 Android 10(API 级别 29)或更高版本为目标平台,请在应用的清单文件中将 requestLegacyExternalStorage 的值设置为 true:

  <manifest ... >
    <!-- This attribute is "false" by default on apps targeting
         Android 10 or higher. -->
      <application android:requestLegacyExternalStorage="true" ... >
        ...
      </application>
  </manifest>
  
#### <span id="jump2">在后台运行时访问设备位置信息需要权限
为了让用户更好地控制应用对位置信息的访问权限,Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限,与 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 权限不同,ACCESS_BACKGROUND_LOCATION 权限仅会影响应用在后台运行时对位置信息的访问权限。除非符合以下条件之一,否则应用将被视为在后台访问位置信息:
- 属于该应用的 Activity 可见
- 该应用运行的某个前台设备已声明前台服务类型为 location,要声明您的应用中的某个服务的前台服务类型,请将应用的 targetSdkVersion 或 compileSdkVersion 设置为 29 或更高版本

如果你的应用在Anroid 10或者更高的版本上运行,但你的目标版本低于Android 10,如果您的应用添加了 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限,则系统会在安装期间自动为应用添加 ACCESS_BACKGROUND_LOCATION 权限,如果应用请求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限,系统会自动将 ACCESS_BACKGROUND_LOCATION 权限添加到请求中。

#### <span id="jump3">针对从后台启动 Activity 的限制

从 Android 10 开始,系统会增加针对从后台启动 Activity 的限制。此项行为变更有助于最大限度地减少对用户造成的中断,并且可以让用户更好地控制其屏幕上显示的内容。只要您的应用启动 Activity 是因用户互动直接引发的,该应用就极有可能不会受到这些限制的影响。在特定情况下,如果您的应用需要立即引起用户的注意,可以使用以下方案:
- 创建高优先级通知

创建通知时,请务必添加描述性标题和消息。您还可以选择使用全屏 intent(必须在应用的清单文件中请求 USE_FULL_SCREEN_INTENT 权限。这是普通权限)
- 向用户显示通知

将通知与前台服务相关联, startForeground(notificationId, notification);

#### <span id="jump4"> 设备唯一标识符的变更
从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)

** 注意:从 Google Play 商店安装的第三方应用无法声明特许权限**

受影响的方法包括:

   Build
   getSerial()

   TelephonyManager
   getImei()
   getDeviceId()
   getMeid()
   getSimSerialNumber()
   getSubscriberId()
如果您的应用没有该权限,但您仍尝试查询不可重置标识符的相关信息,则平台的响应会因目标 SDK 版本而异:

如果应用以 Android 10 或更高版本为目标平台,则会发生 SecurityException。
如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回 null 或占位符数据(如果应用具有 READ_PHONE_STATE 权限)。否则,会发生 SecurityException。

### 2. <span id="jumpBehavior">行为变更 
#### 限制非 SDK 接口
非SDK接口限制就是某些SDK中的私用方法,如private方法,你通过Java反射等方法获取并调用了。那么这些调用将在target>=P或target>=Q的设备上被限制使用,当你使用了这些方法后,会报错。如果你想查看你的应用是非使用了非SDK接口,可以使用以下方法:

- 您可以通过在搭载 Android 9(API 级别 28)或更高版本的设备或模拟器上构建和运行可调试应用来测试该应用是否使用非 SDK 接口。请确保您使用的设备或模拟器与应用的目标 API 级别相匹配。在您的应用上运行测试时,如果该应用访问了某些非 SDK 接口,系统就会输出一条日志消息。您可以检查应用的日志消息,查找以下详细信息:

 - 声明的类、名称和类型(采用 Android 运行时所使用的格式)。
 - 访问方式:链接、反射或 JNI
 - 所访问的非 SDK 接口属于哪个名单。

您可以使用 adb logcat 来查看这些日志消息,这些消息显示在所运行应用的 PID 下。举例而言,日志中可能包含如下条目:

    Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
- 使用 veridex 工具进行测试

您可以在 APK 上运行静态分析工具 [veridex](https://android.googlesource.com/platform/prebuilts/runtime/+/master/appcompat)。下载解压后打开命令行切换到veridex目录下运行命令:

    ./appcompat.sh --dex-file=your-app.apk

veridex 工具会扫描 APK 的整个代码库(包括所有第三方库), 扫描结果分为两部分,一部分为被调用的非SDK接口的位置,另一部分为非SDK接口数量统计。
   - 开发者自己编写的反射

   解决方案:此时我们需要讲使用非SDK接口迁移到SDK接口
   - 第三方SDK中使用了非SDK接口

   解决方案:提交工单或者换第三方SDK

#### 针对全屏 Intent 的权限变更
如果应用以 Android 10 或更高版本为目标平台并使用涉及全屏 intent 的通知,则必须在应用的清单文件中请求 USE_FULL_SCREEN_INTENT 权限。这是普通权限,因此,系统会自动为请求权限的应用授予此权限,如果以 Android 10 或更高版本为目标平台的应用尝试创建使用全屏 intent 的通知而未请求必要权限,则系统会忽略此全屏 intent 
### 3.<span id="jumpNew"> 新功能和 API
#### 深色主题
应用的深色主题背景将与系统控制的夜间模式标记相关联

有以下两种方式实现深色主题:
- 设置主题方式

  将主题设置为继承 DayNight 主题背景

      <style name="AppTheme" parent="Theme.AppCompat.DayNight">
  您还可以使用 MaterialComponent 的深色主题背景:
      <style name="AppTheme" parent="Theme.MaterialComponents.DayNight"> 
如要切换主题背景,调用 AppCompatDelegate.setDefaultNightMode(),在 AppCompat v1.1.0 后会自动重新创建所有已启动的 Activity       
- Force Dark方式
        
 Force Dark 可让开发者快速实现深色主题背景,而无需明确设置 DayNight 主题背景,如果你想启用Force Dark,在主题背景设置:
        
     android:forceDarkAllowed="true"
 如果你想在单独的视图上停用Force Dark,在布局属性上添加 ```android:forceDarkAllowed```或者代码调用 ```setForceDarkAllowed()```
        
 ** 注意如果你使用了深色主题或者使用了DayNight主题背景,系统将不会应用Force Dark  **     
#### 前台服务类型
Android 10 引入了 foregroundServiceType XML 清单属性,您可以将其包含在多项特定服务的定义中。虽然很少适用,但您可以为一项特定服务分配多个前台服务类型。

下表显示了不同的前台服务类型,以及适合在其中声明特定类型的服务:
        
| 前台服务类型 |应声明相应类型的服务的示例使用情形 
| ------ | ------ | ------ |
| connectedDevice | 监控穿戴式设备健身跟踪器 |
| dataSync | 从网络下载文件 | 
|location|延续用户发起的操作|
|mediaPlayback|播放有声读物、播客或音乐|
|mediaProjection|简短地录屏|
|phoneCall|处理正在进行的通话|