Share logic for saving page/cover (#6787)

* Use MediaStore on newer Android Q or newer

* Use flow instead of Observable

* Review comment fixes

* Use suspended function instead of flow
This commit is contained in:
Andreas
2022-03-19 21:46:23 +01:00
committed by GitHub
parent ddb856edc7
commit 1163aa4e4e
10 changed files with 265 additions and 143 deletions
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.ProgressDialog
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
@@ -13,6 +12,7 @@ import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.graphics.drawable.RippleDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.Gravity
@@ -69,13 +69,13 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.util.preference.toggle
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
import eu.kanade.tachiyomi.util.system.getThemeColor
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.copy
import eu.kanade.tachiyomi.util.view.popupMenu
@@ -89,7 +89,6 @@ import kotlinx.coroutines.flow.sample
import logcat.LogPriority
import nucleus.factory.RequiresPresenter
import uy.kohesive.injekt.injectLazy
import java.io.File
import kotlin.math.abs
import kotlin.math.max
@@ -830,18 +829,14 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
* Called from the presenter when a page is ready to be shared. It shows Android's default
* sharing tool.
*/
fun onShareImageResult(file: File, page: ReaderPage) {
fun onShareImageResult(uri: Uri, page: ReaderPage) {
val manga = presenter.manga ?: return
val chapter = page.chapter.chapter
val uri = file.getUriCompat(this)
val intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, getString(R.string.share_page_info, manga.title, chapter.name, page.number))
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
type = "image/*"
}
val intent = uri.toShareIntent(
context = applicationContext,
message = getString(R.string.share_page_info, manga.title, chapter.name, page.number)
)
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
}
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader
import android.app.Application
import android.net.Uri
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
@@ -10,6 +11,9 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.saver.Image
import eu.kanade.tachiyomi.data.saver.ImageSaver
import eu.kanade.tachiyomi.data.saver.Location
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
@@ -28,11 +32,10 @@ import eu.kanade.tachiyomi.util.chapter.getChapterSort
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getPicturesDir
import eu.kanade.tachiyomi.util.storage.getTempShareDir
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.updateCoverLastModified
@@ -45,7 +48,7 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import uy.kohesive.injekt.injectLazy
import java.util.Date
import java.util.concurrent.TimeUnit
@@ -92,6 +95,8 @@ class ReaderPresenter(
*/
private val isLoadingAdjacentChapterRelay = BehaviorRelay.create<Boolean>()
private val imageSaver: ImageSaver by injectLazy()
/**
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
* time in a background thread to avoid blocking the UI.
@@ -560,32 +565,6 @@ class ReaderPresenter(
})
}
/**
* Saves the image of this [page] in the given [directory] and returns the file location.
*/
private fun saveImage(page: ReaderPage, directory: File, manga: Manga): File {
val stream = page.stream!!
val type = ImageUtil.findImageType(stream) ?: throw Exception("Not an image")
directory.mkdirs()
val chapter = page.chapter.chapter
// Build destination file.
val filenameSuffix = " - ${page.number}.${type.extension}"
val filename = DiskUtil.buildValidFilename(
"${manga.title} - ${chapter.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize())
) + filenameSuffix
val destFile = File(directory, filename)
stream().use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
return destFile
}
/**
* Saves the image of this [page] on the pictures directory and notifies the UI of the result.
* There's also a notification to allow sharing the image somewhere else or deleting it.
@@ -593,32 +572,42 @@ class ReaderPresenter(
fun saveImage(page: ReaderPage) {
if (page.status != Page.READY) return
val manga = manga ?: return
val context = Injekt.get<Application>()
val context = Injekt.get<Application>()
val notifier = SaveImageNotifier(context)
notifier.onClear()
// Generate filename
val chapter = page.chapter.chapter
val filenameSuffix = " - ${page.number}"
val filename = DiskUtil.buildValidFilename(
"${manga.title} - ${chapter.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize())
) + filenameSuffix
// Pictures directory.
val baseDir = getPicturesDir(context).absolutePath
val destDir = if (preferences.folderPerManga()) {
File(baseDir + File.separator + DiskUtil.buildValidFilename(manga.title))
} else {
File(baseDir)
}
val relativePath = if (preferences.folderPerManga()) DiskUtil.buildValidFilename(manga.title) else ""
// Copy file in background.
Observable.fromCallable { saveImage(page, destDir, manga) }
.doOnNext { file ->
DiskUtil.scanMedia(context, file)
notifier.onComplete(file)
try {
presenterScope.launchIO {
val uri = imageSaver.save(
image = Image.Page(
inputStream = page.stream!!,
name = filename,
location = Location.Pictures.create(relativePath)
)
)
launchUI {
DiskUtil.scanMedia(context, uri)
notifier.onComplete(uri)
view!!.onSaveImageResult(SaveImageResult.Success(uri))
}
}
.doOnError { notifier.onError(it.message) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
)
} catch (e: Throwable) {
notifier.onError(e.message)
view!!.onSaveImageResult(SaveImageResult.Error(e))
}
}
/**
@@ -631,18 +620,27 @@ class ReaderPresenter(
fun shareImage(page: ReaderPage) {
if (page.status != Page.READY) return
val manga = manga ?: return
val context = Injekt.get<Application>()
val destDir = context.cacheImageDir
val destDir = getTempShareDir(context)
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
.map { saveImage(page, destDir, manga) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, file -> view.onShareImageResult(file, page) },
{ _, _ -> /* Empty */ }
)
try {
presenterScope.launchIO {
destDir.deleteRecursively()
val uri = imageSaver.save(
image = Image.Page(
inputStream = page.stream!!,
name = manga.title,
location = Location.Cache
)
)
launchUI {
view!!.onShareImageResult(uri, page)
}
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
}
}
/**
@@ -691,7 +689,7 @@ class ReaderPresenter(
* Results of the save image feature.
*/
sealed class SaveImageResult {
class Success(val file: File) : SaveImageResult()
class Success(val uri: Uri) : SaveImageResult()
class Error(val error: Throwable) : SaveImageResult()
}
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import androidx.core.app.NotificationCompat
import coil.imageLoader
import coil.request.CachePolicy
@@ -13,7 +14,6 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import java.io.File
/**
* Class used to show BigPictureStyle notifications
@@ -36,14 +36,14 @@ class SaveImageNotifier(private val context: Context) {
*
* @param file image file containing downloaded page image.
*/
fun onComplete(file: File) {
fun onComplete(uri: Uri) {
val request = ImageRequest.Builder(context)
.data(file)
.data(uri)
.memoryCachePolicy(CachePolicy.DISABLED)
.size(720, 1280)
.target(
onSuccess = { result ->
showCompleteNotification(file, (result as BitmapDrawable).bitmap)
showCompleteNotification(uri, (result as BitmapDrawable).bitmap)
},
onError = {
onError(null)
@@ -53,7 +53,7 @@ class SaveImageNotifier(private val context: Context) {
context.imageLoader.enqueue(request)
}
private fun showCompleteNotification(file: File, image: Bitmap) {
private fun showCompleteNotification(uri: Uri, image: Bitmap) {
with(notificationBuilder) {
setContentTitle(context.getString(R.string.picture_saved))
setSmallIcon(R.drawable.ic_photo_24dp)
@@ -64,18 +64,18 @@ class SaveImageNotifier(private val context: Context) {
// Clear old actions if they exist
clearActions()
setContentIntent(NotificationHandler.openImagePendingActivity(context, file))
setContentIntent(NotificationHandler.openImagePendingActivity(context, uri))
// Share action
addAction(
R.drawable.ic_share_24dp,
context.getString(R.string.action_share),
NotificationReceiver.shareImagePendingBroadcast(context, file.absolutePath, notificationId)
NotificationReceiver.shareImagePendingBroadcast(context, uri.path!!, notificationId)
)
// Delete action
addAction(
R.drawable.ic_delete_24dp,
context.getString(R.string.action_delete),
NotificationReceiver.deleteImagePendingBroadcast(context, file.absolutePath, notificationId)
NotificationReceiver.deleteImagePendingBroadcast(context, uri.path!!, notificationId)
)
updateNotification()