Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@
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
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
Expand All @@ -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<ImageView, Job>())

fun cancelAllActiveJobs() {
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,15 @@ private static Point getScreenDimension() {

/**
* Add thumbnail to cache
*
* @param imageKey: thumb key
* @param bitmap: image for extracting thumbnail
* @param path: image path
* @param pxW: thumbnail width in pixel
* @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);

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -1140,7 +1143,7 @@ public AsyncMediaThumbnailDrawable(Resources res, Bitmap bitmap) {
/**
* adapted from <a href="https://stackoverflow.com/a/8113368">...</a>
*/
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();
Expand Down Expand Up @@ -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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
Expand Down
Loading