在Android Q中使用MediaStore创建/复制文件

28

我正在尝试寻找一种方法来处理除了媒体文件(图片/视频/音频)之外的任何文件的创建和复制,以便将其从一个位置复制到另一个位置内部存储在Android Q中。 在这里,我有我的文件创建在我的应用程序文件夹中,我想要将它们移动到下载文件夹或我可以在内部存储器中创建的某个目录中,然后移动它们。

我搜索并发现以下修改后的代码,但缺少一些东西使其可用。 能否有人提供帮助。

ContentResolver contentResolver = getContentResolver();

ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "sam.txt");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);

Uri uri = contentResolver.insert(MediaStore.Files.getContentUri("external"), contentValues);

try {
    InputStream inputStream = contentResolver.openInputStream(uri);
    OutputStream outputStream = new FileOutputStream(Environment.DIRECTORY_DOWNLOADS+"/");

    byte[] buffer = new byte[1024];

    int length;

    //copy the file content in bytes
    while ((length = inputStream.read(buffer)) > 0) {
            outputStream.write(buffer, 0, length);
        }
        inputStream.close();
        outputStream.close();
        } catch (Exception e) {
           e.printStackTrace();
        }

以上代码是否完整,我一直得到“未知 URL”错误。有什么缺失吗?请帮忙。


否则,由于在Android Q中Environment.getExternalStoragePublicDirectory已经过时,我该如何获取下载文件夹的路径。如果你能建议其他的选项,那我也可以接受。 - Panache
1
Uri.fromFile(file)替换为MediaStore.Files.getContentUri("external") - blackapps
@blackapps,出现了“没有这个文件”的错误。我正在尝试将文件从Android/data/packagename/..的应用程序文件夹复制到下载目录中。我是否正确捕获了输入?我们在哪里告诉代码输入文件的路径? - Panache
1
输入文件的路径对于媒体存储来说是无关紧要的。您可以从存储中获取URI。对于该URI,您打开一个输出流。同时,您还需要为输入文件打开一个FileInputStream。然后,您将复制这个流。 - blackapps
1
如果输入文件的路径不相关,则仅在从媒体存储获取输出 URI 时才无关紧要。当您打开输入流以执行复制时,它当然是相关的。 - blackapps
显示剩余6条评论
3个回答

52

注意: 如果您重新安装应用程序,MediaStore 将不再识别之前创建的文件:Android 11 重新安装应用程序后无法检索使用 MediaStore 创建的文件,使用 Intent 让用户选择文件是唯一的解决方案。


1. 创建并写入文件

createAndWriteButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        try {
            ContentValues values = new ContentValues();

            values.put(MediaStore.MediaColumns.DISPLAY_NAME, "menuCategory");       //file name                     
            values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");        //file extension, will automatically add to file
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/");     //end "/" is not mandatory

            Uri uri = getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);      //important!

            OutputStream outputStream = getContentResolver().openOutputStream(uri);

            outputStream.write("This is menu category data.".getBytes());

            outputStream.close();

            Toast.makeText(view.getContext(), "File created successfully", Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            Toast.makeText(view.getContext(), "Fail to create file", Toast.LENGTH_SHORT).show();
        }
    }
});

2. 查找和读取文件

findAndReadButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Uri contentUri = MediaStore.Files.getContentUri("external");

        String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";

        String[] selectionArgs = new String[]{Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/"};

        Cursor cursor = getContentResolver().query(contentUri, null, selection, selectionArgs, null);

        Uri uri = null;

        if (cursor.getCount() == 0) {
            Toast.makeText(view.getContext(), "No file found in \"" + Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/\"", Toast.LENGTH_LONG).show();
        } else {
            while (cursor.moveToNext()) {
                String fileName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));

                if (fileName.equals("menuCategory.txt")) {
                    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));

                    uri = ContentUris.withAppendedId(contentUri, id);

                    break;
                }
            }

            if (uri == null) {
                Toast.makeText(view.getContext(), "\"menuCategory.txt\" not found", Toast.LENGTH_SHORT).show();
            } else {
                try {
                    InputStream inputStream = getContentResolver().openInputStream(uri);

                    int size = inputStream.available();

                    byte[] bytes = new byte[size];

                    inputStream.read(bytes);

                    inputStream.close();

                    String jsonString = new String(bytes, StandardCharsets.UTF_8);

                    AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());

                    builder.setTitle("File Content");
                    builder.setMessage(jsonString);
                    builder.setPositiveButton("OK", null);

                    builder.create().show();
                } catch (IOException e) {
                    Toast.makeText(view.getContext(), "Fail to read file", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
});

