摘要
1、本文档基于谷歌AndroidQ官方文档和一加Q版本应用兼容性整改指导 2、本文档主要对影响比较大的部分进行简单总结,内容并不全面; 3、版本号对应关系:
Android-Q = Android-10 = Api29 Android-P = Android-9.0 = Api28
一、Android Q 行为变更:版本新特性
权限受影响应用如何启用(影响范围)存储权限访问和共享外部存储设备中的文件的应用adb shell sm set-isolated-storage on(下文详述)定位权限在后台时请求访问用户位置信息的应用这种权限策略在 Android Q 上始终处于启用状态从后台启动Activity不需要用户互动就启动 Activity 的应用关闭允许系统执行后台活动开发者选项即可启用限制设备标识符(deviceId)访问设备序列号或 IMEI 的应用在搭载 Android Q 的设备上安装应用无线扫描权限使用 WLAN API 和 Bluetooth API 的应用以 Android Q 为目标平台上面的内容官方文档将这一部分内容独立于Q 行为变更:所有应用来介绍,是因为这一部分内容庞大且重要,其中最大的更新就是用户隐私权限变更. 因为无线扫描权限这种权限的变更影响较少。本文不作详述,如有涉及请查阅官方文档。
二、兼容性适配
以下对各个权限分别作介绍以及解决方法。
1、存储权限
Android Q 在外部存储设备中为每个应用提供了一个“隔离存储沙盒”(例如 /sdcard)。任何其他应用都无法直接访问您应用的沙盒文件。由于文件是您应用的私有文件,因此您不再需要任何权限即可在外部存储设备中访问和保存自己的文件。此变更可让您更轻松地保证用户文件的隐私性,并有助于减少应用所需的权限数量。
沙盒,简单而言就是应用私有专属文件夹,并且访问这个文件夹无需权限。
谷歌官方推荐应用在沙盒内存储文件的地址为 Context.getExternalFilesDir()下的文件夹。比如要存储一张图片,则应放在Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)中。
以下将按访问的目标文件的地址介绍如何适配。
所以请判断当应用运行在Q平台上时,取消对READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE两个权限的申请。并替换为新的媒体特定权限。
影响范围
Google 把 Android Q 上会被沙箱化条件设为 Target SDK 至少为 Q (29) 的应用或者运行 Android Q 时全新安装的应用。不符合这个条件的应用将会运行在兼容模式下,在兼容模式中应用行为大致和过去相同,以保证不会出现严重的数据丢失问题。兼容模式在应用重新安装后会被关闭。 注意:即使应用Target SDK <29也会被沙箱化
影响点
情况描述
=Q,默认启用过滤视图,应用以外的文件需要通过存储访问框架(SAF,StorageAccessFramework)读写。
解决方法
方法一、停用过滤视图,使用旧版存储模式
...
方法二、将文件存储到过滤视图中,官方推荐。
// /Android/data/com.example.androidq/files/Documents File dir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
优点:不用申请读写权限; 缺点:随应用卸载而删除;
方法三、使用存储访问框架(SAF),由用户指定要读写的文件。 这个功能Android 4.4(API: 19)就有,参考官方文档。
方法四、获取用户指定的某个目录的读写权限 从Android5.0(Api 21)开始就有,官方文档。
步骤
1. 申请目录的访问权限 会打开系统的文件目录,由用户自己选择允许访问的目录,不用申请WRITE/READ_EXTERNAL_STORAGE权限。
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);startActivityForResult(intent, REQ_CODE);
允许了之后通过onActivityResult()的intent.getData()得到该目录的Uri,通过Uri可获取子目录和文件。这种方式的缺点是应用重装后权限失效,即使可以保存了这个Uri也没用。
Uri dirUri = intent.getData();// 持久化;应用重装后权限失效,即使知道这个uri也没用SPUtil.setValue(this, SP_DOC_KEY, dirUri.toString());//重要:少这行代码手机重启后会失去权限getContentResolver().takePersistableUriPermission(dirUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
2. 通过Uri读写文件
- 创建文件
// 在mUri目录(‘DuoKan’目录)下创建'test.txt'文件private void createFile() { DocumentFile documentFile = DocumentFile.fromTreeUri(this, mUri); DocumentFile file = documentFile.createFile("text/plain", "test.txt"); if (file != null }}
主要用到DocumentFile类,和File类的方法类似,有isFile、isDirectory、exists、listFiles等方法
- 删除文件
//删除"test.txt"private void deleteFile() { DocumentFile documentFile = DocumentFile.fromTreeUri(this, mUri); // listFiles(),列出所有的子文件和文件夹 for (DocumentFile file : documentFile.listFiles()) { if (file.isFile() LogUtil.log("deleteFile: " + delete); break; } }}
- 写入数据
private void writeFile(Uri uri) { try { ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w"); //这种方法会覆盖原来文件内容 OutputStreamWriter output = new OutputStreamWriter(new FileOutputStream(pfd.getFileDescriptor())); // 不能传uri.toString(),否则FileNotFoundException // OutputStreamWriter output = new OutputStreamWriter(new FileOutputStream(uri.toString(), true)); output.write("这是一段文件写入测试\n"); output.close(); LogUtil.log("写入成功。"); } catch (IOException e) { LogUtil.log(e); }}
2、定位权限
为了让用户更好地控制应用对位置信息的访问权限,Android Q 引入了新的位置权限ACCESS_BACKGROUND_LOCATION。与现有的 ACCESS_FINE_LOCATION 和ACCESS_COARSE_LOCATION 权限不同,新权限仅会影响应用在后台运行时对位置信息的访问权。除非应用的某个 Activity 可见或应用正在运行前台服务,否则应用将被视为在后台运行。
与iOS系统一样,Q中也加入了后台位置权限ACCESS_BACKGROUND_LOCATION,如果应用需要在后台时也获得用户位置(比如滴滴),就需要动态申请ACCESS_BACKGROUND_LOCATION权限。
当然如果不需要的话,应用就无需任何改动,且谷歌会按照应用的targetSDK作出不同处理: targetSDK <= P 应用如果请求了ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限,Q设备会自动帮你申请ACCESS_BACKGROUND_LOCATION权限。
情况描述
- targetSdkVersion
- targetSdkVersion>=Q,需申请;
- 应用变为后台应用90s后开始定位失败(Pixel AndroidQ-beta6)
解决方法
动态申请即可;
启动前台服务
3、禁止后台启动activity
官方文档
情况描述
解决方法
发送全屏通知:
//AndroidManifest 声明新权限,不用动态申请Intent intent = new Intent(this, ScopedStorageActivity.class);PendingIntent pendingIntent = PendingIntent.getActivity(this, REQ_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT);Notification notification = new NotificationCompat.Builder(this, Constants.CHANNEL_ID) .setSmallIcon(R.drawable.ic_launcher_foreground) .setContentTitle("Incoming call") .setContentText("(919) 555-1234") .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_ALARM) //设置全屏通知后,发送通知直接启动Activity .setFullScreenIntent(pendingIntent, true) .build();NotificationManager manager = getSystemService(NotificationManager.class);manager.notify(445456, notification);
但是:在华为mate20(Api-28)上需要到设置中打开横幅通知;原生AndroidQ(beta6)上有效。
4、设备硬件标识符访问限制
限制应用访问不可重设的设备识别码,如 IMEI、序列号等,系统应用不受影响。
原来的做法
TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);tm.getDeviceId();Build.getSerial();
- 如targetSdkVersion
- 如targetSdkVersion>=Q,抛异常:
SecurityException: getDeviceId: The user 10196 does not meet the requirements to access device identifiers.
解决方案
方案一: 使用AndroidId代替,缺点是应用签署密钥或用户(如系统恢复出产设置)不同返回的Id不同。与实际测试结果相符。 经实际测试:相同签名密钥的不同应用androidId相同,不同签名的应用androidId不同。恢复出产设置或升级系统没测。
String androidId = Settings.System.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
方案二:
通过硬件信息拼接,缺点是还是不能保证唯一。 经测试:似乎与方案一比更稳定,不受密钥影响,但非官方建议,没安全感。
private static String makeDeviceId(Context context) { String deviceInfo = new StringBuilder() .append(Build.BOARD).append("#") .append(Build.BRAND).append("#") //CPU_ABI,这个值和appp使用的so库是arm64-v8a还是armeabi-v7a有关,舍弃 //.append(Build.CPU_ABI).append("#") .append(Build.DEVICE).append("#") .append(Build.DISPLAY).append("#") .append(Build.HOST).append("#") .append(Build.ID).append("#") .append(Build.MANUFACTURER).append("#") .append(Build.MODEL).append("#") .append(Build.PRODUCT).append("#") .append(Build.TAGS).append("#") .append(Build.TYPE).append("#") .append(Build.USER).append("#") .toString(); try { //22a49a46-b39e-36d1-b75f-a0d0b9c72d6c return UUID.nameUUIDFromBytes(deviceInfo.getBytes("utf8")).toString(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String androidId = Settings.System.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); return androidId;}
参考文献
官方文档