Android 10/11 存储适配完全指南:MediaStore、SAF与权限管理
java/*** Android存储适配最佳实践检查清单*//*** 适配检查清单*/// 基础配置"✅ 在AndroidManifest.xml中声明必要的权限","✅ 为Android 10.0设置requestLegacyExternalStorage标志(过渡期)","✅ 更新targetSdkVersion到最新稳定版本",// 数据存储位置"✅ 敏感数据存储在内部存储","✅ 大文件存
·
一、分区存储核心机制深度解析
1.1 存储架构与访问权限矩阵
java
/**
* Android存储权限与访问方式矩阵
* 展示不同存储区域在不同Android版本下的访问方式
*/
public class StorageAccessMatrix {
public enum StorageArea {
INTERNAL_APP_PRIVATE, // /data/data/<package>/
EXTERNAL_APP_PRIVATE, // /storage/emulated/0/Android/data/<package>/
SHARED_MEDIA, // DCIM, Pictures, Music等
SHARED_DOCUMENTS, // Documents, Downloads等
OTHER_PUBLIC_DIRS // /sdcard/下的自定义目录
}
public enum AccessMethod {
DIRECT_PATH, // 直接路径访问
MEDIA_STORE, // 通过MediaStore API
SAF, // 存储访问框架
MANAGED_ACCESS // 管理所有文件权限
}
/**
* 获取存储访问策略矩阵
*/
public static Map<StorageArea, AccessMethod[]> getAccessMatrix(
int sdkVersion, int targetSdkVersion, boolean hasLegacyFlag) {
Map<StorageArea, AccessMethod[]> matrix = new HashMap<>();
// 内部应用私有存储 - 始终可用
matrix.put(StorageArea.INTERNAL_APP_PRIVATE,
new AccessMethod[]{AccessMethod.DIRECT_PATH});
// 外部应用私有存储 - 始终可用
matrix.put(StorageArea.EXTERNAL_APP_PRIVATE,
new AccessMethod[]{AccessMethod.DIRECT_PATH});
// 共享媒体文件
if (sdkVersion < Build.VERSION_CODES.Q) {
matrix.put(StorageArea.SHARED_MEDIA,
new AccessMethod[]{AccessMethod.DIRECT_PATH, AccessMethod.MEDIA_STORE});
} else if (sdkVersion == Build.VERSION_CODES.Q && hasLegacyFlag) {
// Android 10且启用了传统存储
matrix.put(StorageArea.SHARED_MEDIA,
new AccessMethod[]{AccessMethod.DIRECT_PATH, AccessMethod.MEDIA_STORE});
} else if (sdkVersion >= Build.VERSION_CODES.Q) {
// Android 10+分区存储
matrix.put(StorageArea.SHARED_MEDIA,
new AccessMethod[]{AccessMethod.MEDIA_STORE});
}
// 共享文档和其他文件
matrix.put(StorageArea.SHARED_DOCUMENTS,
new AccessMethod[]{AccessMethod.SAF, AccessMethod.MEDIA_STORE});
// 其他公共目录
if (sdkVersion < Build.VERSION_CODES.Q) {
matrix.put(StorageArea.OTHER_PUBLIC_DIRS,
new AccessMethod[]{AccessMethod.DIRECT_PATH});
} else if (sdkVersion >= Build.VERSION_CODES.R) {
// Android 11+可以申请管理所有文件权限
matrix.put(StorageArea.OTHER_PUBLIC_DIRS,
new AccessMethod[]{AccessMethod.MANAGED_ACCESS, AccessMethod.SAF});
} else {
// Android 10无管理权限,只能通过SAF
matrix.put(StorageArea.OTHER_PUBLIC_DIRS,
new AccessMethod[]{AccessMethod.SAF});
}
return matrix;
}
}
二、MediaStore API 深度应用
2.1 MediaStore 数据库结构解析
java
/**
* MediaStore数据库结构与访问机制
*/
public class MediaStoreAnalyzer {
/**
* MediaStore数据库表结构概览
*/
public static class MediaStoreTables {
// 核心表:files表,包含所有媒体文件
// 各类型媒体通过视图访问:
// - images视图:SELECT * FROM files WHERE media_type = 1
// - video视图:SELECT * FROM files WHERE media_type = 3
// - audio视图:SELECT * FROM files WHERE media_type = 2
// - downloads视图:SELECT * FROM files WHERE media_type = 4
// 关键字段演变:
// Android 10.0之前:DATA字段存储绝对路径
// Android 10.0之后:DATA字段废弃,使用RELATIVE_PATH + DISPLAY_NAME
}
/**
* 统一的MediaStore文件操作类
*/
public class MediaStoreOperations {
private final ContentResolver contentResolver;
public MediaStoreOperations(Context context) {
this.contentResolver = context.getContentResolver();
}
/**
* 插入图片到相册(兼容所有Android版本)
*/
public Uri insertImageToGallery(String displayName, InputStream imageStream,
int width, int height) throws IOException {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, displayName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
// 设置图片尺寸信息
values.put(MediaStore.Images.Media.WIDTH, width);
values.put(MediaStore.Images.Media.HEIGHT, height);
// Android版本兼容处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ 使用RELATIVE_PATH
values.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + File.separator + "MyApp");
// 设置IS_PENDING标志,文件写入完成前其他应用无法访问
values.put(MediaStore.Images.Media.IS_PENDING, 1);
} else {
// Android 9.0及以下使用DATA字段
File picturesDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
File imageFile = new File(picturesDir, "MyApp" + File.separator + displayName);
values.put(MediaStore.Images.Media.DATA, imageFile.getAbsolutePath());
}
// 插入数据库记录
Uri imageUri = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (imageUri == null) {
throw new IOException("Failed to create media record");
}
try {
// 写入图片数据
OutputStream os = contentResolver.openOutputStream(imageUri);
if (os == null) {
throw new IOException("Failed to open output stream");
}
// 复制数据
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = imageStream.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
imageStream.close();
// Android 10+:清除IS_PENDING标志,使文件可被其他应用访问
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
contentResolver.update(imageUri, values, null, null);
}
return imageUri;
} catch (IOException e) {
// 写入失败,删除数据库记录
contentResolver.delete(imageUri, null, null);
throw e;
}
}
/**
* 查询相册中的图片(支持多种查询条件)
*/
public List<ImageInfo> queryImages(Date afterDate, long minSize, long maxSize) {
List<ImageInfo> images = new ArrayList<>();
// 构建查询条件
StringBuilder selection = new StringBuilder();
List<String> selectionArgs = new ArrayList<>();
if (afterDate != null) {
selection.append(MediaStore.Images.Media.DATE_ADDED + " > ?");
selectionArgs.add(String.valueOf(afterDate.getTime() / 1000));
}
if (minSize > 0) {
if (selection.length() > 0) selection.append(" AND ");
selection.append(MediaStore.Images.Media.SIZE + " >= ?");
selectionArgs.add(String.valueOf(minSize));
}
if (maxSize > 0) {
if (selection.length() > 0) selection.append(" AND ");
selection.append(MediaStore.Images.Media.SIZE + " <= ?");
selectionArgs.add(String.valueOf(maxSize));
}
// 排序条件
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
// 查询的列
String[] projection = {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT
};
try (Cursor cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection.length() > 0 ? selection.toString() : null,
selectionArgs.toArray(new String[0]),
sortOrder)) {
if (cursor != null) {
int idIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
int nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
int sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE);
int dateIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED);
while (cursor.moveToNext()) {
long id = cursor.getLong(idIndex);
String name = cursor.getString(nameIndex);
long size = cursor.getLong(sizeIndex);
long dateAdded = cursor.getLong(dateIndex);
// 构造Uri
Uri imageUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
images.add(new ImageInfo(id, name, imageUri, size, dateAdded));
}
}
} catch (Exception e) {
e.printStackTrace();
}
return images;
}
/**
* 删除媒体文件(需要用户确认)
*/
public boolean deleteMediaFile(Uri mediaUri) {
try {
// 检查是否有删除权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+ 需要用户明确授权
List<Uri> urisToDelete = new ArrayList<>();
urisToDelete.add(mediaUri);
Intent deleteIntent = new Intent(Intent.ACTION_DELETE);
deleteIntent.setType("vnd.android.cursor.dir/image");
deleteIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
deleteIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,
new ArrayList<>(urisToDelete));
// 需要启动Activity让用户确认
// 此处仅展示逻辑,实际需要处理Activity结果
return false;
} else {
// Android 10及以下,应用可以删除自己创建的媒体文件
int rowsDeleted = contentResolver.delete(mediaUri, null, null);
return rowsDeleted > 0;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
public static class ImageInfo {
public long id;
public String displayName;
public Uri uri;
public long size;
public long dateAdded;
public ImageInfo(long id, String displayName, Uri uri, long size, long dateAdded) {
this.id = id;
this.displayName = displayName;
this.uri = uri;
this.size = size;
this.dateAdded = dateAdded;
}
}
}
2.2 MediaStore性能优化与批量操作
java
/**
* MediaStore批量操作与性能优化
*/
public class MediaStoreBatchOperations {
/**
* 批量插入图片(减少ContentResolver调用次数)
*/
public List<Uri> batchInsertImages(Context context, List<ImageData> images) {
List<Uri> insertedUris = new ArrayList<>();
ArrayList<ContentValues> valueList = new ArrayList<>();
// 1. 准备批量数据
for (ImageData image : images) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, image.fileName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/MyApp/Batch");
values.put(MediaStore.Images.Media.IS_PENDING, 1);
}
valueList.add(values);
}
// 2. 批量插入数据库记录
ContentResolver resolver = context.getContentResolver();
ContentValues[] valuesArray = valueList.toArray(new ContentValues[0]);
int insertedCount = resolver.bulkInsert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, valuesArray);
if (insertedCount != images.size()) {
// 批量插入失败,回退到单条插入
return insertImagesIndividually(context, images);
}
// 3. 查询刚插入的记录获取Uri
String selection = MediaStore.Images.Media.RELATIVE_PATH + " LIKE ? AND " +
MediaStore.Images.Media.DATE_ADDED + " > ?";
String[] selectionArgs = {
"%MyApp/Batch%",
String.valueOf(System.currentTimeMillis() / 1000 - 10) // 10秒内插入的
};
try (Cursor cursor = resolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME},
selection, selectionArgs,
MediaStore.Images.Media.DATE_ADDED + " DESC")) {
if (cursor != null && cursor.moveToFirst()) {
do {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(
MediaStore.Images.Media._ID));
Uri uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
insertedUris.add(uri);
} while (cursor.moveToNext() && insertedUris.size() < images.size());
}
}
// 4. 批量写入文件数据(使用线程池)
ExecutorService executor = Executors.newFixedThreadPool(
Math.min(images.size(), Runtime.getRuntime().availableProcessors()));
List<Future<Boolean>> futures = new ArrayList<>();
for (int i = 0; i < Math.min(insertedUris.size(), images.size()); i++) {
final Uri uri = insertedUris.get(i);
final ImageData image = images.get(i);
futures.add(executor.submit(() -> {
try {
writeImageData(resolver, uri, image.data);
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}));
}
// 5. 检查写入结果
for (Future<Boolean> future : futures) {
try {
if (!future.get()) {
// 写入失败,清理已插入的记录
cleanupFailedBatch(resolver, insertedUris);
return new ArrayList<>();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 6. 更新IS_PENDING标志(Android 10+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
for (Uri uri : insertedUris) {
ContentValues updateValues = new ContentValues();
updateValues.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(uri, updateValues, null, null);
}
}
executor.shutdown();
return insertedUris;
}
private boolean writeImageData(ContentResolver resolver, Uri uri, byte[] data)
throws IOException {
try (OutputStream os = resolver.openOutputStream(uri)) {
if (os == null) return false;
os.write(data);
return true;
}
}
static class ImageData {
String fileName;
byte[] data;
}
}
三、存储访问框架(SAF)高级应用
3.1 SAF 完整工作流程
java
/**
* Storage Access Framework (SAF) 完整实现
*/
public class StorageAccessFrameworkHelper {
private static final int REQUEST_CODE_OPEN_DOCUMENT = 1001;
private static final int REQUEST_CODE_CREATE_DOCUMENT = 1002;
private static final int REQUEST_CODE_OPEN_DIRECTORY = 1003;
private Activity activity;
private SAFCallback callback;
public interface SAFCallback {
void onFileSelected(Uri uri, String mimeType);
void onMultipleFilesSelected(List<Uri> uris);
void onDirectorySelected(Uri treeUri);
void onCancelled();
}
public StorageAccessFrameworkHelper(Activity activity, SAFCallback callback) {
this.activity = activity;
this.callback = callback;
}
/**
* 打开单个文件
*/
public void openSingleFile(String[] mimeTypes, boolean allowMultiple) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (mimeTypes != null && mimeTypes.length > 0) {
if (mimeTypes.length == 1) {
intent.setType(mimeTypes[0]);
} else {
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
}
} else {
intent.setType("*/*");
}
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
activity.startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT);
}
/**
* 创建新文档
*/
public void createDocument(String suggestedName, String mimeType) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_TITLE, suggestedName);
activity.startActivityForResult(intent, REQUEST_CODE_CREATE_DOCUMENT);
}
/**
* 选择目录(获取目录访问权限)
*/
public void openDirectory() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
activity.startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY);
}
/**
* 处理Activity结果
*/
public void handleActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK || data == null) {
callback.onCancelled();
return;
}
switch (requestCode) {
case REQUEST_CODE_OPEN_DOCUMENT:
handleOpenDocumentResult(data);
break;
case REQUEST_CODE_CREATE_DOCUMENT:
handleCreateDocumentResult(data);
break;
case REQUEST_CODE_OPEN_DIRECTORY:
handleOpenDirectoryResult(data);
break;
}
}
private void handleOpenDocumentResult(Intent data) {
ClipData clipData = data.getClipData();
if (clipData != null) {
// 多文件选择
List<Uri> uris = new ArrayList<>();
for (int i = 0; i < clipData.getItemCount(); i++) {
uris.add(clipData.getItemAt(i).getUri());
}
callback.onMultipleFilesSelected(uris);
} else {
// 单文件选择
Uri uri = data.getData();
String mimeType = activity.getContentResolver().getType(uri);
callback.onFileSelected(uri, mimeType);
}
// 获取持久化权限
takePersistableUriPermission(data);
}
private void handleCreateDocumentResult(Intent data) {
Uri uri = data.getData();
String mimeType = activity.getContentResolver().getType(uri);
callback.onFileSelected(uri, mimeType);
takePersistableUriPermission(data);
}
private void handleOpenDirectoryResult(Intent data) {
Uri treeUri = data.getData();
// 授予对目录的持久化权限
final int takeFlags = data.getFlags() &
(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
activity.getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
callback.onDirectorySelected(treeUri);
}
private void takePersistableUriPermission(Intent data) {
Uri uri = data.getData();
if (uri != null) {
final int takeFlags = data.getFlags() &
(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (takeFlags != 0) {
try {
activity.getContentResolver()
.takePersistableUriPermission(uri, takeFlags);
} catch (SecurityException e) {
e.printStackTrace();
}
}
}
}
/**
* 使用SAF Uri读取文件内容
*/
public static String readTextFromUri(Context context, Uri uri) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
}
return stringBuilder.toString();
}
/**
* 使用SAF Uri写入文件内容
*/
public static void writeTextToUri(Context context, Uri uri, String content)
throws IOException {
try (OutputStream outputStream = context.getContentResolver().openOutputStream(uri);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(outputStream))) {
writer.write(content);
}
}
/**
* 复制文件通过SAF Uri
*/
public static boolean copyFileViaSAF(Context context, Uri srcUri, Uri dstUri) {
try (InputStream is = context.getContentResolver().openInputStream(srcUri);
OutputStream os = context.getContentResolver().openOutputStream(dstUri)) {
if (is == null || os == null) {
return false;
}
byte[] buffer = new byte[8192];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
3.2 目录树访问与递归操作
java
/**
* SAF目录树访问与递归操作
*/
public class SAFDirectoryOperations {
/**
* 递归遍历目录树
*/
public static List<DocumentInfo> traverseDirectoryTree(Context context, Uri treeUri) {
List<DocumentInfo> allDocuments = new ArrayList<>();
traverseDirectory(context, treeUri, allDocuments);
return allDocuments;
}
private static void traverseDirectory(Context context, Uri directoryUri,
List<DocumentInfo> documents) {
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
directoryUri, DocumentsContract.getDocumentId(directoryUri));
ContentResolver resolver = context.getContentResolver();
try (Cursor cursor = resolver.query(
childrenUri,
new String[]{
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED
},
null, null, null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
String documentId = cursor.getString(0);
String displayName = cursor.getString(1);
String mimeType = cursor.getString(2);
long size = cursor.getLong(3);
long lastModified = cursor.getLong(4);
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(
directoryUri, documentId);
DocumentInfo docInfo = new DocumentInfo(
documentId, displayName, mimeType,
documentUri, size, lastModified);
documents.add(docInfo);
// 如果是目录,递归遍历
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
traverseDirectory(context, documentUri, documents);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 在SAF目录中创建子目录
*/
public static Uri createDirectory(Context context, Uri parentUri, String directoryName) {
try {
return DocumentsContract.createDocument(
context.getContentResolver(),
parentUri,
DocumentsContract.Document.MIME_TYPE_DIR,
directoryName
);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 重命名SAF文档
*/
public static boolean renameDocument(Context context, Uri documentUri, String newName) {
try {
return DocumentsContract.renameDocument(
context.getContentResolver(),
documentUri,
newName
) != null;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除SAF文档
*/
public static boolean deleteDocument(Context context, Uri documentUri) {
try {
return DocumentsContract.deleteDocument(
context.getContentResolver(),
documentUri
);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static class DocumentInfo {
public String documentId;
public String displayName;
public String mimeType;
public Uri uri;
public long size;
public long lastModified;
public DocumentInfo(String documentId, String displayName, String mimeType,
Uri uri, long size, long lastModified) {
this.documentId = documentId;
this.displayName = displayName;
this.mimeType = mimeType;
this.uri = uri;
this.size = size;
this.lastModified = lastModified;
}
public boolean isDirectory() {
return DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);
}
}
}
四、Android 11 MANAGE_EXTERNAL_STORAGE 权限
4.1 管理所有文件权限完整实现
java
/**
* Android 11+ MANAGE_EXTERNAL_STORAGE 权限管理
*/
public class ManageExternalStorageManager {
private static final int REQUEST_CODE_MANAGE_EXTERNAL_STORAGE = 2001;
private Activity activity;
private PermissionCallback callback;
public interface PermissionCallback {
void onManagePermissionGranted();
void onManagePermissionDenied();
void onPermissionExplanationNeeded();
}
public ManageExternalStorageManager(Activity activity, PermissionCallback callback) {
this.activity = activity;
this.callback = callback;
}
/**
* 检查并请求管理所有文件权限
*/
public void checkAndRequestManagePermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// Android 10及以下不需要此权限
callback.onManagePermissionGranted();
return;
}
if (Environment.isExternalStorageManager()) {
callback.onManagePermissionGranted();
} else {
// 检查是否需要解释权限用途
if (shouldShowRequestPermissionRationale()) {
callback.onPermissionExplanationNeeded();
} else {
requestManagePermission();
}
}
}
/**
* 显示权限解释对话框
*/
public void showPermissionRationaleDialog() {
new AlertDialog.Builder(activity)
.setTitle("需要文件管理权限")
.setMessage("此功能需要访问所有文件权限,以便:\n" +
"1. 扫描设备上的所有文件\n" +
"2. 清理无用文件\n" +
"3. 备份重要数据\n\n" +
"我们承诺不会滥用此权限,仅用于上述功能。")
.setPositiveButton("授予权限", (dialog, which) -> requestManagePermission())
.setNegativeButton("取消", (dialog, which) -> callback.onManagePermissionDenied())
.show();
}
/**
* 请求权限
*/
private void requestManagePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
intent.addCategory("android.intent.category.DEFAULT");
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_EXTERNAL_STORAGE);
} catch (Exception e) {
// 某些设备可能没有标准的设置页面
Intent intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_EXTERNAL_STORAGE);
}
}
}
/**
* 处理权限请求结果
*/
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_MANAGE_EXTERNAL_STORAGE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
callback.onManagePermissionGranted();
} else {
callback.onManagePermissionDenied();
}
}
}
}
private boolean shouldShowRequestPermissionRationale() {
// 这里可以根据应用逻辑决定是否显示解释
// 例如:用户第一次拒绝时显示解释
SharedPreferences prefs = activity.getSharedPreferences(
"permission_prefs", Context.MODE_PRIVATE);
boolean hasShownBefore = prefs.getBoolean("manage_permission_explained", false);
boolean shouldShow = !hasShownBefore;
if (!hasShownBefore) {
prefs.edit().putBoolean("manage_permission_explained", true).apply();
}
return shouldShow;
}
/**
* 获取所有文件访问能力的工具方法
*/
public static class AllFilesAccessUtils {
/**
* 递归遍历所有文件(需要MANAGE_EXTERNAL_STORAGE权限)
*/
public static List<FileInfo> getAllFiles(File rootDir, int maxDepth) {
List<FileInfo> files = new ArrayList<>();
traverseFiles(rootDir, files, 0, maxDepth);
return files;
}
private static void traverseFiles(File dir, List<FileInfo> files,
int currentDepth, int maxDepth) {
if (currentDepth > maxDepth || !dir.exists() || !dir.isDirectory()) {
return;
}
File[] fileList = dir.listFiles();
if (fileList == null) return;
for (File file : fileList) {
// 跳过应用私有目录(即使有权限也不应该访问)
if (file.getAbsolutePath().contains("/Android/data/") ||
file.getAbsolutePath().contains("/Android/obb/")) {
continue;
}
files.add(new FileInfo(file));
if (file.isDirectory()) {
traverseFiles(file, files, currentDepth + 1, maxDepth);
}
}
}
/**
* 清理无用文件(示例功能)
*/
public static CleanupResult cleanupUnusedFiles(File directory,
long olderThanMillis,
long smallerThanBytes) {
CleanupResult result = new CleanupResult();
if (!directory.exists() || !directory.isDirectory()) {
return result;
}
File[] files = directory.listFiles();
if (files == null) return result;
for (File file : files) {
// 检查文件是否符合清理条件
boolean shouldDelete = false;
if (file.isFile()) {
// 检查文件大小
if (file.length() < smallerThanBytes) {
shouldDelete = true;
}
// 检查文件修改时间
if (System.currentTimeMillis() - file.lastModified() > olderThanMillis) {
shouldDelete = true;
}
// 检查文件类型(扩展名)
String fileName = file.getName().toLowerCase();
if (fileName.endsWith(".tmp") || fileName.endsWith(".temp") ||
fileName.endsWith(".log") || fileName.endsWith(".cache")) {
shouldDelete = true;
}
}
if (shouldDelete) {
if (file.delete()) {
result.deletedFiles++;
result.freedSpace += file.length();
}
}
}
return result;
}
public static class FileInfo {
public String name;
public String path;
public boolean isDirectory;
public long size;
public long lastModified;
public String mimeType;
public FileInfo(File file) {
this.name = file.getName();
this.path = file.getAbsolutePath();
this.isDirectory = file.isDirectory();
this.size = file.length();
this.lastModified = file.lastModified();
this.mimeType = getMimeType(file);
}
private String getMimeType(File file) {
if (file.isDirectory()) {
return "inode/directory";
}
String extension = getFileExtension(file.getName());
return MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(extension.toLowerCase());
}
private String getFileExtension(String fileName) {
int lastDot = fileName.lastIndexOf('.');
if (lastDot != -1 && lastDot < fileName.length() - 1) {
return fileName.substring(lastDot + 1);
}
return "";
}
}
public static class CleanupResult {
public int deletedFiles = 0;
public long freedSpace = 0; // bytes
}
}
}
五、统一适配方案
5.1 存储适配管理器
java
/**
* Android存储统一适配管理器
* 支持Android 4.4到Android 11+
*/
public class StorageAdapterManager {
private final Context context;
private final StorageAccessMatrix.AccessMethod[][] accessMatrix;
public StorageAdapterManager(Context context) {
this.context = context.getApplicationContext();
// 初始化访问矩阵
int sdkVersion = Build.VERSION.SDK_INT;
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
boolean hasLegacyFlag = hasLegacyExternalStorageFlag(context);
this.accessMatrix = convertMatrix(StorageAccessMatrix.getAccessMatrix(
sdkVersion, targetSdkVersion, hasLegacyFlag));
}
/**
* 获取文件路径(兼容所有版本)
*/
public String getFilePath(String relativePath, StorageAccessMatrix.StorageArea area) {
StorageAccessMatrix.AccessMethod[] methods = getAvailableMethods(area);
for (StorageAccessMatrix.AccessMethod method : methods) {
switch (method) {
case DIRECT_PATH:
return getDirectPath(relativePath, area);
case MEDIA_STORE:
// 通过MediaStore获取Uri,然后尝试获取路径
Uri uri = getUriViaMediaStore(relativePath, area);
if (uri != null) {
return getPathFromUri(uri);
}
break;
case SAF:
// 需要用户交互,返回null
break;
case MANAGED_ACCESS:
if (hasManageExternalStoragePermission()) {
return getDirectPath(relativePath, area);
}
break;
}
}
return null;
}
/**
* 读取文件(自动选择最佳方式)
*/
public byte[] readFile(String relativePath, StorageAccessMatrix.StorageArea area)
throws IOException {
StorageAccessMatrix.AccessMethod[] methods = getAvailableMethods(area);
for (StorageAccessMatrix.AccessMethod method : methods) {
try {
switch (method) {
case DIRECT_PATH:
return readFileDirect(relativePath, area);
case MEDIA_STORE:
Uri uri = getUriViaMediaStore(relativePath, area);
if (uri != null) {
return readFileViaUri(uri);
}
break;
case SAF:
// 需要用户交互,跳过
break;
case MANAGED_ACCESS:
if (hasManageExternalStoragePermission()) {
return readFileDirect(relativePath, area);
}
break;
}
} catch (SecurityException e) {
// 权限不足,尝试下一种方法
continue;
} catch (IOException e) {
// 读取失败,尝试下一种方法
continue;
}
}
throw new IOException("无法读取文件: " + relativePath);
}
/**
* 写入文件(自动选择最佳方式)
*/
public boolean writeFile(String relativePath, byte[] data,
StorageAccessMatrix.StorageArea area) {
StorageAccessMatrix.AccessMethod[] methods = getAvailableMethods(area);
for (StorageAccessMatrix.AccessMethod method : methods) {
try {
switch (method) {
case DIRECT_PATH:
return writeFileDirect(relativePath, data, area);
case MEDIA_STORE:
Uri uri = createOrGetUriViaMediaStore(relativePath, area);
if (uri != null) {
return writeFileViaUri(uri, data);
}
break;
case SAF:
// 需要用户交互,跳过
break;
case MANAGED_ACCESS:
if (hasManageExternalStoragePermission()) {
return writeFileDirect(relativePath, data, area);
}
break;
}
} catch (Exception e) {
continue;
}
}
return false;
}
// 私有辅助方法
private StorageAccessMatrix.AccessMethod[] getAvailableMethods(
StorageAccessMatrix.StorageArea area) {
// 根据area从matrix中获取可用方法
// 简化实现,实际需要完整的matrix数据结构
return new StorageAccessMatrix.AccessMethod[0];
}
private boolean hasManageExternalStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Environment.isExternalStorageManager();
}
return true; // Android 10及以下不需要此权限
}
private boolean hasLegacyExternalStorageFlag(Context context) {
// 检查是否设置了requestLegacyExternalStorage标志
try {
ApplicationInfo appInfo = context.getPackageManager()
.getApplicationInfo(context.getPackageName(), 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 通过反射或其他方式检查标志
// 注意:这只是一个示例,实际实现可能不同
return (appInfo.flags & 0x80000) != 0; // 示例标志位
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return false;
}
// 各种具体实现方法
private byte[] readFileDirect(String relativePath,
StorageAccessMatrix.StorageArea area) throws IOException {
String fullPath = getDirectPath(relativePath, area);
return Files.readAllBytes(new File(fullPath).toPath());
}
private boolean writeFileDirect(String relativePath, byte[] data,
StorageAccessMatrix.StorageArea area) throws IOException {
String fullPath = getDirectPath(relativePath, area);
Files.write(new File(fullPath).toPath(), data);
return true;
}
private byte[] readFileViaUri(Uri uri) throws IOException {
try (InputStream is = context.getContentResolver().openInputStream(uri)) {
if (is == null) throw new IOException("无法打开输入流");
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[8192];
int nRead;
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
return buffer.toByteArray();
}
}
private boolean writeFileViaUri(Uri uri, byte[] data) throws IOException {
try (OutputStream os = context.getContentResolver().openOutputStream(uri)) {
if (os == null) return false;
os.write(data);
return true;
}
}
private String getDirectPath(String relativePath,
StorageAccessMatrix.StorageArea area) {
switch (area) {
case INTERNAL_APP_PRIVATE:
return new File(context.getFilesDir(), relativePath).getAbsolutePath();
case EXTERNAL_APP_PRIVATE:
return new File(context.getExternalFilesDir(null), relativePath)
.getAbsolutePath();
case SHARED_MEDIA:
case OTHER_PUBLIC_DIRS:
return new File(Environment.getExternalStorageDirectory(), relativePath)
.getAbsolutePath();
default:
return null;
}
}
// 其他辅助方法...
}
六、测试策略与调试工具
6.1 存储适配测试工具
java
/**
* 存储适配测试工具
*/
public class StorageTestUtils {
/**
* 运行完整的存储兼容性测试
*/
public static TestResult runCompatibilityTest(Context context) {
TestResult result = new TestResult();
// 1. 测试内部存储
result.internalStorageTest = testInternalStorage(context);
// 2. 测试外部私有存储
result.externalPrivateTest = testExternalPrivateStorage(context);
// 3. 测试MediaStore访问
result.mediaStoreTest = testMediaStoreAccess(context);
// 4. 测试SAF访问
result.safTest = testSAFAccess(context);
// 5. 测试直接路径访问(如果可用)
result.directPathTest = testDirectPathAccess(context);
// 6. 测试权限管理
result.permissionTest = testPermissions(context);
return result;
}
private static StorageTestResult testInternalStorage(Context context) {
StorageTestResult result = new StorageTestResult("内部存储测试");
try {
// 测试写入
File testFile = new File(context.getFilesDir(), "test_internal.txt");
try (FileWriter writer = new FileWriter(testFile)) {
writer.write("测试内容");
}
result.writeSuccess = true;
// 测试读取
try (BufferedReader reader = new BufferedReader(new FileReader(testFile))) {
String content = reader.readLine();
result.readSuccess = content != null && content.equals("测试内容");
}
// 测试删除
result.deleteSuccess = testFile.delete();
} catch (IOException e) {
result.error = e.getMessage();
}
return result;
}
private static StorageTestResult testMediaStoreAccess(Context context) {
StorageTestResult result = new StorageTestResult("MediaStore访问测试");
// 检查是否有存储权限
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
result.error = "无存储权限";
return result;
}
try {
// 查询图片数量
ContentResolver resolver = context.getContentResolver();
try (Cursor cursor = resolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID},
null, null, null)) {
result.readSuccess = cursor != null;
if (cursor != null) {
result.data = "找到 " + cursor.getCount() + " 张图片";
}
}
} catch (Exception e) {
result.error = e.getMessage();
}
return result;
}
public static class TestResult {
public StorageTestResult internalStorageTest;
public StorageTestResult externalPrivateTest;
public StorageTestResult mediaStoreTest;
public StorageTestResult safTest;
public StorageTestResult directPathTest;
public StorageTestResult permissionTest;
public boolean isAllTestsPassed() {
return internalStorageTest.isSuccess() &&
externalPrivateTest.isSuccess() &&
mediaStoreTest.isSuccess() &&
permissionTest.isSuccess();
}
}
public static class StorageTestResult {
public String testName;
public boolean writeSuccess = false;
public boolean readSuccess = false;
public boolean deleteSuccess = false;
public String error = null;
public String data = null;
public StorageTestResult(String testName) {
this.testName = testName;
}
public boolean isSuccess() {
return writeSuccess && readSuccess && deleteSuccess && error == null;
}
}
}
七、最佳实践总结
7.1 适配建议清单
java
/**
* Android存储适配最佳实践检查清单
*/
public class StorageBestPractices {
/**
* 适配检查清单
*/
public static class Checklist {
public static final List<String> REQUIRED_ACTIONS = Arrays.asList(
// 基础配置
"✅ 在AndroidManifest.xml中声明必要的权限",
"✅ 为Android 10.0设置requestLegacyExternalStorage标志(过渡期)",
"✅ 更新targetSdkVersion到最新稳定版本",
// 数据存储位置
"✅ 敏感数据存储在内部存储",
"✅ 大文件存储在外部私有目录",
"✅ 用户共享文件使用MediaStore",
"✅ 用户文档使用SAF或MediaStore",
// 权限管理
"✅ 动态请求运行时权限(Android 6.0+)",
"✅ 处理权限拒绝场景",
"✅ 为Android 11+实现MANAGE_EXTERNAL_STORAGE权限流程",
// 代码适配
"✅ 替换所有直接路径访问为Uri访问",
"✅ 实现版本兼容的存储访问层",
"✅ 适配第三方库的存储访问",
// 数据迁移
"✅ 实现旧数据迁移到新位置",
"✅ 通知用户数据迁移进度",
"✅ 清理迁移后的旧数据",
// 测试验证
"✅ 在不同Android版本上测试存储功能",
"✅ 测试权限被拒绝时的降级处理",
"✅ 测试大文件读写性能",
"✅ 验证数据迁移的正确性"
);
}
/**
* 常见问题与解决方案
*/
public static class CommonProblems {
public static final Map<String, String> SOLUTIONS = new HashMap<>();
static {
SOLUTIONS.put("第三方库不支持分区存储",
"1. 联系库作者更新\n" +
"2. 寻找替代库\n" +
"3. 自行修改库代码\n" +
"4. 使用反射或其他hack方法(不推荐)");
SOLUTIONS.put("用户数据迁移困难",
"1. 分批次迁移,避免阻塞主线程\n" +
"2. 提供迁移进度显示\n" +
"3. 允许用户选择迁移时间\n" +
"4. 迁移失败时提供恢复方案");
SOLUTIONS.put("SAF用户体验不佳",
"1. 缓存用户选择的目录权限\n" +
"2. 提供明确的文件类型筛选\n" +
"3. 实现自定义文件选择器(如果需要)\n" +
"4. 优化SAF调用时机");
SOLUTIONS.put("性能问题",
"1. 使用批量操作减少ContentResolver调用\n" +
"2. 实现文件操作队列\n" +
"3. 使用缓存减少重复查询\n" +
"4. 异步执行耗时操作");
}
}
}
7.2 版本兼容性配置示例
gradle
// build.gradle 配置示例
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.app"
minSdkVersion 21
targetSdkVersion 30 // 根据应用商店要求设置
// 针对Android 10的兼容性配置
if (project.hasProperty('android.injected.invoked.from.ide')) {
// IDE构建时使用较低版本便于调试
targetSdkVersion 29
}
}
// 针对不同版本的编译配置
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
// 存储相关依赖
implementation "androidx.documentfile:documentfile:1.0.1"
implementation "androidx.exifinterface:exifinterface:1.3.2"
// 权限管理
implementation "com.github.permissions-dispatcher:permissionsdispatcher:4.8.0"
annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:4.8.0"
// 文件操作工具
implementation "commons-io:commons-io:2.8.0"
}
通过本文的详细分析和代码示例,您应该能够全面理解Android 10/11的存储适配要求,并实现一个健壮、兼容的存储访问层。记住,适配分区存储是一个渐进的过程,需要仔细规划、充分测试,并考虑用户体验。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)