3. 查找并覆盖文件

findAndWriteButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Uri contentUri = MediaStore.Files.getContentUri("external");

        String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";

        String[] selectionArgs = new String[]{Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/"};    //must include "/" in front and end

        Cursor cursor = getContentResolver().query(contentUri, null, selection, selectionArgs, null);

        Uri uri = null;

        if (cursor.getCount() == 0) {
            Toast.makeText(view.getContext(), "No file found in \"" + Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/\"", Toast.LENGTH_LONG).show();
        } else {
            while (cursor.moveToNext()) {
                String fileName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));

                if (fileName.equals("menuCategory.txt")) {                          //must include extension
                    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));

                    uri = ContentUris.withAppendedId(contentUri, id);

                    break;
                }
            }

            if (uri == null) {
                Toast.makeText(view.getContext(), "\"menuCategory.txt\" not found", Toast.LENGTH_SHORT).show();
            } else {
                try {
                    OutputStream outputStream = getContentResolver().openOutputStream(uri, "rwt");      //overwrite mode, see below

                    outputStream.write("This is overwritten data。\n你就不要想起我。".getBytes());

                    outputStream.close();

                    Toast.makeText(view.getContext(), "File written successfully", Toast.LENGTH_SHORT).show();
                } catch (IOException e) {
                    Toast.makeText(view.getContext(), "Fail to write file", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
});

演示: https://www.youtube.com/watch?v=idsUMiWjfnM

希望这可以帮助到您。


1
@sureshbabu 外部的话,你可以查看我的演示: https://www.youtube.com/watch?v=idsUMiWjfnM - Sam Chen
对我来说,我只是放弃它们 : )。对于旧设备,您应该使用旧方法,只需搜索其他帖子,如“Android在外部存储中创建保存文件”。 - Sam Chen
@SamChen 谢谢。我知道旧方法...但它是否兼容?如果用户从Android9升级到10,那么使用旧方法保存的文件将可用/可检测吗?在mediastore查询中。 - Sourav Kannantha B
如何将图像保存在外部SDCARD的DCIM文件夹中? - Milan Tejani
@vijaya zararia 是的。 - Sam Chen
显示剩余15条评论

3
正如您所提到的,Environment.getExternalStoragePublicDirectory已被标记为过时。因此,没有常规方法可以获得下载目录的路径以将文件保存在其中。作为替代方案,您可以使用ACTION_CREATE_DOCUMENT来显示路径选择器,然后使用返回的uri将文件写入所选位置。
以下是显示选择器的方法:
// Request code for creating a document.

const val CREATE_FILE = 1

private fun createFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "text/plain"
        putExtra(Intent.EXTRA_TITLE, "sam.txt")

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker before your app creates the document.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, CREATE_FILE)
}

以下是获取选定URI并写入文件的方法:

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == CREATE_FILE && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        resultData?.data?.also { outputUri ->
            // Perform operations on the document using its URI.
            FileInputStream(inputFile).use { inputStream ->
                context.contentResolver.openFileDescriptor(outputUri, "w")?.use {
                    FileOutputStream(it.fileDescriptor).use { outputStream ->
                        FileUtils.copy(inputStream, outputStream)
                    }
                }
            }
        }
    }
}

更多信息可以在这里找到。

编辑:

可以使用ACTION_OPEN_DOCUMENT_TREE来选择要持久保存文件的目录。然后使用takePersistableUriPermission方法获得持续授权,以便在设备重新启动后继续使用它。然后使用DocumentFile执行文件操作。

打开目录请求:

private static final int OPEN_DIRECTORY_REQUEST_CODE = 1;

