mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	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:
		| @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.saver.ImageSaver | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| @@ -46,6 +47,8 @@ class AppModule(val app: Application) : InjektModule { | ||||
|  | ||||
|         addSingletonFactory { DelayedTrackingStore(app) } | ||||
|  | ||||
|         addSingletonFactory { ImageSaver(app) } | ||||
|  | ||||
|         // Asynchronously init expensive components for a faster cold start | ||||
|         ContextCompat.getMainExecutor(app).execute { | ||||
|             get<PreferencesHelper>() | ||||
|   | ||||
| @@ -6,8 +6,6 @@ import android.content.Intent | ||||
| import android.net.Uri | ||||
| import eu.kanade.tachiyomi.extension.util.ExtensionInstaller | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import java.io.File | ||||
|  | ||||
| /** | ||||
|  * Class that manages [PendingIntent] of activity's | ||||
| @@ -32,9 +30,8 @@ object NotificationHandler { | ||||
|      * @param context context of application | ||||
|      * @param file file containing image | ||||
|      */ | ||||
|     internal fun openImagePendingActivity(context: Context, file: File): PendingIntent { | ||||
|     internal fun openImagePendingActivity(context: Context, uri: Uri): PendingIntent { | ||||
|         val intent = Intent(Intent.ACTION_VIEW).apply { | ||||
|             val uri = file.getUriCompat(context) | ||||
|             setDataAndType(uri, "image/*") | ||||
|             flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||||
|         } | ||||
|   | ||||
							
								
								
									
										143
									
								
								app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| package eu.kanade.tachiyomi.data.saver | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.ContentValues | ||||
| import android.content.Context | ||||
| import android.graphics.Bitmap | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.os.Environment | ||||
| import android.provider.MediaStore | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.cacheImageDir | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import okio.IOException | ||||
| import java.io.ByteArrayInputStream | ||||
| import java.io.ByteArrayOutputStream | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
|  | ||||
| class ImageSaver( | ||||
|     val context: Context | ||||
| ) { | ||||
|  | ||||
|     @SuppressLint("InlinedApi") | ||||
|     suspend fun save(image: Image): Uri { | ||||
|         val data = image.data | ||||
|  | ||||
|         val type = ImageUtil.findImageType(data) ?: throw Exception("Not an image") | ||||
|         val filename = DiskUtil.buildValidFilename("${image.name}.$type") | ||||
|  | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { | ||||
|             return save(data(), image.location.directory(context), filename) | ||||
|         } | ||||
|  | ||||
|         if (image.location !is Location.Pictures) { | ||||
|             return save(data(), image.location.directory(context), filename) | ||||
|         } | ||||
|  | ||||
|         val pictureDir = | ||||
|             MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) | ||||
|  | ||||
|         val contentValues = ContentValues().apply { | ||||
|             put(MediaStore.Images.Media.DISPLAY_NAME, image.name) | ||||
|             put( | ||||
|                 MediaStore.Images.Media.RELATIVE_PATH, | ||||
|                 "${Environment.DIRECTORY_PICTURES}/${context.getString(R.string.app_name)}/" + | ||||
|                     (image.location as Location.Pictures).relativePath | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         val picture = context.contentResolver.insert( | ||||
|             pictureDir, | ||||
|             contentValues | ||||
|         ) ?: throw IOException("Couldn't create file") | ||||
|  | ||||
|         data().use { input -> | ||||
|             @Suppress("BlockingMethodInNonBlockingContext") | ||||
|             context.contentResolver.openOutputStream(picture, "w").use { output -> | ||||
|                 input.copyTo(output!!) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return picture | ||||
|     } | ||||
|  | ||||
|     private fun save(inputStream: InputStream, directory: File, filename: String): Uri { | ||||
|         directory.mkdirs() | ||||
|  | ||||
|         val destFile = File(directory, filename) | ||||
|  | ||||
|         inputStream.use { input -> | ||||
|             destFile.outputStream().use { output -> | ||||
|                 input.copyTo(output) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return destFile.getUriCompat(context) | ||||
|     } | ||||
| } | ||||
|  | ||||
| sealed class Image( | ||||
|     open val name: String, | ||||
|     open val location: Location | ||||
| ) { | ||||
|     data class Cover( | ||||
|         val bitmap: Bitmap, | ||||
|         override val name: String, | ||||
|         override val location: Location | ||||
|     ) : Image(name, location) | ||||
|  | ||||
|     data class Page( | ||||
|         val inputStream: () -> InputStream, | ||||
|         override val name: String, | ||||
|         override val location: Location | ||||
|     ) : Image(name, location) | ||||
|  | ||||
|     val data: () -> InputStream | ||||
|         get() { | ||||
|             return when (this) { | ||||
|                 is Cover -> { | ||||
|                     { | ||||
|                         val baos = ByteArrayOutputStream() | ||||
|                         bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos) | ||||
|                         ByteArrayInputStream(baos.toByteArray()) | ||||
|                     } | ||||
|                 } | ||||
|                 is Page -> inputStream | ||||
|             } | ||||
|         } | ||||
| } | ||||
|  | ||||
| sealed class Location { | ||||
|     data class Pictures private constructor(val relativePath: String) : Location() { | ||||
|         companion object { | ||||
|             fun create(relativePath: String = ""): Pictures { | ||||
|                 return Pictures(relativePath) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     object Cache : Location() | ||||
|  | ||||
|     fun directory(context: Context): File { | ||||
|         return when (this) { | ||||
|             Cache -> context.cacheImageDir | ||||
|             is Pictures -> { | ||||
|                 val file = File( | ||||
|                     Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), | ||||
|                     context.getString(R.string.app_name) | ||||
|                 ) | ||||
|                 if (relativePath.isNotEmpty()) { | ||||
|                     return File( | ||||
|                         file, | ||||
|                         relativePath | ||||
|                     ) | ||||
|                 } | ||||
|                 file | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -44,6 +44,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.saver.Image | ||||
| import eu.kanade.tachiyomi.data.saver.Location | ||||
| import eu.kanade.tachiyomi.data.track.EnhancedTrackService | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| @@ -85,7 +87,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewActivity | ||||
| import eu.kanade.tachiyomi.util.chapter.NoChaptersException | ||||
| import eu.kanade.tachiyomi.util.hasCustomCover | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.lang.launchUI | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import eu.kanade.tachiyomi.util.system.toShareIntent | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| @@ -775,26 +777,47 @@ class MangaController : | ||||
|  | ||||
|     fun shareCover() { | ||||
|         try { | ||||
|             val manga = manga!! | ||||
|             val activity = activity!! | ||||
|             useCoverAsBitmap(activity) { coverBitmap -> | ||||
|                 val cover = presenter.shareCover(activity, coverBitmap) | ||||
|                 val uri = cover.getUriCompat(activity) | ||||
|                 startActivity(uri.toShareIntent(activity)) | ||||
|                 viewScope.launchIO { | ||||
|                     val uri = presenter.saveImage( | ||||
|                         image = Image.Cover( | ||||
|                             bitmap = coverBitmap, | ||||
|                             name = manga.title, | ||||
|                             location = Location.Cache | ||||
|                         ) | ||||
|                     ) | ||||
|                     launchUI { | ||||
|                         startActivity(uri.toShareIntent(activity)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|         } catch (e: Throwable) { | ||||
|             logcat(LogPriority.ERROR, e) | ||||
|             activity?.toast(R.string.error_sharing_cover) | ||||
|             activity?.toast(R.string.error_saving_cover) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun saveCover() { | ||||
|         try { | ||||
|             val manga = manga!! | ||||
|             val activity = activity!! | ||||
|             useCoverAsBitmap(activity) { coverBitmap -> | ||||
|                 presenter.saveCover(activity, coverBitmap) | ||||
|                 activity.toast(R.string.cover_saved) | ||||
|                 viewScope.launchIO { | ||||
|                     presenter.saveImage( | ||||
|                         image = Image.Cover( | ||||
|                             bitmap = coverBitmap, | ||||
|                             name = manga.title, | ||||
|                             location = Location.Pictures.create() | ||||
|                         ) | ||||
|                     ) | ||||
|                     launchUI { | ||||
|                         activity.toast(R.string.cover_saved) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|         } catch (e: Throwable) { | ||||
|             logcat(LogPriority.ERROR, e) | ||||
|             activity?.toast(R.string.error_saving_cover) | ||||
|         } | ||||
|   | ||||
| @@ -19,6 +19,8 @@ import eu.kanade.tachiyomi.data.database.models.toMangaInfo | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| 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.track.EnhancedTrackService | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| @@ -39,10 +41,6 @@ import eu.kanade.tachiyomi.util.lang.withUIContext | ||||
| import eu.kanade.tachiyomi.util.prepUpdateCover | ||||
| import eu.kanade.tachiyomi.util.removeCovers | ||||
| import eu.kanade.tachiyomi.util.shouldDownloadNewChapters | ||||
| 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.system.logcat | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.util.updateCoverLastModified | ||||
| @@ -58,7 +56,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 | ||||
|  | ||||
| class MangaPresenter( | ||||
| @@ -110,6 +108,8 @@ class MangaPresenter( | ||||
|  | ||||
|     private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } | ||||
|  | ||||
|     private val imageSaver: ImageSaver by injectLazy() | ||||
|  | ||||
|     private var trackSubscription: Subscription? = null | ||||
|     private var searchTrackerJob: Job? = null | ||||
|     private var refreshTrackersJob: Job? = null | ||||
| @@ -338,44 +338,13 @@ class MangaPresenter( | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Save manga cover Bitmap to temporary share directory. | ||||
|      * Save manga cover Bitmap to picture or temporary share directory. | ||||
|      * | ||||
|      * @param context for the temporary share directory | ||||
|      * @param coverBitmap the cover to save (as Bitmap) | ||||
|      * @return cover File in temporary share directory | ||||
|      * @param image the image with specified location | ||||
|      * @return flow Flow which emits the Uri which specifies where the image is saved when | ||||
|      */ | ||||
|     fun shareCover(context: Context, coverBitmap: Bitmap): File { | ||||
|         return saveCover(getTempShareDir(context), coverBitmap) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Save manga cover to pictures directory of the device. | ||||
|      * | ||||
|      * @param context for the pictures directory of the user | ||||
|      * @param coverBitmap the cover to save (as Bitmap) | ||||
|      * @return cover File in pictures directory | ||||
|      */ | ||||
|     fun saveCover(context: Context, coverBitmap: Bitmap) { | ||||
|         saveCover(getPicturesDir(context), coverBitmap) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Save a manga cover Bitmap to a new File in a given directory. | ||||
|      * Overwrites file if it already exists. | ||||
|      * | ||||
|      * @param directory The directory in which the new file will be created | ||||
|      * @param coverBitmap The manga cover to save | ||||
|      * @return the newly created File | ||||
|      */ | ||||
|     private fun saveCover(directory: File, coverBitmap: Bitmap): File { | ||||
|         directory.mkdirs() | ||||
|         val filename = DiskUtil.buildValidFilename("${manga.title}.${ImageUtil.ImageType.PNG}") | ||||
|  | ||||
|         val destFile = File(directory, filename) | ||||
|         destFile.outputStream().use { desFileOutputStream -> | ||||
|             coverBitmap.compress(Bitmap.CompressFormat.PNG, 100, desFileOutputStream) | ||||
|         } | ||||
|         return destFile | ||||
|     suspend fun saveImage(image: Image): Uri { | ||||
|         return imageSaver.save(image) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -3,20 +3,13 @@ package eu.kanade.tachiyomi.util.storage | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.os.Environment | ||||
| import androidx.core.content.FileProvider | ||||
| import androidx.core.net.toUri | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.R | ||||
| import java.io.File | ||||
|  | ||||
| fun getTempShareDir(context: Context) = File(context.cacheDir, "shared_image") | ||||
|  | ||||
| fun getPicturesDir(context: Context) = File( | ||||
|     Environment.getExternalStorageDirectory().absolutePath + | ||||
|         File.separator + Environment.DIRECTORY_PICTURES + | ||||
|         File.separator + context.getString(R.string.app_name) | ||||
| ) | ||||
| val Context.cacheImageDir: File | ||||
|     get() = File(cacheDir, "shared_image") | ||||
|  | ||||
| /** | ||||
|  * Returns the uri of a file | ||||
|   | ||||
| @@ -6,10 +6,11 @@ import android.content.Intent | ||||
| import android.net.Uri | ||||
| import eu.kanade.tachiyomi.R | ||||
|  | ||||
| fun Uri.toShareIntent(context: Context, type: String = "image/*"): Intent { | ||||
| fun Uri.toShareIntent(context: Context, type: String = "image/*", message: String? = null): Intent { | ||||
|     val uri = this | ||||
|  | ||||
|     val shareIntent = Intent(Intent.ACTION_SEND).apply { | ||||
|         if (message != null) putExtra(Intent.EXTRA_TEXT, message) | ||||
|         putExtra(Intent.EXTRA_STREAM, uri) | ||||
|         clipData = ClipData.newRawUri(null, uri) | ||||
|         setType(type) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user