如何将图像保存在 android Q 的子目录中,同时保持向后兼容

How to save an image in a subdirectory on android Q whilst remaining backwards compatible

我正在创建一个简单的图像编辑器应用程序,因此需要加载和保存图像文件。我希望保存的文件显示在单独相册的图库中。从 Android API 28 到 29,应用程序能够访问存储的范围发生了巨大变化。我可以在 Android Q (API 29) 中做我想做的事,但这种方式不向后兼容。

当我想在较低的 API 版本中实现相同的结果时,到目前为止我只找到了方法,这需要使用已弃用的代码(截至 API 29)。

其中包括:

我的问题是:是否有可能以这种方式实现它,使其向后兼容,但不需要弃用代码?如果不是,在这种情况下是否可以使用已弃用的代码,或者这些方法会很快从 sdk 中删除吗?无论如何,使用已弃用的方法感觉很糟糕,所以我宁愿不使用 :)

这是我发现的适用于 API 29:

的方法
ContentValues values = new ContentValues();
String filename = System.currentTimeMillis() + ".jpg";

values.put(MediaStore.Images.Media.TITLE, filename);
values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.Images.Media.RELATIVE_PATH, "PATH/TO/ALBUM");

getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values);

然后我使用插入方法返回的URI 来保存位图。问题是字段 RELATIVE_PATH 是在 API 29 中引入的,所以当我 运行 较低版本的代码时,图像被放入 "Pictures" 文件夹而不是"PATH/TO/ALBUM" 个文件夹。

is it okay to use deprecated code in this situation or will these methods soon be deleted from the sdk?

DATA 选项对 Android Q 不起作用,因为该数据不包含在 query() 结果中,即使您要求它也不能使用返回的路径它,即使他们被退回。

默认情况下,Environment.getExternalStoragePublicDirectory(...) 选项在 Android Q 上不起作用,但您可以向 re-enable 添加清单条目。但是,清单条目可能会在 Android R 中删除,所以除非你时间不够,否则我不会走这条路。

据我所知,MediaStore.Images.Media.insertImage(...) 仍然有效,即使它已被弃用。

is it possible to implement it in such a way, so it's backwards compatible, but doesn't require deprecated code?

我的猜测是您将需要使用两种不同的存储策略,一种用于 API 级别 29+,另一种用于旧设备。我在 this sample app 中采用了这种方法,尽管我正在处理视频内容,而不是图像,所以 insertImage() 不是一个选项。

这是适合我的代码。此代码将图像保存到 phone 上的子目录文件夹中。它检查 android 版本的 phone,如果它高于 android q,它运行所需的代码,如果低于它,它运行 else 语句中的代码。

来源:https://androidnoon.com/save-file-in-android-10-and-below-using-scoped-storage-in-android-studio/

 private void saveImageToStorage(Bitmap bitmap) throws IOException {
    OutputStream imageOutStream;
   
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DISPLAY_NAME, 
        "image_screenshot.jpg");
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        values.put(MediaStore.Images.Media.RELATIVE_PATH, 
         Environment.DIRECTORY_PICTURES + File.pathSeparator + "AppName");

        Uri uri = 
     getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
    values);

        imageOutStream = getContentResolver().openOutputStream(uri);

    } else {

        String imagesDir = 
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES). toString() + "/AppName";
        File image = new File(imagesDir, "image_screenshot.jpg");
        imageOutStream = new FileOutputStream(image);
    }

   
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
        imageOutStream.close();
    

}

对于旧 API (<29),我将图像放入外部媒体目录并通过 MediaScannerConnection 扫描它。

让我们看看我的代码。

此函数创建图像文件。注意 appName 变量 - 它是将显示图像的相册的名称。

override fun createImageFile(appName: String): File {
    val dir = File(appContext.externalMediaDirs[0], appName)
    if(!dir.exists()) {
        ir.mkdir()
    }

    return File(dir, createFileName())
}

然后,我将图像放入文件中,最后,我 运行 像这样的媒体扫描仪:

private suspend fun scanNewFile(shot: File): Uri? {
    return suspendCancellableCoroutine { continuation ->
        MediaScannerConnection.scanFile(
            appContext, 
            arrayOf<String>(shot.absolutePath), 
            arrayOf(imageMimeType)) { _, uri -> continuation.resume(uri)
        }
    }
}

经过反复试验,我发现可以以向后兼容的方式使用 MediaStore,以便在不同版本的实现之间共享尽可能多的代码。唯一的技巧是记住,如果您使用 MediaColumns.DATA,您 需要自己创建文件

让我们看看 code from my project (Kotlin)。此示例用于保存音频,而不是图像,但您只需将 MIME_TYPEDIRECTORY_MUSIC 替换为您需要的任何内容即可。

private fun newFile(): FileDescriptor? {
    // Create a file descriptor for a new recording.
    val date = DateFormat.getDateTimeInstance().format(Calendar.getInstance().time)
    val filename = "$date.mp3"

    val values = ContentValues().apply {
        put(MediaColumns.TITLE, date)
        put(MediaColumns.MIME_TYPE, "audio/mp3")

        // store the file in a subdirectory
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaColumns.DISPLAY_NAME, filename)
            put(MediaColumns.RELATIVE_PATH, saveTo)
        } else {
            // RELATIVE_PATH was added in Q, so work around it by using DATA and creating the file manually
            @Suppress("DEPRECATION")
            val music = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).path

            with(File("$music/P2oggle/$filename")) {
                @Suppress("DEPRECATION")
                put(MediaColumns.DATA, path)

                parentFile!!.mkdir()
                createNewFile()
            }
        }
    }

    val uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values)!!
    return contentResolver.openFileDescriptor(uri, "w")?.fileDescriptor
}

在Android 10及以上版本,我们使用DISPLAY_NAME设置文件名,RELATIVE_PATH设置子目录。在旧版本中,我们使用 DATA 并手动创建文件(及其目录)。在此之后,两者的实现是相同的:我们只是从 MediaStore 和 return 中提取文件描述符以供使用。