diff --git a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt index bc1be300a434..2e532c00ac88 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt @@ -8,11 +8,17 @@ package com.nextcloud.client.jobs.gallery import android.graphics.Bitmap +import android.graphics.Point +import android.media.ThumbnailUtils +import android.os.Build +import android.provider.MediaStore +import android.util.Size +import android.view.WindowManager import android.widget.ImageView import androidx.core.content.ContextCompat import com.nextcloud.client.account.User -import com.nextcloud.utils.allocationKilobyte import com.nextcloud.utils.extensions.isPNG +import com.nextcloud.utils.extensions.toFile import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager @@ -20,6 +26,7 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -29,15 +36,12 @@ import kotlinx.coroutines.withContext import java.util.Collections import java.util.WeakHashMap +@Suppress("DEPRECATION", "TooGenericExceptionCaught", "ReturnCount") class GalleryImageGenerationJob(private val user: User, private val storageManager: FileDataStorageManager) { + companion object { private const val TAG = "GalleryImageGenerationJob" - private val semaphore = Semaphore( - maxOf( - 3, - Runtime.getRuntime().availableProcessors() / 2 - ) - ) + private val semaphore = Semaphore(maxOf(3, Runtime.getRuntime().availableProcessors() / 2)) private val activeJobs = Collections.synchronizedMap(WeakHashMap()) fun cancelAllActiveJobs() { @@ -83,56 +87,113 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag } } - @Suppress("TooGenericExceptionCaught") suspend fun run(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { try { - var newImage = false - if (file.remoteId == null && !file.isPreviewAvailable) { Log_OC.e(TAG, "file has no remoteId and no preview") - withContext(Dispatchers.Main) { - listener.onError() - } + withContext(Dispatchers.Main) { listener.onError() } return } - val bitmap: Bitmap? = getBitmap(file, onThumbnailGeneration = { - newImage = true - }) + var newImage = false + val bitmap: Bitmap? = getBitmap(file, onNewThumbnail = { newImage = true }) if (bitmap == null) { - withContext(Dispatchers.Main) { - listener.onError() - } + withContext(Dispatchers.Main) { listener.onError() } return } setThumbnail(bitmap, file, imageView, newImage, listener) } catch (_: Exception) { - withContext(Dispatchers.Main) { - listener.onError() - } + withContext(Dispatchers.Main) { listener.onError() } } } - private suspend fun getBitmap(file: OCFile, onThumbnailGeneration: () -> Unit): Bitmap? = - withContext(Dispatchers.IO) { - val key = file.remoteId - val cachedThumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId - ) - if (cachedThumbnail != null && !file.isUpdateThumbnailNeeded) { - Log_OC.d(TAG, "cached thumbnail is used for: ${file.fileName}") - return@withContext getThumbnailFromCache(file, cachedThumbnail, key) - } + private suspend fun getBitmap(file: OCFile, onNewThumbnail: () -> Unit): Bitmap? = withContext(Dispatchers.IO) { + val cacheKey = ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId - Log_OC.d(TAG, "generating new thumbnail for: ${file.fileName}") + val cached = ThumbnailsCacheManager.getBitmapFromDiskCache(cacheKey) + if (cached != null && !file.isUpdateThumbnailNeeded) { + return@withContext applyVideoOverlayIfNeeded(file, cached) + } - onThumbnailGeneration() - semaphore.withPermit { - return@withContext getThumbnailFromServerAndAddToCache(file, cachedThumbnail) - } + onNewThumbnail() + + val local = decodeLocalThumbnail(file) + if (local != null) { + ThumbnailsCacheManager.addBitmapToCache(cacheKey, local) + return@withContext applyVideoOverlayIfNeeded(file, local) + } + + val remote = semaphore.withPermit { fetchFromServer(file) } + if (remote != null) { + return@withContext applyVideoOverlayIfNeeded(file, remote) + } + + null + } + + private fun decodeLocalThumbnail(file: OCFile): Bitmap? = if (MimeTypeUtil.isVideo(file)) { + createVideoThumbnail(file.storagePath) + } else { + createImageThumbnail(file) + } + + private fun createImageThumbnail(file: OCFile): Bitmap? { + val wm = MainApp.getAppContext().getSystemService(android.content.Context.WINDOW_SERVICE) as WindowManager + val p = Point() + wm.defaultDisplay.getSize(p) + + val pxW = p.x + val pxH = p.y + + val cacheKey = ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId + + var bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.storagePath, pxW, pxH) ?: return null + + if (file.isPNG()) { + bitmap = ThumbnailsCacheManager.handlePNG(bitmap, pxW, pxH) + } + + val thumbnail = ThumbnailsCacheManager.addThumbnailToCache(cacheKey, bitmap, file.storagePath, pxW, pxH) + file.isUpdateThumbnailNeeded = false + + return thumbnail + } + + private fun createVideoThumbnail(storagePath: String): Bitmap? { + val ioFile = storagePath.toFile() ?: return null + val size = ThumbnailsCacheManager.getThumbnailDimension() + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + ThumbnailUtils.createVideoThumbnail(ioFile, Size(size, size), null) + } catch (e: Exception) { + Log_OC.e(TAG, "Failed to create video thumbnail from local file: ${e.message}") + null + } + } else { + @Suppress("DEPRECATION") + ThumbnailUtils.createVideoThumbnail(storagePath, MediaStore.Images.Thumbnails.MINI_KIND) } + } + + private suspend fun fetchFromServer(file: OCFile): Bitmap? = try { + val client = withContext(Dispatchers.IO) { + OwnCloudClientManagerFactory.getDefaultSingleton() + .getClientFor(user.toOwnCloudAccount(), MainApp.getAppContext()) + } + ThumbnailsCacheManager.setClient(client) + ThumbnailsCacheManager.doResizedImageInBackground(file, storageManager) + } catch (t: Throwable) { + Log_OC.e(TAG, "Server fetch failed for $file", t) + null + } + + private fun applyVideoOverlayIfNeeded(file: OCFile, bitmap: Bitmap): Bitmap = if (MimeTypeUtil.isVideo(file)) { + ThumbnailsCacheManager.addVideoOverlay(bitmap, MainApp.getAppContext()) + } else { + bitmap + } private suspend fun setThumbnail( bitmap: Bitmap, @@ -146,16 +207,11 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag if (imageView.tag.toString() == tagId) { if (file.isPNG()) { imageView.setBackgroundColor( - ContextCompat.getColor( - MainApp.getAppContext(), - R.color.bg_default - ) + ContextCompat.getColor(MainApp.getAppContext(), R.color.bg_default) ) } - if (newImage) { - listener.onNewGalleryImage() - } + if (newImage) listener.onNewGalleryImage() if (imageView.isAttachedToWindow) { imageView.setImageBitmap(bitmap) @@ -165,40 +221,4 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag listener.onSuccess() } - - private fun getThumbnailFromCache(file: OCFile, thumbnail: Bitmap, key: String): Bitmap { - var result = thumbnail - if (MimeTypeUtil.isVideo(file)) { - result = ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()) - } - - if (thumbnail.allocationKilobyte() > ThumbnailsCacheManager.THUMBNAIL_SIZE_IN_KB) { - result = ThumbnailsCacheManager.getScaledThumbnailAfterSave(result, key) - } - - return result - } - - @Suppress("DEPRECATION", "TooGenericExceptionCaught") - private suspend fun getThumbnailFromServerAndAddToCache(file: OCFile, thumbnail: Bitmap?): Bitmap? { - var thumbnail = thumbnail - try { - val client = withContext(Dispatchers.IO) { - OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor( - user.toOwnCloudAccount(), - MainApp.getAppContext() - ) - } - ThumbnailsCacheManager.setClient(client) - thumbnail = ThumbnailsCacheManager.doResizedImageInBackground(file, storageManager) - - if (MimeTypeUtil.isVideo(file) && thumbnail != null) { - thumbnail = ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()) - } - } catch (t: Throwable) { - Log_OC.e(TAG, "Generation of gallery image for $file failed", t) - } - - return thumbnail - } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index 72cfadc35f03..3586f6adeba5 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -187,6 +187,7 @@ private static Point getScreenDimension() { /** * Add thumbnail to cache + * * @param imageKey: thumb key * @param bitmap: image for extracting thumbnail * @param path: image path @@ -194,7 +195,7 @@ private static Point getScreenDimension() { * @param pxH: thumbnail height in pixel * @return Bitmap */ - private static Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int pxW, int pxH){ + public static Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int pxW, int pxH) { Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH); @@ -1092,10 +1093,12 @@ public static Bitmap addVideoOverlay(Bitmap thumbnail, Context context) { c.drawBitmap(thumbnail, 0, 0, null); + float left = (thumbnail.getWidth() - px) / 2f; + float top = (thumbnail.getHeight() - px) / 2f; + Paint p = new Paint(); p.setAlpha(230); - - c.drawBitmap(resizedPlayButton, px, px, p); + c.drawBitmap(resizedPlayButton, left, top, p); return resultBitmap; } @@ -1140,7 +1143,7 @@ public AsyncMediaThumbnailDrawable(Resources res, Bitmap bitmap) { /** * adapted from ... */ - private static Bitmap handlePNG(Bitmap source, int newWidth, int newHeight) { + public static Bitmap handlePNG(Bitmap source, int newWidth, int newHeight) { Bitmap softwareBitmap = source.copy(Bitmap.Config.ARGB_8888, false); int sourceWidth = source.getWidth(); @@ -1296,7 +1299,7 @@ public static Bitmap doResizedImageInBackground(OCFile file, FileDataStorageMana Log_OC.d(TAG, "resized image generated"); } } else { - Log_OC.e(TAG, "cannot generate thumbnail not supported file type, status: " + status); + Log_OC.e(TAG, "cannot generate thumbnail not supported file type, status: " + status + " file: " + file.getRemotePath()); mClient.exhaustResponse(getMethod.getResponseBodyAsStream()); } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt index 72e619f869b0..3d1966239d0f 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -36,6 +36,7 @@ import com.owncloud.android.ui.fragment.SearchType import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.CoroutineScope @@ -113,11 +114,15 @@ class OCFileListDelegate( imageView.tag = file.fileId // set placeholder before async job - val cached = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId - ) - if (cached != null) { - imageView.setImageBitmap(cached) + val cacheKey = ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId + val cachedBitmap = ThumbnailsCacheManager.getBitmapFromDiskCache(cacheKey) + if (cachedBitmap != null) { + val overlay = if (MimeTypeUtil.isVideo(file)) { + ThumbnailsCacheManager.addVideoOverlay(cachedBitmap, context) + } else { + cachedBitmap + } + imageView.setImageBitmap(overlay) } else { imageView.setImageDrawable(OCFileUtils.getMediaPlaceholder(file, imageDimension)) }