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

基于Android Scoped Storage实现文件解压至Android/data目录

问题

我正在开发一款Android应用,需要将文件解压到Android/data目录下的其他应用文件夹(而非自身应用专属数据目录),已通过Scoped Storage的getContentResolver().takePersistableUriPermission()方法获取了目标目录的写入权限。现在的问题是:给定一个指向Android/data下子文件夹的Uri对象,如何将压缩包内容解压到该位置?

我尝试过使用Android默认Zip库和Zip4j,但都出现了权限错误,推测是路径相关的问题。

权限获取代码

以下是我获取Android/data写入权限的代码:

启动权限选择Intent

StorageManager sm = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();

String startSubDir = "Android%2Fdata";
Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");
String scheme = uri.toString();

scheme = scheme.replace("/root/", "/document/");
scheme += "%3A" + startSubDir;

uri = Uri.parse(scheme);
intent.putExtra("android.provider.extra.INITIAL_URI", uri);

someActivityResultLauncher.launch(intent);

请求持久权限

ActivityResultLauncher<Intent> someActivityResultLauncher = registerForActivityResult(
    new ActivityResultContracts.StartActivityForResult(),
    new ActivityResultCallback<ActivityResult>() {
        @Override
        public void onActivityResult(ActivityResult result) {
            if (result.getResultCode() == Activity.RESULT_OK) {
                Intent data = result.getData();
                if (data != null) {
                    getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                    
                } else {
                    System.exit(0);
                }
            }
        }
    });
解决方案

由于Scoped Storage限制,无法直接通过文件路径操作目标目录,必须基于Uri和ContentResolver来处理文件的创建与写入,以下是两种可行的实现方式:

方式一:使用Android默认ZipInputStream手动处理

核心思路是遍历压缩包内的每个文件,通过ContentResolver在目标Uri对应的目录下创建对应的文件/文件夹,再将压缩流写入:

/**
 * 解压压缩包到指定Uri目录
 * @param zipFileUri 压缩包的Uri
 * @param targetDirUri 目标目录的Uri(已获取写入权限)
 * @param context 上下文
 */
public void unzipToUri(Uri zipFileUri, Uri targetDirUri, Context context) {
    ContentResolver resolver = context.getContentResolver();
    try (ZipInputStream zis = new ZipInputStream(resolver.openInputStream(zipFileUri))) {
        ZipEntry entry;
        while ((entry = zis.getNextEntry()) != null) {
            // 构建当前条目对应的目标Uri
            String entryName = entry.getName();
            // 处理目录:如果是文件夹,先创建
            if (entry.isDirectory()) {
                createDirectory(targetDirUri, entryName, resolver);
                zis.closeEntry();
                continue;
            }
            // 处理文件:创建文件并写入内容
            Uri fileUri = createFileInDirectory(targetDirUri, entryName, resolver);
            try (OutputStream os = resolver.openOutputStream(fileUri)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = zis.read(buffer)) != -1) {
                    os.write(buffer, 0, bytesRead);
                }
            }
            zis.closeEntry();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

/**
 * 在指定目录Uri下创建子目录
 */
private void createDirectory(Uri parentDirUri, String dirName, ContentResolver resolver) throws IOException {
    // 构建目录的Document ID
    String parentDocId = DocumentsContract.getDocumentId(parentDirUri);
    String childDocId = parentDocId + "/" + dirName;
    Uri childDirUri = DocumentsContract.buildDocumentUriUsingTree(parentDirUri, childDocId);
    
    // 检查目录是否存在,不存在则创建
    if (resolver.openInputStream(childDirUri) == null) {
        DocumentsContract.createDocument(resolver, parentDirUri, DocumentsContract.Document.MIME_TYPE_DIR, dirName);
    }
}

/**
 * 在指定目录Uri下创建文件
 */
private Uri createFileInDirectory(Uri parentDirUri, String fileName, ContentResolver resolver) throws IOException {
    // 获取文件的扩展名,用于设置MIME类型
    String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(fileName));
    if (mimeType == null) {
        mimeType = "application/octet-stream";
    }
    return DocumentsContract.createDocument(resolver, parentDirUri, mimeType, fileName);
}

方式二:适配Zip4j(通过自定义IOutputFile)

Zip4j默认使用File路径操作,需要自定义IOutputFile接口实现,将文件操作转成ContentResolver的Uri操作:

1. 自定义UriOutputFile类

public class UriOutputFile implements IOutputFile {
    private final ContentResolver resolver;
    private final Uri fileUri;
    private OutputStream outputStream;

    public UriOutputFile(ContentResolver resolver, Uri fileUri) {
        this.resolver = resolver;
        this.fileUri = fileUri;
    }

    @Override
    public void createFile() throws IOException {
        // 文件由外部创建,这里不需要额外操作
    }

    @Override
    public void openFile(OpenMode openMode) throws IOException {
        int mode = (openMode == OpenMode.APPEND) ? 
            (Context.MODE_APPEND | Context.MODE_WRITE_ONLY) : 
            (Context.MODE_CREATE | Context.MODE_WRITE_ONLY);
        this.outputStream = resolver.openOutputStream(fileUri, "w");
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        if (outputStream != null) {
            outputStream.write(b, off, len);
        }
    }

    @Override
    public void close() throws IOException {
        if (outputStream != null) {
            outputStream.close();
        }
    }

    @Override
    public void setFileLength(long l) throws IOException {
        // 无需实现,ContentResolver会自动处理
    }

    @Override
    public long getFilePointer() throws IOException {
        return 0; // 简化实现,若需要精确指针可自行扩展
    }

    @Override
    public void seek(long l) throws IOException {
        // 无需实现,Zip4j解压时一般不需要随机访问
    }

    @Override
    public boolean exists() throws IOException {
        try (InputStream is = resolver.openInputStream(fileUri)) {
            return is != null;
        } catch (FileNotFoundException e) {
            return false;
        }
    }

    @Override
    public long getLastModifiedTime() throws IOException {
        return 0; // 简化实现
    }
}

2. 使用Zip4j解压的代码

/**
 * 使用Zip4j解压到指定Uri目录
 * @param zipFilePath 本地压缩包路径(或先将Uri转成本地临时文件)
 * @param targetDirUri 目标目录Uri
 * @param context 上下文
 */
public void unzipWithZip4j(String zipFilePath, Uri targetDirUri, Context context) {
    ContentResolver resolver = context.getContentResolver();
    try {
        ZipFile zipFile = new ZipFile(zipFilePath);
        List<FileHeader> fileHeaders = zipFile.getFileHeaders();
        
        for (FileHeader header : fileHeaders) {
            String entryName = header.getFileName();
            if (header.isDirectory()) {
                // 创建目录
                createDirectory(targetDirUri, entryName, resolver);
                continue;
            }
            // 创建文件Uri
            Uri fileUri = createFileInDirectory(targetDirUri, entryName, resolver);
            // 使用自定义的UriOutputFile写入
            UriOutputFile outputFile = new UriOutputFile(resolver, fileUri);
            zipFile.extractFile(header, outputFile);
        }
    } catch (ZipException | IOException e) {
        e.printStackTrace();
    }
}

注意事项

  • 压缩包如果是通过Uri获取的,可先将其复制到应用的临时目录(如getCacheDir()),再用Zip4j处理,避免直接操作Uri流的兼容性问题。
  • 处理目录时要注意层级结构,确保父目录存在后再创建子目录。
  • 所有操作必须在子线程中执行,避免阻塞主线程。

内容的提问来源于stack exchange,提问作者Warley Soares

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 07:50:18