You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android11+卸载重装/清数据后访问自建目录遇EACCES权限拒绝

问题分析与解决方案

问题根源

Android 11(API 30)引入的Scoped Storage机制,彻底改变了应用访问公共存储的规则:

  • 公共Documents目录属于系统管理的共享存储区域,应用通过文件路径直接创建的目录,其权限关联仅对当前应用实例有效。
  • 当应用被清除数据或卸载重装后,系统会销毁原应用的权限上下文,即使包名、签名完全一致,新实例也无法通过原文件路径直接访问旧实例创建的目录——系统不再识别两者的归属关联,因此抛出EACCES权限拒绝错误。

推荐解决方案:使用MediaStore API操作公共目录

Scoped Storage要求应用通过MediaStore API访问共享存储中的文件/目录,该方式会让系统记录资源的访问权限,即使应用重装,只要通过API查询到目标资源的Uri,就能正常读写。

1. 查询或创建GT-backup目录

private static Uri getBackupDirectoryUri(Context context) {
    // 查询GT-backup目录是否存在
    String[] projection = {MediaStore.Files.FileColumns._ID};
    String selection = MediaStore.Files.FileColumns.DISPLAY_NAME + " = ? AND " +
                       MediaStore.Files.FileColumns.MIME_TYPE + " = ?";
    String[] selectionArgs = {"GT-backup", "vnd.android.document/directory"};

    Cursor cursor = context.getContentResolver().query(
            MediaStore.Files.getContentUri("external"),
            projection,
            selection,
            selectionArgs,
            null
    );

    if (cursor != null && cursor.moveToFirst()) {
        long dirId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));
        cursor.close();
        // 返回已存在目录的Uri
        return MediaStore.Files.getContentUri("external").buildUpon()
                .appendPath(String.valueOf(dirId))
                .build();
    } else {
        // 目录不存在,创建新目录
        ContentValues values = new ContentValues();
        values.put(MediaStore.Files.FileColumns.DISPLAY_NAME, "GT-backup");
        values.put(MediaStore.Files.FileColumns.MIME_TYPE, "vnd.android.document/directory");
        values.put(MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS);

        if (cursor != null) cursor.close();
        return context.getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);
    }
}

2. 在目录中创建/读写文件

通过上述方法获取目录Uri后,使用ContentResolver操作文件,无需直接访问文件路径:

// 创建备份文件
private static Uri createBackupFile(Context context, String fileName) throws IOException {
    Uri dirUri = getBackupDirectoryUri(context);
    if (dirUri == null) {
        throw new IOException("Failed to access backup directory");
    }

    ContentValues fileValues = new ContentValues();
    fileValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName);
    fileValues.put(MediaStore.Files.FileColumns.MIME_TYPE, "application/octet-stream"); // 根据文件类型调整
    fileValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH, 
                   Environment.DIRECTORY_DOCUMENTS + "/GT-backup");

    return context.getContentResolver().insert(MediaStore.Files.getContentUri("external"), fileValues);
}

// 写入文件示例
public void writeBackupData(Context context, String fileName, byte[] data) {
    try {
        Uri fileUri = createBackupFile(context, fileName);
        try (OutputStream os = context.getContentResolver().openOutputStream(fileUri)) {
            os.write(data);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 读取文件示例
public byte[] readBackupData(Context context, String fileName) throws IOException {
    // 先查询目标文件的Uri
    String[] projection = {MediaStore.Files.FileColumns._ID};
    String selection = MediaStore.Files.FileColumns.DISPLAY_NAME + " = ? AND " +
                       MediaStore.Files.FileColumns.RELATIVE_PATH + " = ?";
    String[] selectionArgs = {fileName, Environment.DIRECTORY_DOCUMENTS + "/GT-backup"};

    Cursor cursor = context.getContentResolver().query(
            MediaStore.Files.getContentUri("external"),
            projection,
            selection,
            selectionArgs,
            null
    );

    if (cursor != null && cursor.moveToFirst()) {
        long fileId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));
        cursor.close();
        Uri fileUri = MediaStore.Files.getContentUri("external").buildUpon()
                .appendPath(String.valueOf(fileId))
                .build();
        
        try (InputStream is = context.getContentResolver().openInputStream(fileUri);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        }
    } else {
        if (cursor != null) cursor.close();
        throw new FileNotFoundException("Backup file not found");
    }
}

权限配置

在AndroidManifest.xml中声明必要权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<!-- Android 13+ 需要细分权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

动态申请权限的逻辑与常规存储权限申请一致,此处不再赘述。

备选方案:使用MANAGE_EXTERNAL_STORAGE权限(不推荐)

若必须通过文件路径直接访问公共目录,可申请MANAGE_EXTERNAL_STORAGE权限,但该权限为全存储访问权限,Google Play会严格审核非必要使用该权限的应用,可能导致上架被拒。

配置与申请

  1. 在Manifest中声明:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:minSdkVersion="30" />
  1. 动态申请权限:
private static final int REQUEST_MANAGE_STORAGE = 1001;

public void checkManageStoragePermission(Context context) {
    if (!Environment.isExternalStorageManager()) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
        Uri uri = Uri.fromParts("package", context.getPackageName(), null);
        intent.setData(uri);
        ((Activity) context).startActivityForResult(intent, REQUEST_MANAGE_STORAGE);
    }
}

备选方案:使用应用专属外部存储

如果备份文件无需在卸载后保留,可将目录放在应用专属外部存储区,该区域无需额外权限,且清除数据后仍可访问:

File backupDir = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "GT-backup");
if (!backupDir.exists()) {
    backupDir.mkdirs();
}
// 后续直接通过File类操作文件即可

注意:卸载应用时,该目录会被系统自动删除。

内容的提问来源于stack exchange,提问作者Amer Al zibak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 10:42:49