ImageSaver.kt 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. package eu.kanade.tachiyomi.data.saver
  2. import android.content.ContentUris
  3. import android.content.Context
  4. import android.graphics.Bitmap
  5. import android.net.Uri
  6. import android.os.Build
  7. import android.os.Environment
  8. import android.provider.MediaStore
  9. import androidx.annotation.RequiresApi
  10. import androidx.core.content.contentValuesOf
  11. import androidx.core.net.toUri
  12. import eu.kanade.tachiyomi.R
  13. import eu.kanade.tachiyomi.util.storage.DiskUtil
  14. import eu.kanade.tachiyomi.util.storage.cacheImageDir
  15. import eu.kanade.tachiyomi.util.storage.getUriCompat
  16. import logcat.LogPriority
  17. import okio.IOException
  18. import tachiyomi.core.util.system.ImageUtil
  19. import tachiyomi.core.util.system.logcat
  20. import java.io.ByteArrayInputStream
  21. import java.io.ByteArrayOutputStream
  22. import java.io.File
  23. import java.io.InputStream
  24. import java.util.Date
  25. class ImageSaver(
  26. val context: Context,
  27. ) {
  28. fun save(image: Image): Uri {
  29. val data = image.data
  30. val type = ImageUtil.findImageType(data) ?: throw IllegalArgumentException("Not an image")
  31. val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
  32. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
  33. return save(data(), image.location.directory(context), filename)
  34. }
  35. return saveApi29(image, type, filename, data)
  36. }
  37. private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
  38. directory.mkdirs()
  39. val destFile = File(directory, filename)
  40. inputStream.use { input ->
  41. destFile.outputStream().use { output ->
  42. input.copyTo(output)
  43. }
  44. }
  45. DiskUtil.scanMedia(context, destFile.toUri())
  46. return destFile.getUriCompat(context)
  47. }
  48. @RequiresApi(Build.VERSION_CODES.Q)
  49. private fun saveApi29(
  50. image: Image,
  51. type: ImageUtil.ImageType,
  52. filename: String,
  53. data: () -> InputStream,
  54. ): Uri {
  55. val pictureDir =
  56. MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
  57. val imageLocation = (image.location as Location.Pictures).relativePath
  58. val relativePath = listOf(
  59. Environment.DIRECTORY_PICTURES,
  60. context.getString(R.string.app_name),
  61. imageLocation,
  62. ).joinToString(File.separator)
  63. val contentValues = contentValuesOf(
  64. MediaStore.Images.Media.RELATIVE_PATH to relativePath,
  65. MediaStore.Images.Media.DISPLAY_NAME to image.name,
  66. MediaStore.Images.Media.MIME_TYPE to type.mime,
  67. MediaStore.Images.Media.DATE_MODIFIED to Date().time * 1000,
  68. )
  69. val picture = findUriOrDefault(relativePath, filename) {
  70. context.contentResolver.insert(
  71. pictureDir,
  72. contentValues,
  73. ) ?: throw IOException(context.getString(R.string.error_saving_picture))
  74. }
  75. try {
  76. data().use { input ->
  77. context.contentResolver.openOutputStream(picture, "w").use { output ->
  78. input.copyTo(output!!)
  79. }
  80. }
  81. } catch (e: Exception) {
  82. logcat(LogPriority.ERROR, e)
  83. throw IOException(context.getString(R.string.error_saving_picture))
  84. }
  85. DiskUtil.scanMedia(context, picture)
  86. return picture
  87. }
  88. @RequiresApi(Build.VERSION_CODES.Q)
  89. private fun findUriOrDefault(path: String, filename: String, default: () -> Uri): Uri {
  90. val projection = arrayOf(
  91. MediaStore.MediaColumns._ID,
  92. MediaStore.MediaColumns.DISPLAY_NAME,
  93. MediaStore.MediaColumns.RELATIVE_PATH,
  94. )
  95. val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?"
  96. // Need to make sure it ends with the separator
  97. val normalizedPath = "${path.removeSuffix(File.separator)}${File.separator}"
  98. context.contentResolver.query(
  99. MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
  100. projection,
  101. selection,
  102. arrayOf(normalizedPath, filename),
  103. null,
  104. ).use { cursor ->
  105. if (cursor != null && cursor.count >= 1) {
  106. if (cursor.moveToFirst()) {
  107. val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
  108. return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
  109. }
  110. }
  111. }
  112. return default()
  113. }
  114. }
  115. sealed class Image(
  116. open val name: String,
  117. open val location: Location,
  118. ) {
  119. data class Cover(
  120. val bitmap: Bitmap,
  121. override val name: String,
  122. override val location: Location,
  123. ) : Image(name, location)
  124. data class Page(
  125. val inputStream: () -> InputStream,
  126. override val name: String,
  127. override val location: Location,
  128. ) : Image(name, location)
  129. val data: () -> InputStream
  130. get() {
  131. return when (this) {
  132. is Cover -> {
  133. {
  134. val baos = ByteArrayOutputStream()
  135. bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
  136. ByteArrayInputStream(baos.toByteArray())
  137. }
  138. }
  139. is Page -> inputStream
  140. }
  141. }
  142. }
  143. sealed interface Location {
  144. data class Pictures private constructor(val relativePath: String) : Location {
  145. companion object {
  146. fun create(relativePath: String = ""): Pictures {
  147. return Pictures(relativePath)
  148. }
  149. }
  150. }
  151. data object Cache : Location
  152. fun directory(context: Context): File {
  153. return when (this) {
  154. Cache -> context.cacheImageDir
  155. is Pictures -> {
  156. val file = File(
  157. Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
  158. context.getString(R.string.app_name),
  159. )
  160. if (relativePath.isNotEmpty()) {
  161. return File(
  162. file,
  163. relativePath,
  164. )
  165. }
  166. file
  167. }
  168. }
  169. }
  170. }