Skip to content
Closed
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 @@ -9,16 +9,13 @@ 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.LocalThumbnailGenerator
import com.nextcloud.utils.extensions.isPNG
import com.nextcloud.utils.extensions.toFile
import com.nextcloud.utils.extensions.setMediaThumbnail
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.datamodel.FileDataStorageManager
Expand Down Expand Up @@ -117,26 +114,38 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag
return@withContext applyVideoOverlayIfNeeded(file, cached)
}

onNewThumbnail()

if (file.isDown) {
val local = decodeLocalThumbnail(file)
if (local != null) {
ThumbnailsCacheManager.addBitmapToCache(cacheKey, local)
onNewThumbnail()
return@withContext applyVideoOverlayIfNeeded(file, local)
}
}

val remote = semaphore.withPermit { fetchFromServer(file) }
if (remote != null) {
onNewThumbnail()
return@withContext applyVideoOverlayIfNeeded(file, remote)
}

val sharedThumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
)
if (sharedThumbnail != null) {
return@withContext applyVideoOverlayIfNeeded(file, sharedThumbnail)
}

null
}

private fun decodeLocalThumbnail(file: OCFile): Bitmap? = if (MimeTypeUtil.isVideo(file)) {
createVideoThumbnail(file.storagePath)
LocalThumbnailGenerator.createThumbnail(
file.storagePath,
ThumbnailsCacheManager.getThumbnailDimension(),
ThumbnailsCacheManager.getThumbnailDimension(),
file.mimeType
)
} else {
createImageThumbnail(file)
}
Expand All @@ -163,22 +172,6 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag
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()
Expand Down Expand Up @@ -216,7 +209,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag
if (newImage) listener.onNewGalleryImage()

if (imageView.isAttachedToWindow) {
imageView.setImageBitmap(bitmap)
imageView.setMediaThumbnail(bitmap)
imageView.invalidate()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import java.io.File
import java.util.concurrent.ConcurrentHashMap
import kotlin.random.Random

Expand Down Expand Up @@ -396,22 +395,18 @@ class FileUploadWorker(
var result: RemoteOperationResult<Any?>

try {
val storageManager = operation.storageManager
fileUploadEventBroadcaster.sendUploadStarted(operation, context)
result = operation.execute(client)

// only generate a thumbnail if the upload actually succeeded
if (result.isSuccess) {
val task = ThumbnailsCacheManager.ThumbnailGenerationTask(storageManager, user)
val file = File(operation.originalStoragePath)
val remoteId: String? = operation.file.remoteId
task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, remoteId))
}
} catch (e: Exception) {
Log_OC.e(TAG, "Error uploading", e)
result = RemoteOperationResult(e)
}

// only generate a thumbnail if the upload actually succeeded
if (result.isSuccess) {
generateThumbnailFromSourceFile(operation, user)
}

if (!isStopped) {
UploadErrorNotificationManager.handleResult(
context,
Expand All @@ -436,6 +431,12 @@ class FileUploadWorker(
return@withContext result
}

private fun generateThumbnailFromSourceFile(operation: UploadFileOperation, user: User) {
val sourceFile = operation.originalStoragePath.toFile() ?: return
val task = ThumbnailsCacheManager.ThumbnailGenerationTask(operation.storageManager, user)
task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(sourceFile, operation.file.remoteId))
}

@Suppress("MagicNumber")
private val minProgressUpdateInterval = 750
private var lastUpdateTime = 0L
Expand Down
82 changes: 82 additions & 0 deletions app/src/main/java/com/nextcloud/utils/LocalThumbnailGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils

import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.media.ThumbnailUtils
import android.os.Build
import android.provider.MediaStore
import android.util.Size
import com.nextcloud.utils.extensions.toFile
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.utils.BitmapUtils
import com.owncloud.android.utils.MimeTypeUtil
import java.io.File

@Suppress("TooGenericExceptionCaught")
object LocalThumbnailGenerator {
private const val TAG = "LocalThumbnailGenerator"
private const val LAST_FRAME = -1L

@JvmStatic
@JvmOverloads
fun createThumbnail(path: String?, pxW: Int, pxH: Int, mimeType: String? = null): Bitmap? {
val file = path.toFile() ?: return null

return if (MimeTypeUtil.isVideo(mimeType) || MimeTypeUtil.isVideo(file)) {
createVideoThumbnail(file, pxW, pxH)
} else {
BitmapUtils.decodeSampledBitmapFromFile(file.absolutePath, pxW, pxH)
}
}

private fun createVideoThumbnail(file: File, pxW: Int, pxH: Int): Bitmap? =
createWithThumbnailUtils(file, pxW, pxH) ?: createWithMetadataRetriever(file, pxW, pxH)

private fun createWithThumbnailUtils(file: File, pxW: Int, pxH: Int): Bitmap? = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ThumbnailUtils.createVideoThumbnail(file, Size(pxW, pxH), null)
} else {
@Suppress("DEPRECATION")
ThumbnailUtils.createVideoThumbnail(file.absolutePath, MediaStore.Images.Thumbnails.MINI_KIND)
}
} catch (e: Exception) {
Log_OC.d(TAG, "ThumbnailUtils could not decode ${file.name}: ${e.message}")
null
}

