上周开发项目的时候,做了Android11存储方案升级适配,因为之前创建的文件是在外部公有目录下,为了延续老用户的使用习惯,项目组决定继续在外部公有目录下写入、读取文件的方案。为了拿到相应的访问权限,用到了下面这个标签。
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
官方文档上也有介绍,我这里截取了关键部分
做了相关编码后,本以为就万事大吉了,哪成想Google Play上不去。原因是因新冠疫情,对这个标签的适配延后到2021年初。所以现阶段带这个标签的APP上不了Google Play。(这是个人经历,不具有权威性)
现在开始采用第二种方案进行开发,主要有两点:
1.在Android11平台上在应用专属目录存储下进行文件的读、写;
2.将旧版公有目录下的数据迁移到新的专属目录中;
(一)
针对第一点,有两个可用区域:内部专属存储空间和外部专属存储空间
内部专属存储空间目录是这样的:/data/user/0/packagename/files。
您的应用不需要任何系统权限即可读取和写入这些目录中的文件。其他应用无法访问存储在内部存储空间中的文件。这使得内部存储空间非常适合存储其他应用不应访问的应用数据。但是,请注意,这些目录的空间通常比较小。在将应用专属文件写入内部存储空间之前,应用应查询设备上的可用空间。卸载应用后,系统会移除这些目录中存储的文件。
这里官方提供了查询可用空间的示例
// App needs 10 MB within internal storage.
const val NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;
val storageManager = applicationContext.getSystemService<StorageManager>()!!
val appSpecificInternalDirUuid: UUID = storageManager.getUuidForPath(filesDir)
val availableBytes: Long =
storageManager.getAllocatableBytes(appSpecificInternalDirUuid)
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
storageManager.allocateBytes(
appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP)
} else {
val storageIntent = Intent().apply {
action = ACTION_MANAGE_STORAGE
}
// Display prompt to user, requesting that they choose files to remove.
}
外部专属存储空间目录是这样的:/storage/emulated/0/Android/data/packagename/files。
在 Android 4.4(API 级别 19)或更高版本中,应用无需请求任何与存储空间相关的权限即可访问外部存储空间中的应用专属目录。卸载应用后,系统会移除这些目录中存储的文件。
另外,还需考虑存储空间满了之后做的事情。
第一种是为用户提供友好的的操作界面,让用户决定删除哪些文件。可以在自己app中配置,也可以用系统提供的;
第二种是我们设计好删除规则,直接用代码进行删除操作,让用户无感知。
当因存储空间满而无法写入成功,会出IO异常,具体异常信息如下:
java.io.IOException: write failed: ENOSPC (No space left on device)
(二)
针对第二点,官方文档又区分了目标平台:Android11、Android10。我这里针对的目标平台为Android11。
在系统权限这块需要做以下操作:
1. 在manifest文件application块下声明标签 android:preserveLegacyExternalStorage="true",目的是保留旧版存储模型,以便在用户升级到以 Android 11 为目标平台的新版应用时,应用可以迁移用户的数据。
2.在manifest文件application块下声明标签 android:requestLegacyExternalStorage="true",继续停用分区存储,以便应用可以继续在搭载 Android 10 的设备上访问旧版存储位置中的文件。
然后就是在业务中,进行文件数据的拷贝迁移操作了。这里贴下代码,可以参考下
private void beganMigrateOldFiles(String newDirectoryName, File oldFile) {
if (null != oldFile && oldFile.exists()) {
if (TextUtils.isEmpty(newDirectoryName))
newDirectoryName = getInnerDirectoryPath();
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
File newFile = null;
byte[] buffer = new byte[1024 * 1 << 2];
if (oldFile.isDirectory()) {
if (oldFile.getName().endsWith("sushi")) {
newDirectoryName = newDirectoryName + File.separator;
} else {
newDirectoryName = newDirectoryName + File.separator + oldFile.getName();
}
newFile = new File(newDirectoryName);
newFile.mkdir();
File[] oldFiles = oldFile.listFiles();
if (null != oldFiles && oldFiles.length != 0) {
for (File file : oldFiles) {
if (null == file || !file.exists() || file.length() == 0 || !file.canRead())
continue;
beganMigrateOldFiles(newDirectoryName, file);
}
}
} else {
try {
fileInputStream = new FileInputStream(oldFile);
String newFileName = newDirectoryName + File.separator + oldFile.getName();
newFile = new File(newFileName);
if (!newFile.exists()) {
fileOutputStream = new FileOutputStream(newFile);
int len;
while ((len = fileInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
}
fileOutputStream.flush();
}
} catch (Exception e) {
if (newFile != null && newFile.exists())
newFile.delete();
if (e.toString().contains("ENOSPC")) {
}
} finally {
try {
if (null != fileOutputStream)
fileOutputStream.close();
if (null != fileInputStream)
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
最后稍微提下,如果要做文件加密,可以使用Jetpack中提供的Security库,可以安全地管理密钥并对文件和 sharedpreferences 进行加密。