一、分区存储核心机制深度解析

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的存储适配要求,并实现一个健壮、兼容的存储访问层。记住,适配分区存储是一个渐进的过程,需要仔细规划、充分测试,并考虑用户体验。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