void openDirectory() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    startActivityForResult(intent, OPEN_DIRECTORY_REQUEST_CODE);
}

接收选定目录并获取可持续权限:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == OPEN_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        Uri directoryUri = data.getData();
        if (directoryUri == null)
            return;
        requireContext()
                .getContentResolver()
                .takePersistableUriPermission(directoryUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        // persist picked uri to be able to reuse it later
    } else
        super.onActivityResult(requestCode, resultCode, data);
}

最后持久化该文件:
private void persistFile(@NonNull Uri directoryUri,
                         @NonNull File fileToPersist,
                         @NonNull String mimeType,
                         @NonNull String displayName) {
    DocumentFile dirFile = DocumentFile.fromSingleUri(requireContext(), directoryUri);
    if (dirFile != null) {
        DocumentFile file = dirFile.createFile(mimeType, displayName);
        if (file != null) {
            Uri outputUri = file.getUri();
            try (ParcelFileDescriptor fd = requireContext().getContentResolver().openFileDescriptor(outputUri, "w")) {
                if (fd != null) {
                    try (FileInputStream inputStream = new FileInputStream(fileToPersist)) {
                        try (FileOutputStream outputStream = new FileOutputStream(fd.getFileDescriptor())) {
                            FileUtils.copy(inputStream, outputStream);
                        }
                    }
                }
            } catch (Throwable th) {
                th.printStackTrace();
            }
        }
    }
}

查看此存储库,了解如何使用ACTION_CREATE_DOCUMENT功能。


请分享Java中onActivityResult的代码,还有两个疑问:1. 我的文件存储在Android/data/Files/sam.txt,我想使用SAF将其复制到下载文件夹或用户选择的文件夹中(如您所指导的),那么这个路径将在哪里提到。2. 我可以保存此URI以供将来使用,以便用户无需每次询问,代码自动将文件复制到那里。 - Panache
@Panache,在将文件移动或复制到外部存储随机路径之前,我建议您三思。在未来的Android版本中,应用程序必须向Google Play控制台提交使用情况,以启用对外部存储的访问权限。更多详情,请查看Android开发峰会2019年有关Scoped Storage的讲解。 - Raghul Vaikundam
@Panache 1. 这是我第二个代码片段中的_inputFile_变量,它可以是活动或片段的成员。2. 在帖子中进行了编辑。 - art
我建议您在将文件移动或复制到外部存储随机路径之前三思。抱歉,但您对OP的问题的评论并不切题。 OP尝试将现有文件复制到MediaStore中是可以的。使用MediaStore,可以在外部存储器上的文档和下载目录中进行复制而无需任何权限。@Vaikundam Raghul - blackapps
@art OP试图将现有文件复制到MediaStore。ACTION_OPEN_DOCUMENT_TREE和DocumentFile与解决方案无关。 - blackapps

0
你可以创建一个函数,该函数接受文件和contentResolver,并使用输入和输出流来复制文件。
// A class representing a file (You can use DocumentFile directly instead if you like)
data class LocalMedia(
    val name: String,
    val mimeType: String,
    val uri: Uri = Uri.EMPTY,
)

class MyViewModel: ViewModel() {

    private val downloadsFolder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)

    @Suppress("BlockingMethodInNonBlockingContext")
    fun copyDocumentFile(
        contentResolver: ContentResolver,
        localMedia: LocalMedia,
    ) {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                var inputStream: InputStream? = null
                var outputStream: OutputStream? = null
                try {
                    val outFile = DocumentFile.fromFile(downloadsFolder).createFile(localMedia.mimeType, localMedia.name)
                    if (outFile != null) {
                        inputStream = contentResolver.openInputStream(localMedia.uri)
                        outputStream = contentResolver.openOutputStream(outFile.uri)
                        if (inputStream != null && outputStream != null) {
                            outputStream.write(inputStream.readBytes())
                        }
                    }
                } catch (e: IOException) {
                    log("Failed to save file. $e")
                    _state.update { it.copy(error = e.message) }
                } finally {
                    inputStream?.close()
                    outputStream?.close()
                }
            }
        }
    }

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接