Android Q Java环境下如何将assets任意文件移动/复制到分区存储
Android Q分区存储下Assets目录任意类型文件复制方案
问题背景
- 已实现图片类文件从assets目录到Android Q分区存储的写入,但同逻辑无法迁移/复制其他类型文件,本次目标写入文件为数据库文件
- 原有数据库复制代码运行无报错,但未生成预期复制结果
现有可正常运行的图片相关实现
图片遍历复制逻辑
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ try { try { pictures = assetManager.list("photos/dataset1"); } catch (IOException e) { Log.e("tag", "Failed to get asset file list.", e); } if (pictures != null) { for (String filename : pictures) { InputStream in; OutputStream out; InputStream inputStream = assetManager.open("photos/dataset1/"+filename); Bitmap bitmap = BitmapFactory.decodeStream(inputStream); saveImageToGallery(bitmap); } } } catch (IOException e) { e.printStackTrace(); } }
图片存储到系统相册方法
public void saveImageToGallery(Bitmap bitmap) { OutputStream outputStream; Context myContext = requireContext(); try { if(Build.VERSION.SDK_INT >=Build.VERSION_CODES.Q){ ContentResolver contentResolver = requireContext().getContentResolver(); ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME,"Image_"+".jpg"); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES); Uri imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues); outputStream = contentResolver.openOutputStream(Objects.requireNonNull(imageUri)); bitmap.compress(Bitmap.CompressFormat.JPEG,100, outputStream); Objects.requireNonNull(outputStream); } }catch (FileNotFoundException e) { e.printStackTrace(); } }
原有数据库复制代码(失效)
AssetManager assetManager = Objects.requireNonNull(requireContext()).getAssets(); Context myContext = requireContext(); //Essential for creating the external storage directory for the first launch myContext.getExternalFilesDir(null); File databasesFolder = new File(myContext.getExternalFilesDir(null).getParent(), "com.mydb.orca/databases"); databasesFolder.mkdirs(); if (files!= null) { for (String filename : files) { InputStream in; OutputStream out; try { in = assetManager.open("database/test/" + filename); File outFile = new File(databasesFolder, filename); out = new FileOutputStream(outFile); copyFile(in, out); in.close(); out.flush(); out.close(); } catch (IOException e) { Log.e("tag", "Failed to copy asset file: " + filename, e); } } } else { Log.e("Error NPE", "files is null"); } private void copyFile(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[1024]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } }
失效原因
- 路径硬编码违规:通过
getExternalFilesDir(null).getParent()拼接其他应用包名路径的写法,违反Android Q分区存储权限规则,应用无权访问非自身专属目录,系统会静默拦截写入操作,不会抛出异常 - API使用错误:非应用专属目录的写入不能直接使用传统
FileOutputStream,必须通过ContentResolver配合MediaStore接口操作,和现有图片写入逻辑对齐
通用实现方案
场景1:复制到应用自身专属目录(推荐用于自用数据库,无需任何存储权限)
该场景下文件仅当前应用可访问,适合存放应用私有的数据库、配置文件,全版本兼容无需额外适配。
/** * 通用Assets文件/目录递归复制到应用专属目录 * @param context 上下文 * @param assetPath assets下源路径,支持文件/目录 * @param targetDir 目标目录,数据库可传入context.getDatabasePath("dbname").getParent() */ public void copyAssetToAppPrivateDir(Context context, String assetPath, File targetDir) throws IOException { if (!targetDir.exists()) targetDir.mkdirs(); AssetManager am = context.getAssets(); String[] fileList = am.list(assetPath); // 路径为单个文件 if (fileList == null || fileList.length == 0) { String fileName = assetPath.substring(assetPath.lastIndexOf('/') + 1); try (InputStream in = am.open(assetPath); OutputStream out = new FileOutputStream(new File(targetDir, fileName))) { byte[] buf = new byte[4096]; int len; while ((len = in.read(buf)) != -1) out.write(buf, 0, len); out.flush(); } return; } // 路径为目录,递归复制 for (String name : fileList) { String subPath = assetPath + "/" + name; if (am.list(subPath).length > 0) { copyAssetToAppPrivateDir(context, subPath, new File(targetDir, name)); } else { try (InputStream in = am.open(subPath); OutputStream out = new FileOutputStream(new File(targetDir, name))) { byte[] buf = new byte[4096]; int len; while ((len = in.read(buf)) != -1) out.write(buf, 0, len); out.flush(); } } } }
调用示例(复制数据库到应用数据库目录):
copyAssetToAppPrivateDir( requireContext(), "database/test", requireContext().getDatabasePath("target_db.db").getParentFile() );
场景2:复制到公共存储目录(和图片写入逻辑一致,用户/其他应用可访问)
该场景对齐现有图片写入的MediaStore逻辑,Android Q+无需申请存储权限,支持任意类型文件写入公共目录。
/** * 通用Assets文件写入公共存储 * @param assetPath assets下源文件路径(不支持目录,可外层遍历目录调用) * @param mimeType 文件MIME类型,数据库传"application/x-sqlite3",任意二进制传"application/octet-stream" * @param relativePath 公共目录相对路径,比如Environment.DIRECTORY_DOCUMENTS + "/MyDbs" * @param displayName 保存后的文件名 */ public Uri copyAssetToPublicDir(Context context, String assetPath, String mimeType, String relativePath, String displayName) throws IOException { ContentResolver cr = context.getContentResolver(); ContentValues cv = new ContentValues(); cv.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName); cv.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { cv.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath); Uri insertUri = cr.insert(MediaStore.Files.getContentUri("external"), cv); if (insertUri == null) throw new IOException("Create public file failed"); try (InputStream in = context.getAssets().open(assetPath); OutputStream out = cr.openOutputStream(insertUri)) { byte[] buf = new byte[4096]; int len; while ((len = in.read(buf)) != -1) out.write(buf, 0, len); out.flush(); } return insertUri; } else { // 低版本兼容 File publicDir = Environment.getExternalStoragePublicDirectory(relativePath); if (!publicDir.exists()) publicDir.mkdirs(); File targetFile = new File(publicDir, displayName); try (InputStream in = context.getAssets().open(assetPath); OutputStream out = new FileOutputStream(targetFile)) { byte[] buf = new byte[4096]; int len; while ((len = in.read(buf)) != -1) out.write(buf, 0, len); out.flush(); } return Uri.fromFile(targetFile); } }
调用示例(遍历复制所有数据库文件到公共文档目录):
String[] dbFiles = requireContext().getAssets().list("database/test"); if (dbFiles != null) { for (String fileName : dbFiles) { copyAssetToPublicDir( requireContext(), "database/test/" + fileName, "application/x-sqlite3", Environment.DIRECTORY_DOCUMENTS + "/AppDatabases", fileName ); } }
内容的提问来源于stack exchange,提问作者Louis Chabert
相关产品推荐
相关产品推荐

