ソースを参照

使用MediaStore保存图片

HapeLee 1 年間 前
親
コミット
deb945dfbc

+ 37 - 58
app/src/main/java/io/legato/kazusa/ui/book/read/ReadBookActivity.kt

@@ -19,9 +19,6 @@ import androidx.activity.addCallback
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.widget.PopupMenu
-import androidx.core.net.toUri
-import androidx.core.view.doOnLayout
-import androidx.core.view.doOnPreDraw
 import androidx.core.view.get
 import androidx.core.view.size
 import androidx.lifecycle.lifecycleScope
@@ -30,7 +27,6 @@ import com.google.android.material.transition.platform.MaterialSharedAxis
 import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
 import io.legato.kazusa.BuildConfig
 import io.legato.kazusa.R
-import io.legato.kazusa.constant.AppConst
 import io.legato.kazusa.constant.AppLog
 import io.legato.kazusa.constant.BookType
 import io.legato.kazusa.constant.EventBus
@@ -99,13 +95,11 @@ import io.legato.kazusa.ui.book.toc.TocActivityResult
 import io.legato.kazusa.ui.book.toc.rule.TxtTocRuleDialog
 import io.legato.kazusa.ui.browser.WebViewActivity
 import io.legato.kazusa.ui.dict.DictDialog
-import io.legato.kazusa.ui.file.HandleFileContract
 import io.legato.kazusa.ui.login.SourceLoginActivity
 import io.legato.kazusa.ui.replace.ReplaceRuleActivity
 import io.legato.kazusa.ui.replace.edit.ReplaceEditActivity
 import io.legato.kazusa.ui.widget.PopupAction
 import io.legato.kazusa.ui.widget.dialog.PhotoDialog
-import io.legato.kazusa.utils.ACache
 import io.legato.kazusa.utils.Debounce
 import io.legato.kazusa.utils.LogUtils
 import io.legato.kazusa.utils.NetworkUtils
@@ -120,7 +114,6 @@ import io.legato.kazusa.utils.iconItemOnLongClick
 import io.legato.kazusa.utils.invisible
 import io.legato.kazusa.utils.isAbsUrl
 import io.legato.kazusa.utils.isTrue
-import io.legato.kazusa.utils.launch
 import io.legato.kazusa.utils.navigationBarGravity
 import io.legato.kazusa.utils.observeEvent
 import io.legato.kazusa.utils.observeEventSticky
@@ -212,12 +205,6 @@ class ReadBookActivity : BaseReadBookActivity(),
                 ReadBook.loadOrUpContent()
             }
         }
-    private val selectImageDir = registerForActivityResult(HandleFileContract()) {
-        it.uri?.let { uri ->
-            ACache.get().put(AppConst.imagePathKey, uri.toString())
-            viewModel.saveImage(it.value, uri)
-        }
-    }
     private var menu: Menu? = null
     private var backupJob: Job? = null
     private var tts: TTS? = null
