feat(file): 添加图片保存到系统相册功能

- 在 FileSDK 中新增 saveImageToGallery 方法用于将图片文件保存到系统照片库
- 支持 Android Q 及以上版本使用 MediaStore API 安全存储图片
- 对于旧版 Android 系统使用兼容方案将图片复制到 Pictures 目录
- 在 WebView 组件下载完成后自动将图片文件保存到相册
- 仅当图片未被保存到相册时才显示文件打开选项以避免重复操作
这个提交包含在:
XuqmGroup 2026-06-10 12:39:40 +08:00
父节点 1e7193add3
当前提交 ff13b54d9e
共有 2 个文件被更改,包括 46 次插入1 次删除

查看文件

@ -3,11 +3,13 @@ package com.xuqm.sdk.file
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -269,6 +271,46 @@ object FileSDK {
return target return target
} }
/**
* Saves an image file to the system photo gallery (MediaStore).
* Returns true if the file is an image and was saved successfully.
*/
fun saveImageToGallery(context: Context, file: File): Boolean {
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(file.extension.lowercase()) ?: return false
if (!mimeType.startsWith("image/")) return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
runCatching {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, file.name)
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
) ?: return false
context.contentResolver.openOutputStream(uri)?.use { out ->
file.inputStream().use { it.copyTo(out) }
}
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
context.contentResolver.update(uri, values, null, null)
true
}.getOrDefault(false)
} else {
@Suppress("DEPRECATION")
runCatching {
val picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
picturesDir.mkdirs()
val dest = uniqueFile(picturesDir, file.name)
file.copyTo(dest)
context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(dest)))
true
}.getOrDefault(false)
}
}
fun openFile(context: Context, file: File) { fun openFile(context: Context, file: File) {
val mimeType = MimeTypeMap.getSingleton() val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(file.extension.lowercase()) .getMimeTypeFromExtension(file.extension.lowercase())

查看文件

@ -272,9 +272,12 @@ fun XWebViewView(
runCatching { runCatching {
FileSDK.saveBlobDownload(context, b64, filename, config.downloadDestination) FileSDK.saveBlobDownload(context, b64, filename, config.downloadDestination)
}.onSuccess { file -> }.onSuccess { file ->
// For image files, save to the system gallery (相册) so they appear in Photos
val savedToGallery = runCatching { FileSDK.saveImageToGallery(context, file) }.getOrDefault(false)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
dispatchDownloadEvent("__xwvDownloadDone", url, ",success:true") dispatchDownloadEvent("__xwvDownloadDone", url, ",success:true")
FileSDK.openFile(context, file) // Only open with file viewer when not saved to gallery (e.g. zip, docx)
if (!savedToGallery) FileSDK.openFile(context, file)
} }
}.onFailure { e -> }.onFailure { e ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {