基于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
相关产品推荐
相关产品推荐

