Android 11(API 级别 30)进一步增强了平台功能,为外部存储设备上的应用和用户数据提供了更好的保护,此版本强制执行分区存储(Scoped Stroage),无论APP的targetsdkversion是多少,都将无法访问Android/data和Android/obb这二个应用私有目录。这无疑对会部分APP的业务场景及用户体验造成冲击,如文件管理类软件的清理缓存功能。
关于分区存储
为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储空间的分区访问权限(即分区存储)。此类应用只能访问外部存储空间上的应用专属目录,以及本应用所创建的特定类型的媒体文件,不能访问其他应用的外部存储空间。
暂时停用分区存储
在您的应用与分区存储完全兼容之前,您可以使用以下方法之一暂时停用分区存储:
- 以 Android 9(API 级别 28)或更低版本为目标平台。
- 如果您以 Android 10(API 级别 29)或更高版本为目标平台,请在应用的清单文件中将
requestLegacyExternalStorage
的值设置为true
当您将应用更新为以 Android 11(API 级别 30)为目标平台后,如果应用在搭载 Android 11 的设备上运行,系统会忽略
requestLegacyExternalStorage
属性。
关于SAF (Storage Access Framework)
Android 4.4(API 级别 19)引入了存储访问框架 (SAF)。借助 SAF,用户可轻松浏览和打开各种文档、图片及其他文件,而不用管这些文件来自其首选文档存储提供程序中的哪一个。用户可通过易用的标准界面,跨所有应用和提供程序以统一的方式浏览文件并访问最近用过的文件。云存储服务或本地存储服务可实现用于封装其服务的
DocumentsProvider
,从而加入此生态系统。客户端应用如需访问提供程序中的文档,只需几行代码即可
Android R实现访问外部存储的Android/data方案
1.通过Intent启动SAF授权界面,导航到Android/data目录,注意URI的百分号编解码(%3A和%2F)
public void requestAccessAndroidData(Activity activity){
try {
Uri uri = Uri.parse("content://com.android.externalstorage.documents/document/primary%3AAndroid%2Fdata");
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
activity.startActivityForResult(intent, REQUEST_CODE);
} catch (Exception e) {
e.printStackTrace();
}
}
2.在用户同意授权后,持久化uri权限(否则关机重启或授权界面finish后,APP就无权限访问了),并只能通过DocumentFile进行业务操作,File API操作是无效的,此授权只是授权uri操作,并未授权文件系统
private static final int REQUEST_CODE = 8082;
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case REQUEST_CODE:
if (resultCode == Activity.RESULT_OK) {
getContentResolver().takePersistableUriPermission(data.getData(),
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Log.i(TAG,"Access persist uri permission to Android/data");
}
break;
default:
break;
}
}
3.注意这个授权用户是可以撤回的,通过点击应用信息界面的存储,就会看到撤回界面,所以业务需要去动态判断
public boolean isGrantAndroidData(Context context) {
for (UriPermission persistedUriPermission : context.getContentResolver().getPersistedUriPermissions()) {
if (persistedUriPermission.getUri().toString().
equals("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata")) {
return true;
}
}
return false;
}
4.清理缓存
public void deletePackagesCache(String packageName){
String filePath= Environment.getExternalStorageDirectory() + "/Android/data/" + packageName +"/cache";
String pathUri = filePath.replace("/storage/emulated/0/", "").replace("/", "%2F");
Uri dirUri=Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A" + pathUri);
Cursor cursor=mContext.getContentResolver().query(dirUri, new String[]{DocumentsContract.Document.COLUMN_DOCUMENT_ID}, null, null, null);
try{
while (cursor.moveToNext()) {
String documentId = cursor.getString(0);
Uri uri = DocumentsContract.buildDocumentUriUsingTree(dirUri, documentId);
DocumentFile documentFile=DocumentFile.fromSingleUri(mContext,uri);
Boolean isDeleteSuccess = documentFile.delete();
Log.e(LOG_TAG,"ACTION_GARBAGE_CACHE delete file="+documentFile.getName() + " isDeleteSuccess="+isDeleteSuccess);
}
}finally {
if(cursor!=null){
cursor.close();
}
}
}
MANAGE_EXTERNAL_STORAGE 权限
1.在AndroidManifest.xml中声明MANAGE_EXTERNAL_STORAGE权限。
2. 发出一个action为Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION的Intent,引导用户手动授权。
3. 调用Environment.isExternalStorageManager()来判断用户是否已授权。
Android访问文件的方式
- MediaStore
- File
- DocumentFile