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

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);
    }
}

失效原因

  1. 路径硬编码违规:通过getExternalFilesDir(null).getParent()拼接其他应用包名路径的写法,违反Android Q分区存储权限规则,应用无权访问非自身专属目录,系统会静默拦截写入操作,不会抛出异常
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 23:31:08