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,19 +8,19 @@
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 com.nextcloud.client.account.User
import com.nextcloud.utils.extensions.getBigThumbnailKey
import com.nextcloud.utils.extensions.getSmallThumbnail
import com.nextcloud.utils.extensions.isPNG
import com.nextcloud.utils.extensions.setMediaThumbnail
import com.nextcloud.utils.extensions.toFile
import com.nextcloud.utils.thumbnail.ResizedImageGenerator
import com.nextcloud.utils.view.ScreenMetrics
import com.owncloud.android.MainApp
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
Expand Down Expand Up @@ -142,9 +142,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag
}

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 p = ScreenMetrics.size()

val pxW = p.x
val pxH = p.y
Expand Down Expand Up @@ -185,7 +183,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag
.getClientFor(user.toOwnCloudAccount(), MainApp.getAppContext())
}
ThumbnailsCacheManager.setClient(client)
ThumbnailsCacheManager.doResizedImageInBackground(file, storageManager)
ResizedImageGenerator.getOrCreate(file, client, storageManager)
} catch (t: Throwable) {
Log_OC.e(TAG, "Server fetch failed for $file", t)
null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.thumbnail

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Point
import com.nextcloud.utils.AppConstants
import com.nextcloud.utils.extensions.getBigThumbnailKey
import com.nextcloud.utils.extensions.getPreviewEndpoint
import com.nextcloud.utils.extensions.getVideoPreviewEndpoint
import com.nextcloud.utils.extensions.isPNG
import com.nextcloud.utils.view.ScreenMetrics
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.files.model.ImageDimension
import com.owncloud.android.utils.BitmapUtils
import com.owncloud.android.utils.MimeTypeUtil
import org.apache.commons.httpclient.HttpStatus
import org.apache.commons.httpclient.methods.GetMethod

object ResizedImageGenerator {
private const val TAG = "ResizedImageGenerator"

@Suppress("MagicNumber")
private val VIDEO_PREVIEW_SIZES = listOf(1024, 512, 256, 128, 64)

fun getOrCreate(file: OCFile, client: OwnCloudClient, storageManager: FileDataStorageManager): Bitmap? {
val cacheKey = file.getBigThumbnailKey()
val cachedImage = ThumbnailsCacheManager.getBitmapFromDiskCache(cacheKey)
if (cachedImage != null && !file.isUpdateThumbnailNeeded) {
Log_OC.d(TAG, "cache hit: ${file.remotePath}")
return cachedImage
}

val screenSize = ScreenMetrics.size()
val resizedImage = if (file.isDown && MimeTypeUtil.isImage(file)) {
Log_OC.d(TAG, "creating from local file: ${file.remotePath}")
val decodedImage = BitmapUtils.decodeSampledBitmapFromFile(file.storagePath, screenSize.x, screenSize.y)
if (decodedImage == null) {
Log_OC.w(TAG, "local file decode failed, falling back to cached image: ${file.remotePath}")
cachedImage
} else {
resizeAndCacheLocalImage(file, cacheKey, decodedImage, screenSize)
}
} else {
Log_OC.d(TAG, "creating from server preview: ${file.remotePath}")
downloadAndCacheServerPreview(file, cacheKey, client, screenSize)
}

if (resizedImage != null && file.imageDimension == null) {
saveImageDimension(file, resizedImage, storageManager)
}

return resizedImage
}

private fun resizeAndCacheLocalImage(
file: OCFile,
cacheKey: String,
decodedImage: Bitmap,
screenSize: Point
): Bitmap? {
val image = if (file.isPNG()) {
BitmapUtils.centerCropOnPngBackground(decodedImage, screenSize.x, screenSize.y)
} else {
decodedImage
}
file.isUpdateThumbnailNeeded = false
return ThumbnailsCacheManager.addThumbnailToCache(cacheKey, image, file.storagePath, screenSize.x, screenSize.y)
}

private fun downloadAndCacheServerPreview(
file: OCFile,
cacheKey: String,
client: OwnCloudClient,
screenSize: Point
): Bitmap? {
val preview = if (MimeTypeUtil.isVideo(file)) {
downloadVideoPreview(file, client)
} else {
downloadPreview(file, client, client.getPreviewEndpoint(file.localId, screenSize.x, screenSize.y))
} ?: return null

val image = if (file.isPNG()) {
BitmapUtils.centerCropOnPngBackground(preview, preview.width, preview.height)
} else {
preview
}
ThumbnailsCacheManager.addBitmapToCache(cacheKey, image)
return image
}

private fun downloadVideoPreview(file: OCFile, client: OwnCloudClient): Bitmap? =
VIDEO_PREVIEW_SIZES.firstNotNullOfOrNull { size ->
downloadPreview(file, client, client.getVideoPreviewEndpoint(file.localId, size))
}

@Suppress("TooGenericExceptionCaught")
private fun downloadPreview(file: OCFile, client: OwnCloudClient, uri: String): Bitmap? {
Log_OC.d(TAG, "downloading preview: ${file.remotePath}, uri: $uri")

var getMethod: GetMethod? = null
return try {
getMethod = GetMethod(uri).apply { params.soTimeout = AppConstants.DEFAULT_READ_TIMEOUT_MS }

val status = client.executeMethod(getMethod)
if (status != HttpStatus.SC_OK) {
Log_OC.e(TAG, "preview download failed, status: $status, file: ${file.remotePath}")
client.exhaustResponse(getMethod.responseBodyAsStream)
return null
}

getMethod.responseBodyAsStream.use { BitmapFactory.decodeStream(it) }
} catch (e: Exception) {
Log_OC.e(TAG, "preview download failed: ${file.remotePath}", e)
null
} finally {
getMethod?.releaseConnection()
}
}

private fun saveImageDimension(file: OCFile, image: Bitmap, storageManager: FileDataStorageManager) {
file.imageDimension = ImageDimension(image.width.toFloat(), image.height.toFloat())
file.isUpdateThumbnailNeeded = false
storageManager.saveFile(file)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.thumbnail.job

import android.graphics.Bitmap
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.content.ContextCompat
import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.repository.ClientRepository
import com.nextcloud.utils.thumbnail.ResizedImageGenerator
import com.nextcloud.utils.thumbnail.VideoOverlayGenerator
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.OwnCloudClient
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.preview.media.PreviewMediaFragment
import com.owncloud.android.utils.MimeTypeUtil
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class ResizedImageGenerationJob(
private val fragment: PreviewMediaFragment,
private val storageManager: FileDataStorageManager,
private val connectivityService: ConnectivityService,
private val clientRepository: ClientRepository,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
companion object {
private const val TAG = "ResizedImageGenerationJob"
}

suspend fun load(file: OCFile, imageView: ImageView, progressContainer: FrameLayout) {
val image = getOrCreateDisplayImage(file)
if (image == null) {
Log_OC.e(TAG, "load failed, no resized image: ${file.remotePath}")
showLoadError()
return
}

if (imageView.tag?.toString() != file.fileId.toString()) {
Log_OC.d(TAG, "load skipped, image view now shows another file: ${file.remotePath}")
return
}

imageView.run {
visibility = View.VISIBLE
setImageBitmap(image)
setBackgroundColor(ContextCompat.getColor(context, R.color.background_color_inverse))
}
progressContainer.visibility = View.GONE
Log_OC.d(TAG, "load completed: ${file.remotePath}")
}

private suspend fun getOrCreateDisplayImage(file: OCFile): Bitmap? {
val client = clientRepository.getOwncloudClient()
if (client == null) {
Log_OC.e(TAG, "no client available: ${file.remotePath}")
return null
}
ThumbnailsCacheManager.setClient(client)
return withContext(ioDispatcher) { getOrCreateWithVideoOverlay(file, client) }
}

@Suppress("TooGenericExceptionCaught")
private fun getOrCreateWithVideoOverlay(file: OCFile, client: OwnCloudClient): Bitmap? = try {
val resizedImage = ResizedImageGenerator.getOrCreate(file, client, storageManager)
if (resizedImage != null && MimeTypeUtil.isVideo(file)) {
VideoOverlayGenerator.addOverlay(resizedImage, MainApp.getAppContext())
} else {
resizedImage
}
} catch (t: Throwable) {
Log_OC.e(TAG, "resized image generation crashed: ${file.remotePath}", t)
null
}

private suspend fun showLoadError() {
val isInternetWalled = withContext(ioDispatcher) { connectivityService.isInternetWalled() }
if (isInternetWalled) {
Log_OC.d(TAG, "showing no connection error")
fragment.setNoConnectionErrorMessage()
} else {
Log_OC.d(TAG, "showing unsupported image error")
fragment.handleUnsupportedImage()
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/com/nextcloud/utils/view/ScreenMetrics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@

package com.nextcloud.utils.view

import android.app.Activity
import android.content.Context
import android.graphics.Point
import android.util.DisplayMetrics
import com.owncloud.android.MainApp

object ScreenMetrics {

@JvmStatic
fun size(activity: Activity?): Point = Point().also { size ->
@Suppress("DEPRECATION")
activity?.windowManager?.defaultDisplay?.getSize(size)
@JvmOverloads
fun size(context: Context? = null): Point {
val displayMetrics = (context ?: MainApp.getAppContext()).displayMetrics
return Point(displayMetrics.widthPixels, displayMetrics.heightPixels)
}

@JvmStatic
Expand All @@ -26,6 +27,9 @@ object ScreenMetrics {
@JvmStatic
fun pxToDp(px: Int, context: Context): Float = px * (DisplayMetrics.DENSITY_DEFAULT / context.densityDpi)

private val Context.displayMetrics: DisplayMetrics
get() = resources.displayMetrics

private val Context.densityDpi: Float
get() = resources.displayMetrics.densityDpi.toFloat()
get() = displayMetrics.densityDpi.toFloat()
}
Loading
Loading