private fun createWithMetadataRetriever(file: File, pxW: Int, pxH: Int): Bitmap? {
val retriever = MediaMetadataRetriever()

return try {
retriever.setDataSource(file.absolutePath)
retriever.getFrameAtTime(LAST_FRAME)?.scaleDown(pxW, pxH)
} catch (e: Exception) {
Log_OC.e(TAG, "MediaMetadataRetriever could not decode ${file.name}", e)
null
} finally {
try {
retriever.release()
} catch (e: Exception) {
Log_OC.w(TAG, "Failed to release MediaMetadataRetriever: ${e.message}")
}
}
}

private fun Bitmap.scaleDown(pxW: Int, pxH: Int): Bitmap {
val longestSide = maxOf(width, height)
val limit = maxOf(pxW, pxH)

return if (longestSide > limit) {
BitmapUtils.scaleBitmap(this, limit.toFloat(), width, height, longestSide)
} else {
this
}
}
}
30 changes: 5 additions & 25 deletions app/src/main/java/com/nextcloud/utils/OCFileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@
package com.nextcloud.utils

import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.toDrawable
import com.nextcloud.utils.extensions.getBitmapSize
import com.nextcloud.utils.extensions.getExifSize
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.utils.BitmapUtils
import com.owncloud.android.utils.MimeTypeUtil
import com.owncloud.android.utils.theme.ViewThemeUtils

@Suppress("TooGenericExceptionCaught", "ReturnCount")
object OCFileUtils {
Expand Down Expand Up @@ -56,27 +53,10 @@ object OCFileUtils {
return fallbackPair
}

fun getMediaPlaceholder(file: OCFile, imageDimension: Pair<Int, Int>): BitmapDrawable {
fun getMediaPlaceholder(file: OCFile, viewThemeUtils: ViewThemeUtils): Drawable {
val context = MainApp.getAppContext()

val drawableId = if (MimeTypeUtil.isImage(file)) {
R.drawable.file_image
} else if (MimeTypeUtil.isVideo(file)) {
R.drawable.file_movie
} else {
R.drawable.file
}

val drawable = ContextCompat.getDrawable(context, drawableId)
?: return Color.GRAY.toDrawable().toBitmap(imageDimension.first, imageDimension.second)
.toDrawable(context.resources)

val bitmap = BitmapUtils.drawableToBitmap(
drawable,
imageDimension.first,
imageDimension.second
)

return bitmap.toDrawable(context.resources)
return MimeTypeUtil.getFileTypeIcon(file.mimeType, file.fileName, context, viewThemeUtils)
?: Color.GRAY.toDrawable()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,31 @@
package com.nextcloud.utils.extensions

import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.GradientDrawable
import android.util.TypedValue
import android.view.ViewOutlineProvider
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import com.nextcloud.utils.OCFileUtils
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.utils.theme.ViewThemeUtils

fun ImageView.setMediaPlaceholder(file: OCFile, viewThemeUtils: ViewThemeUtils) {
scaleType = ImageView.ScaleType.FIT_CENTER
val padding = resources.getDimensionPixelSize(R.dimen.standard_padding)
setPadding(padding, padding, padding, padding)
setImageDrawable(OCFileUtils.getMediaPlaceholder(file, viewThemeUtils))
}

fun ImageView.setMediaThumbnail(bitmap: Bitmap) {
scaleType = ImageView.ScaleType.CENTER_CROP
setPadding(0, 0, 0, 0)
setImageBitmap(bitmap)
}

@JvmOverloads
fun ImageView.makeRoundedWithIcon(
Expand Down
Loading
Loading