@@ -267,7 +254,7 @@ class ReadBookActivity : BaseReadBookActivity(),
     override fun onCreate(savedInstanceState: Bundle?) {
 
         if (AppConfig.sharedElementEnterTransitionEnable) {
-            postponeEnterTransition()
+
             val transform = MaterialContainerTransform().apply {
                 addTarget(binding.rootView)
                 scrimColor = Color.TRANSPARENT
@@ -275,30 +262,33 @@ class ReadBookActivity : BaseReadBookActivity(),
             }
             window.sharedElementEnterTransition = transform
 
-            window.sharedElementReturnTransition =
-                MaterialSharedAxis(MaterialSharedAxis.X, true).apply {
-                    duration = 300
-                }
-
             if (AppConfig.delayBookLoadEnable) {
                 window.sharedElementEnterTransition?.addListener(object :
                     Transition.TransitionListener {
                     override fun onTransitionStart(transition: Transition) {}
                     override fun onTransitionEnd(transition: Transition) {
-                        binding.readView.doOnLayout {
+                        lifecycleScope.launch(Dispatchers.Default) {
                             viewModel.initReadBookConfig(intent)
                             viewModel.initData(intent)
-                            binding.readView.initAfterTransition()
-                            justInitData = true
+
+                            withContext(Main) {
+                                binding.readView.initAfterTransition()
+                                justInitData = true
+                            }
                         }
                         transition.removeListener(this)
                     }
 
                     override fun onTransitionCancel(transition: Transition) {
-                        viewModel.initReadBookConfig(intent)
-                        viewModel.initData(intent)
-                        binding.readView.initAfterTransition()
-                        justInitData = true
+                        lifecycleScope.launch(Dispatchers.Default) {
+                            viewModel.initReadBookConfig(intent)
+                            viewModel.initData(intent)
+
+                            withContext(Main) {
+                                binding.readView.initAfterTransition()
+                                justInitData = true
+                            }
+                        }
                         transition.removeListener(this)
                     }
 
@@ -307,32 +297,21 @@ class ReadBookActivity : BaseReadBookActivity(),
                 })
             }
         }
-
-        super.onCreate(savedInstanceState)
-
-        if (!AppConfig.delayBookLoadEnable || !AppConfig.sharedElementEnterTransitionEnable) {
-            lifecycleScope.launch(Dispatchers.Default) {
-                viewModel.initReadBookConfig(intent)
-                viewModel.initData(intent)
-
-                withContext(Main) {
-                    binding.readView.initAfterTransition()
-                    justInitData = true
-                }
+        window.sharedElementReturnTransition =
+            MaterialSharedAxis(MaterialSharedAxis.X, true).apply {
+                duration = 300
             }
-        }
-
+        super.onCreate(savedInstanceState)
 
         if (AppConfig.sharedElementEnterTransitionEnable)
             binding.rootView.transitionName = intent.getStringExtra("transitionName")
 
+        upScreenTimeOut()
+        ReadBook.register(this)
+
         binding.cursorLeft.setOnTouchListener(this)
         binding.cursorRight.setOnTouchListener(this)
 
-        binding.rootView.doOnPreDraw {
-            startPostponedEnterTransition()
-        }
-
         onBackPressedDispatcher.addCallback(this) {
             if (isShowingSearchResult) {
                 exitSearchMenu()
@@ -362,9 +341,19 @@ class ReadBookActivity : BaseReadBookActivity(),
 
     override fun onPostCreate(savedInstanceState: Bundle?) {
         super.onPostCreate(savedInstanceState)
-        upScreenTimeOut()
-        ReadBook.register(this)
+        if (!AppConfig.delayBookLoadEnable || !AppConfig.sharedElementEnterTransitionEnable) {
+            lifecycleScope.launch(Dispatchers.Default) {
+                viewModel.initReadBookConfig(intent)
+                viewModel.initData(intent)
+
+                withContext(Main) {
+                    binding.readView.initAfterTransition()
+                    justInitData = true
+                }
+            }
+        }
     }
+
     override fun onNewIntent(intent: Intent) {
         super.onNewIntent(intent)
         viewModel.initData(intent)
@@ -1445,9 +1434,8 @@ class ReadBookActivity : BaseReadBookActivity(),
             listOf(
                 SelectItem(getString(R.string.show), "show"),
                 SelectItem(getString(R.string.refresh), "refresh"),
-                SelectItem(getString(R.string.action_save), "save"),
+                SelectItem("保存到相册", "save"),
                 SelectItem(getString(R.string.menu), "menu"),
-                SelectItem(getString(R.string.select_folder), "selectFolder")
             )
         )
         popupAction.onActionClick = {
@@ -1455,18 +1443,9 @@ class ReadBookActivity : BaseReadBookActivity(),
                 "show" -> showDialogFragment(PhotoDialog(src))
                 "refresh" -> viewModel.refreshImage(src)
                 "save" -> {
-                    val path = ACache.get().getAsString(AppConst.imagePathKey)
-                    if (path.isNullOrEmpty()) {
-                        selectImageDir.launch {
-                            value = src
-                        }
-                    } else {
-                        viewModel.saveImage(src, path.toUri())
-                    }
+                    viewModel.saveImage(src)
                 }
-
                 "menu" -> showActionMenu()
-                "selectFolder" -> selectImageDir.launch()
             }
             popupAction.dismiss()
         }

+ 13 - 27
app/src/main/java/io/legato/kazusa/ui/book/read/ReadBookViewModel.kt

@@ -2,8 +2,6 @@ package io.legato.kazusa.ui.book.read
 
 import android.app.Application
 import android.content.Intent
-import android.net.Uri
-import androidx.documentfile.provider.DocumentFile
 import androidx.lifecycle.MutableLiveData
 import io.legato.kazusa.R
 import io.legato.kazusa.base.BaseViewModel
@@ -32,9 +30,7 @@ import io.legato.kazusa.model.webBook.WebBook
 import io.legato.kazusa.service.BaseReadAloudService
 import io.legato.kazusa.ui.book.read.page.entities.TextChapter
 import io.legato.kazusa.ui.book.searchContent.SearchResult
-import io.legato.kazusa.utils.DocumentUtils
-import io.legato.kazusa.utils.FileUtils
-import io.legato.kazusa.utils.isContentScheme
+import io.legato.kazusa.utils.ImageSaveUtils
 import io.legato.kazusa.utils.mapParallelSafe
 import io.legato.kazusa.utils.postEvent
 import io.legato.kazusa.utils.toStringArray
@@ -48,10 +44,7 @@ import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onEmpty
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.take
-import java.io.File
-import java.io.FileInputStream
 import java.io.FileNotFoundException
-import java.io.FileOutputStream
 import kotlin.coroutines.coroutineContext
 
 /**
@@ -504,30 +497,23 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
     /**
      * 保存图片
      */
-    fun saveImage(src: String?, uri: Uri) {
+    fun saveImage(src: String?) {
         src ?: return
         val book = ReadBook.book ?: return
+
         execute {
             val image = BookHelp.getImage(book, src)
-            FileInputStream(image).use { input ->
-                if (uri.isContentScheme()) {
-                    DocumentFile.fromTreeUri(context, uri)?.let { doc ->
-                        val imageDoc = DocumentUtils.createFileIfNotExist(doc, image.name)!!
-                        context.contentResolver.openOutputStream(imageDoc.uri)!!.use { output ->
-                            input.copyTo(output)
-                        }
-                    }
-                } else {
-                    val dir = File(uri.path ?: uri.toString())
-                    val file = FileUtils.createFileIfNotExist(dir, image.name)
-                    FileOutputStream(file).use { output ->
-                        input.copyTo(output)
-                    }
-                }
-            }
+            val byteArray = image.readBytes()
+            val success = ImageSaveUtils.saveImageToGallery(
+                context,
+                byteArray,
+                folderName = "Legado"
+            )
+            if (!success) throw NoStackTraceException("保存到相册失败")
         }.onError {
-            AppLog.put("保存图片出错\n${it.localizedMessage}", it)
-            context.toastOnUi("保存图片出错\n${it.localizedMessage}")
+            context.toastOnUi("保存图片失败: ${it.localizedMessage}")
+        }.onSuccess {
+            context.toastOnUi("已保存到相册")
         }
     }
 

+ 13 - 29
app/src/main/java/io/legato/kazusa/ui/browser/WebViewActivity.kt

@@ -28,11 +28,10 @@ import io.legato.kazusa.help.http.CookieStore
 import io.legato.kazusa.help.source.SourceVerificationHelp
 import io.legato.kazusa.lib.dialogs.SelectItem
 import io.legato.kazusa.lib.dialogs.alert
+import io.legato.kazusa.lib.dialogs.selector
 //import io.legado.app.lib.theme.accentColor
 import io.legato.kazusa.model.Download
 import io.legato.kazusa.ui.association.OnLineImportActivity
-import io.legato.kazusa.ui.file.HandleFileContract
-import io.legato.kazusa.utils.ACache
 import io.legato.kazusa.utils.gone
 import io.legato.kazusa.utils.invisible
 import io.legato.kazusa.utils.keepScreenOn
@@ -51,17 +50,10 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
 
     override val binding by viewBinding(ActivityWebViewBinding::inflate)
     override val viewModel by viewModels<WebViewModel>()
-    private val imagePathKey = "imagePath"
     private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
     private var webPic: String? = null
     private var isCloudflareChallenge = false
     private var isFullScreen = false
-    private val saveImage = registerForActivityResult(HandleFileContract()) {
-        it.uri?.let { uri ->
-            ACache.get().put(imagePathKey, uri.toString())
-            viewModel.saveImage(webPic, uri.toString())
-        }
-    }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -183,9 +175,17 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
             if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
                 hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
             ) {
-                hitTestResult.extra?.let {
-                    saveImage(it)
-                    return@setOnLongClickListener true
+                hitTestResult.extra?.let { webPic ->
+                    selector(
+                        arrayListOf(
+                            SelectItem(getString(R.string.action_save), "save"),
+                        )
+                    ) { _, charSequence, _ ->
+                        when (charSequence.value) {
+                            "save" -> saveImage(webPic)
+                        }
+                    }
+                    return@setOnLongClickListener false
                 }
             }
             return@setOnLongClickListener false
@@ -201,23 +201,7 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
 
     private fun saveImage(webPic: String) {
         this.webPic = webPic
-        val path = ACache.get().getAsString(imagePathKey)
-        if (path.isNullOrEmpty()) {
-            selectSaveFolder()
-        } else {
-            viewModel.saveImage(webPic, path)
-        }
-    }
-
-    private fun selectSaveFolder() {
-        val default = arrayListOf<SelectItem<Int>>()
-        val path = ACache.get().getAsString(imagePathKey)
-        if (!path.isNullOrEmpty()) {
-            default.add(SelectItem(path, -1))
-        }
-        saveImage.launch {
-            otherActions = default
-        }
+        viewModel.saveImage(webPic)
     }
 
     override fun finish() {

+ 16 - 25
app/src/main/java/io/legato/kazusa/ui/browser/WebViewModel.kt

@@ -2,13 +2,11 @@ package io.legato.kazusa.ui.browser
 
 import android.app.Application
 import android.content.Intent
-import android.net.Uri
 import android.util.Base64
 import android.webkit.URLUtil
 import android.webkit.WebView
-import androidx.documentfile.provider.DocumentFile
 import io.legato.kazusa.base.BaseViewModel
-import io.legato.kazusa.constant.AppConst
+import io.legato.kazusa.constant.AppConst.imagePathKey
 import io.legato.kazusa.constant.SourceType
 import io.legato.kazusa.data.appDb
 import io.legato.kazusa.exception.NoStackTraceException
@@ -17,15 +15,11 @@ import io.legato.kazusa.help.http.okHttpClient
 import io.legato.kazusa.help.source.SourceHelp
 import io.legato.kazusa.help.source.SourceVerificationHelp
 import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
-import io.legato.kazusa.utils.DocumentUtils
-import io.legato.kazusa.utils.FileUtils
-import io.legato.kazusa.utils.isContentScheme
+import io.legato.kazusa.utils.ACache
+import io.legato.kazusa.utils.ImageSaveUtils
 import io.legato.kazusa.utils.printOnDebug
 import io.legato.kazusa.utils.toastOnUi
-import io.legato.kazusa.utils.writeBytes
 import org.apache.commons.text.StringEscapeUtils
-import java.io.File
-import java.util.Date
 
 class WebViewModel(application: Application) : BaseViewModel(application) {
     var intent: Intent? = null
@@ -66,26 +60,23 @@ class WebViewModel(application: Application) : BaseViewModel(application) {
         }
     }
 
-    fun saveImage(webPic: String?, path: String) {
+    fun saveImage(webPic: String?) {
         webPic ?: return
         execute {
-            val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg"
-            webData2bitmap(webPic)?.let { biteArray ->
-                if (path.isContentScheme()) {
-                    val uri = Uri.parse(path)
-                    DocumentFile.fromTreeUri(context, uri)?.let { doc ->
-                        DocumentUtils.createFileIfNotExist(doc, fileName)
-                            ?.writeBytes(context, biteArray)
-                    }
-                } else {
-                    val file = FileUtils.createFileIfNotExist(File(path), fileName)
-                    file.writeBytes(biteArray)
-                }
-            } ?: throw Throwable("NULL")
+            val byteArray = webData2bitmap(webPic) ?: throw Throwable("NULL")
+
+            val success = ImageSaveUtils.saveImageToGallery(
+                context,
+                byteArray,
+                folderName = "Legado"
+            )
+
+            if (!success) throw Throwable("保存到相册失败")
         }.onError {
-            context.toastOnUi("保存图片失败:${it.localizedMessage}")
+            ACache.get().remove(imagePathKey)
+            context.toastOnUi("保存图片失败: ${it.localizedMessage}")
         }.onSuccess {
-            context.toastOnUi("保存成功")
+            context.toastOnUi("已保存到相册")
         }
     }
 

+ 1 - 29
app/src/main/java/io/legato/kazusa/ui/rss/read/ReadRssActivity.kt

@@ -30,7 +30,6 @@ import com.script.rhino.runScriptWithContext
 import io.legato.kazusa.R
 import io.legato.kazusa.base.VMBaseActivity
 import io.legato.kazusa.constant.AppConst
-import io.legato.kazusa.constant.AppConst.imagePathKey
 import io.legato.kazusa.constant.AppLog
 import io.legato.kazusa.data.entities.RssSource
 import io.legato.kazusa.databinding.ActivityRssReadBinding
@@ -40,10 +39,8 @@ import io.legato.kazusa.lib.dialogs.SelectItem
 import io.legato.kazusa.lib.dialogs.selector
 import io.legato.kazusa.model.Download
 import io.legato.kazusa.ui.association.OnLineImportActivity
-import io.legato.kazusa.ui.file.HandleFileContract
 import io.legato.kazusa.ui.login.SourceLoginActivity
 import io.legato.kazusa.ui.rss.favorites.RssFavoritesDialog
-import io.legato.kazusa.utils.ACache
 import io.legato.kazusa.utils.NetworkUtils
 import io.legato.kazusa.utils.gone
 import io.legato.kazusa.utils.invisible
@@ -83,12 +80,6 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
     private var ttsMenuItem: MenuItem? = null
     private var isFullScreen = false
     private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
-    private val selectImageDir = registerForActivityResult(HandleFileContract()) {
-        it.uri?.let { uri ->
-            ACache.get().put(imagePathKey, uri.toString())
-            viewModel.saveImage(it.value, uri)
-        }
-    }
     private val rssJsExtensions by lazy { RssJsExtensions(this) }
 
     fun getSource(): RssSource? {
@@ -257,12 +248,10 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
                     selector(
                         arrayListOf(
                             SelectItem(getString(R.string.action_save), "save"),
-                            SelectItem(getString(R.string.select_folder), "selectFolder")
                         )
                     ) { _, charSequence, _ ->
                         when (charSequence.value) {
                             "save" -> saveImage(webPic)
-                            "selectFolder" -> selectSaveFolder(null)
                         }
                     }
                     return@setOnLongClickListener true
@@ -281,24 +270,7 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
     }
 
     private fun saveImage(webPic: String) {
-        val path = ACache.get().getAsString(imagePathKey)
-        if (path.isNullOrEmpty()) {
-            selectSaveFolder(webPic)
-        } else {
-            viewModel.saveImage(webPic, path.toUri())
-        }
-    }
-
-    private fun selectSaveFolder(webPic: String?) {
-        val default = arrayListOf<SelectItem<Int>>()
-        val path = ACache.get().getAsString(imagePathKey)
-        if (!path.isNullOrEmpty()) {
-            default.add(SelectItem(path, -1))
-        }
-        selectImageDir.launch {
-            otherActions = default
-            value = webPic
-        }
+        viewModel.saveImage(webPic)
     }
 
     @SuppressLint("SetJavaScriptEnabled")

+ 12 - 9
app/src/main/java/io/legato/kazusa/ui/rss/read/ReadRssViewModel.kt

@@ -2,14 +2,12 @@ package io.legato.kazusa.ui.rss.read
 
 import android.app.Application
 import android.content.Intent
-import android.net.Uri
 import android.util.Base64
 import android.webkit.URLUtil
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
 import com.script.rhino.runScriptWithContext
 import io.legato.kazusa.base.BaseViewModel
-import io.legato.kazusa.constant.AppConst
 import io.legato.kazusa.data.appDb
 import io.legato.kazusa.data.entities.RssArticle
 import io.legato.kazusa.data.entities.RssSource
@@ -20,11 +18,10 @@ import io.legato.kazusa.help.http.newCallResponseBody
 import io.legato.kazusa.help.http.okHttpClient
 import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
 import io.legato.kazusa.model.rss.Rss
+import io.legato.kazusa.utils.ImageSaveUtils
 import io.legato.kazusa.utils.toastOnUi
-import io.legato.kazusa.utils.writeBytes
 import kotlinx.coroutines.Dispatchers.IO
 import splitties.init.appCtx
-import java.util.Date
 import kotlin.coroutines.coroutineContext
 
 
@@ -170,19 +167,25 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
         }
     }
 
-    fun saveImage(webPic: String?, uri: Uri) {
+    fun saveImage(webPic: String?) {
         webPic ?: return
         execute {
-            val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg"
             val byteArray = webData2bitmap(webPic) ?: throw NoStackTraceException("NULL")
-            uri.writeBytes(context, fileName, byteArray)
+
+            val success = ImageSaveUtils.saveImageToGallery(
+                context,
+                byteArray,
+                folderName = "Legado" // 可以自定义相册子目录
+            )
+            if (!success) throw NoStackTraceException("保存到相册失败")
         }.onError {
-            context.toastOnUi("保存图片失败:${it.localizedMessage}")
+            context.toastOnUi("保存图片失败: ${it.localizedMessage}")
         }.onSuccess {
-            context.toastOnUi("保存成功")
+            context.toastOnUi("已保存到相册")
         }
     }
 
+
     private suspend fun webData2bitmap(data: String): ByteArray? {
         return if (URLUtil.isValidUrl(data)) {
             okHttpClient.newCallResponseBody {

+ 58 - 0
app/src/main/java/io/legato/kazusa/utils/ImageSaveUtils.kt

@@ -0,0 +1,58 @@
+package io.legato.kazusa.utils
+
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.provider.MediaStore
+import io.legato.kazusa.constant.AppConst
+import java.util.Date
+
+object ImageSaveUtils {
+
+    /**
+     * 使用 MediaStore 保存图片到系统相册
+     * @param context Context
+     * @param byteArray 图片数据(JPG/PNG 的 byte[])
+     * @param prefix 文件名前缀,默认 "IMG_"
+     * @param folderName 相册下的子文件夹,默认 "Legado"
+     */
+    fun saveImageToGallery(
+        context: Context,
+        byteArray: ByteArray,
+        prefix: String = "IMG_",
+        folderName: String = "Legado"
+    ): Boolean {
+        return try {
+            val fileName = prefix + AppConst.fileNameFormat.format(Date()) + ".jpg"
+
+            val contentValues = ContentValues().apply {
+                put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
+                put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                    // Android10以上指定相对路径
+                    put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/$folderName")
+                    put(MediaStore.Images.Media.IS_PENDING, 1)
+                }
+            }
+
+            val resolver = context.contentResolver
+            val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+                ?: return false
+
+            resolver.openOutputStream(uri)?.use { outputStream ->
+                outputStream.write(byteArray)
+            }
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                contentValues.clear()
+                contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+                resolver.update(uri, contentValues, null, null)
+            }
+
+            true
+        } catch (e: Exception) {
+            e.printStackTrace()
+            false
+        }
+    }
+}