AndroidQ适配手册

Android预计在今年Q3发布androidQ的最终版本, 8月份Android发布了Beta6版本, 距离Final Release也不远了, 针对Q的适配已经迫在眉睫!

针对Q的隐私权和行为变更, 我们主要焦点还是着重于, 针对于所有应用(不论targetSdkVersion是否为Q的应用)都有影响的变更上来看

Q 针对所有应用隐私权和行为变更

分区存储

在当前的Beta5版本内, 分区存储的只应用在部分应用上, 但是, 明年无论是所有应用均需要分区存储, 所以应用需要提前确保支持分区存储.

默认情况下, 都需要通过Context.getExternalFilesDir())来获取过滤视图, 如果存在文件应用卸载时不应该删除的, 应该通过MediaProvider存储在共享集合内(如拍照的图片), 同时, 应用请求过滤视图内的文件, 不再需要请求对应的读写权限.而访问外部存储文件, 如 /sdcard/DCIM/IMG1024.JPG 等路径文件,应用必须使用 MediaStore,并调用 openFile()) 等方法, 同时需要请求READ_EXTERNAL_STORAGE权限才可访问

具体适配需要参考官网的建议

针对后台启动Activity的限制

在未与用户进行交互的前提下启动Activity存在该条限制, 如果存在该场景的应用(比如闹钟, 语音, 视频电话等), 需要通过通知提醒的方式解决

增加针对后台定位的权限

AndroidQ引入了新的权限ACCESS_BACKGROUND_LOCATION, 用来授予是否允许后台定位, 在低于Q的应用版本上, 如果原来的清单中申请了定位权限, 会自动加上ACCESS_BACKGROUND_LOCATION权限, 但是用户仍然可以通过拒绝授权, 导致后台定位失败. 这个限制主要出现在存在导航或者智能家居操作的应用中. 我们的应用中可以不做处理

设备唯一标识符

三方引用既无法通过READ_PHONE_STATE老权限, 无法通过申请READ_PRIVILEGED_PHONE_STATE新权限(需要系统签名)来读取deviceId, 依赖于deviceId数据上报的接口需要额外适配, Android提供推荐做法, 但是它允许用户重置标识符, 需要根据具体应用场景设计唯一标识符.另外通过WifiInfo.getMacAddress()) 获取Mac地址的将只能获取固定的值02:00:00:00:00:00

相机和网络连接的变更

访问所有相机信息均需要获取权限, AndroidQ更改了 getCameraCharacteristics()) 方法默认返回的信息的广度。具体而言,应用必须具有 CAMERA 权限才能访问此方法的返回值中可能包含的设备特定元数据。

非SDK接口限制

从android P开始做了非SDK接口的限制, Q版本更新了对应的非SDK接口列表, 需要测试并根据接口的不同受限情况进行相对适配

targetSdkVersion限制

当前最低目标版本需要保证在23以上

分区存储兼容性处理

为了改进当前Android手机文件夹混乱的现象, 在Android Q, Google出了分区存储的政策, 开发者将无法通过Environment.getExternalStorageState()访问文件.

存储空间特性

所有Android设备都存在两个文件存储空间: 内部存储和外部存储.在Android早期, 内部存储代表的是内置的存储器, 而外部存储表示可移动的存储介质(譬如sd卡), 现在大部分设备无论是否存在可移动存储介质, 这两个存储空间都会存在. 而无论外部存储是否可移动, 在其API行为上, 是没有任何区别的, 当然我们可以通过以下代码去判断外部存储是否支持去读写

1
2
3
fun isExternalStorageWritable(): Boolean {
return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}

在Android Q之前的手机上, 对于外部存储的文件的读写没有任何限制, 在获取外部存储的相关读写权限后, 就可以随意新建目录, 导致文件目录非常混乱

根据下图, 我们可以看到在Android Q启动了分区存储的功能后, 外部访问范围将仅限定于三个区域, 分别是过滤视图(app-specific), 共享媒体区域, 以及下载文件区域

存储空间P和Q的区别

首先, 我们可以看下这三个区域的区别

文件位置 需要的权限 访问方式 当应用卸载的时候, 是否会删除文件
过滤视图(App-specific directory) getExternalFilesDir
媒体资源集合(照片, 视频, 语音) READ_EXTERNAL_STORAGE(当访问其他应用的文件时) MediaStore
下载文件 SAF(加载系统的文件管理器)

过滤视图

通过哪些API可以访问到过滤视图?

  • Context.getExternalFilesDir()
    • e.g. /sdcard/Android/data/packageName/files
  • Context.getExternalCacheDirs()
    • e.g. /sdcard/Android/data/packageName/cache
  • Context.getExternalMediaDirs()
    • e.g. /sdcard/Android/media/packageName/
  • Context.getObbDirs()
    • e.g. /sdcard/Android/obb/packageName/

在Android P中, 应用可以通过READ_EXTERNAL_STORAGE权限访问外部存储中所有路径文件, 而针对于过滤视图(原先叫共享存储空间, Shared Storage), 是可以不通过权限就可以直接访问读写的.

在Android Q中, 应用只可以直接访问应用自身的过滤视图, 而共享媒体资源仅可通过MediaStore来访问, 同时, 其他文件需要通过SAF进行访问, 假设直接访问过滤视图目录外的目录文件, 则会抛出异常. 这样使得文件存储更加规范, 也使文件访问变得更为安全.

MediaStore

MediaStore在Android 1就已经存在, 在Android Q 中进行了加强. 他主要用于存储用户行为生成的媒体资源文件(且这些资源文件应是应用卸载后用户仍然希望保存的文件), 当我们需要访问媒体文件的时候, 我们需要达成两个条件:

  1. 拥有READ_EXTERNAL_STORAGE权限
  2. 对应访问文件位于以下明确定义的媒体集合中
    1. 照片, 存储在MediaStore.Images
    2. 音频, 存储在MediaStore.Audio
    3. 视频, 存储在 MediaStore.Video
MediaStore Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
val resolver = context.getContentResolver()

// 打开指定的媒体列
resolver.openFileDescriptor(item, mode).use { pfd ->
// todo something
}

// 加载缩略图
val mediaThumbnail = resolver.loadThumbnail(item, Size(640, 480), null)

// 查找所有视频资源
// 包括被标记独占访问的资源(被IS_PENDING标记的资源)
val collection = MediaStore.Video.Media.getContentUri(volumeName)
val collectionWithPending = MediaStore.setIncludePending(collection)
resolver.query(collectionWithPending, null, null, null).use { c ->
// ...
}

// 将指定的媒体文件发布到外部存储空间内
val values = ContentValues().apply {
put(MediaStore.Audio.Media.RELATIVE_PATH, "Video/My Videos")
put(MediaStore.Audio.Media.DISPLAY_NAME, "My Video.mp4")
}
val item = resolver.insert(collection, values)

这里需要注意的是, 在Q版本新增了两个标记

  1. IS_PENDING标记用来表示标记应用具有媒体访问独占权
  2. RELATIVE_PATH 自定义指定相对路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "IMG1024.JPG")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.IS_PENDING, 1)
}

// 通知指定资源文件发布在外部存储空间内
val resolver = context.getContentResolver()
val collection = MediaStore.Images.Media
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item = resolver.insert(collection, values)

resolver.openFileDescriptor(item, "w", null).use { pfd ->
// 写文件
FileOutputStream(pfd.fileDescriptor).write(byte[])
}

// 当我们文件已写入资源集合, 则将IS_PENDING改为0, 表示释放媒体访问独占权
// 允许其他应用访问这个资源
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(item, values, null, null)

另外, 在Android Q中, DATA标记已经被废弃, 在Q之前, 我们可以通过它去获取文件的绝对路径用来访问文件, 在Q乃至之后版本, 我们只能通过ContentResolver#openFileDescriptor(Uri, String)去访问对应的文件

编辑其他应用的媒体文件

理论上, 我们无法去编辑(写, 删除操作)其他应用提供给MediaStore的媒体文件, 当我们去编辑的时候, 会抛出一个RecoverableSecurityException异常, 我们可以通过捕获这个异常, 并请求用户授权针对该文件进行写操作

1
2
3
4
5
6
7
8
try {
// ...
} catch (rse: RecoverableSecurityException) {
val requestAccessIntentSender = rse.userAction.actionIntent.intentSender

startIntentSenderForResult(requestAccessIntentSender, requestCode,
null, 0, 0, 0, null)
}

相关Demo可以看下PhotoQSelector