Skip to content
Merged
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 @@ -52,6 +52,7 @@ class GeneratePDFUseCase @Inject constructor(private val logger: Logger) {
val page = document.startPage(pageInfo)
page.canvas.drawBitmap(bitmap, 0f, 0f, null)
document.finishPage(page)
bitmap.recycle()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag
var bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.storagePath, pxW, pxH) ?: return null

if (file.isPNG()) {
bitmap = ThumbnailsCacheManager.handlePNG(bitmap, pxW, pxH)
bitmap = BitmapUtils.centerCropOnPngBackground(bitmap, pxW, pxH)
}

val thumbnail = ThumbnailsCacheManager.addThumbnailToCache(cacheKey, bitmap, file.storagePath, pxW, pxH)
Expand Down
25 changes: 13 additions & 12 deletions app/src/main/java/com/nextcloud/utils/BitmapExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,30 @@ private const val TAG = "BitmapExtension"
fun Bitmap.allocationKilobyte(): Int = allocationByteCount.div(1024)

/**
* Recursively scales down the Bitmap until its size allocation is within the specified size.
* Scales down the Bitmap step by step until its size allocation is within the specified size.
*
* This function checks if the current Bitmap's size (in kilobytes) is already within
* the target size. If not, it scales the Bitmap down by a factor of `1.5` in both width and height
* and calls itself recursively until the size condition is met.
* Each step scales the previous result down by a factor of `1.5` in both width and height. Intermediate
* bitmaps are recycled as soon as the next step exists; the receiver is never recycled.
*
* @receiver Bitmap The original Bitmap to be resized.
* @param targetKB The target size in kilobytes (KB) that the Bitmap should be reduced to.
* @return A scaled-down Bitmap that meets the size allocation requirement.
*/
@Suppress("MagicNumber")
fun Bitmap.scaleUntil(targetKB: Int): Bitmap {
if (allocationKilobyte() <= targetKB) {
return this
}

// 1.5 is used to gradually scale down while minimizing distortion
val scaleRatio = 1.5
val width = width.div(scaleRatio).toInt()
val height = height.div(scaleRatio).toInt()
var result = this

while (result.allocationKilobyte() > targetKB) {
val scaled = result.scale(result.width.div(scaleRatio).toInt(), result.height.div(scaleRatio).toInt())
if (result !== this && result !== scaled) {
result.recycle()
}
result = scaled
}

val scaledBitmap = scale(width, height)
return scaledBitmap.scaleUntil(targetKB)
return result
}

/**
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/nextcloud/utils/avatar/AvatarLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,13 @@ class AvatarLoader @Inject constructor(
}

val bitmap = BitmapFactory.decodeStream(get.responseBodyAsStream)
val thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px)
val thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px, ThumbnailUtils.OPTIONS_RECYCLE_INPUT)

if (thumbnail == null || newETag.isNullOrEmpty()) {
return TextDrawable.createAvatar(request.user, request.avatarRadius)
}

val avatar = ThumbnailsCacheManager.handlePNG(thumbnail, px, px)
val avatar = BitmapUtils.centerCropOnPngBackground(thumbnail, px, px)
ThumbnailsCacheManager.addBitmapToCache(request.cacheKey(newETag), avatar)
storeRefreshTimestamp(request)
return toDrawable(request, avatar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

package com.nextcloud.utils.thumbnail

import android.content.ComponentCallbacks2
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
Expand Down Expand Up @@ -52,7 +54,7 @@ class FileThumbnailGenerator @Inject constructor(
private val viewThemeUtils: Provider<ViewThemeUtils>,
private val context: Context,
private val accountManager: UserAccountManager
) {
) : ComponentCallbacks2 {

companion object {
private const val TAG = "FileThumbnailGenerator"
Expand All @@ -70,6 +72,21 @@ class FileThumbnailGenerator @Inject constructor(
private val tasks = CopyOnWriteArrayList<ThumbnailGenerationTask>()
private val placeholders = mutableMapOf<String, Bitmap>()

init {
context.applicationContext.registerComponentCallbacks(this)
}

override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
synchronized(placeholders) { placeholders.clear() }
}
}

override fun onConfigurationChanged(newConfig: Configuration) = Unit

@Deprecated("Deprecated in Java")
override fun onLowMemory() = Unit

fun setThumbnail(file: OCFile, view: ImageView, arguments: ThumbnailArguments) {
if (file.remoteId == null) {
setLocalThumbnail(file, view, arguments)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,34 +70,29 @@ class MediaThumbnailGenerationJob(
return ThumbnailsCacheManager.addThumbnailToCache(key, bitmap, file.path, px, px)
}

private fun generateVideoThumbnail(file: File, key: String): Bitmap? =
extractVideoFrame(file)?.let { frame -> downscaleAndCache(frame, file, key) }

private fun downscaleAndCache(frame: Bitmap, file: File, key: String): Bitmap {
val px = ThumbnailsCacheManager.getThumbnailDimension()
val width = frame.width
val height = frame.height
val longestSide = maxOf(width, height)

if (longestSide <= px) {
return frame
}

val scaled = BitmapUtils.scaleBitmap(frame, px.toFloat(), width, height, longestSide)
return ThumbnailsCacheManager.addThumbnailToCache(key, scaled, file.path, px, px)
}

@Suppress("TooGenericExceptionCaught")
private fun extractVideoFrame(file: File): Bitmap? {
private fun generateVideoThumbnail(file: File, key: String): Bitmap? {
if (!file.exists()) {
Log_OC.w(TAG, "Cannot extract thumbnail: file does not exist")
return null
}

val px = ThumbnailsCacheManager.getThumbnailDimension()
val retriever = MediaMetadataRetriever()
return try {
retriever.setDataSource(file.absolutePath)
retriever.getFrameAtTime(ANY_FRAME_TIME_US)
val isVideoLargerThanThumbnail = BitmapUtils.getLargestVideoDimension(retriever) > px
val frame = if (isVideoLargerThanThumbnail) {
retriever.getScaledFrameAtTime(ANY_FRAME_TIME_US, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, px, px)
} else {
retriever.getFrameAtTime(ANY_FRAME_TIME_US)
}

when {
frame == null -> null
!isVideoLargerThanThumbnail && BitmapUtils.fitsInto(frame, px) -> frame
else -> ThumbnailsCacheManager.addThumbnailToCache(key, frame, file.path, px, px)
}
} catch (_: Throwable) {
Log_OC.w(TAG, "Failed to create bitmap from video ${file.absolutePath}")
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.ThumbnailUtils;
Expand Down Expand Up @@ -68,10 +66,8 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import static com.nextcloud.utils.extensions.ThumbnailsCacheManagerExtensionsKt.getExifOrientation;
Expand Down Expand Up @@ -196,7 +192,7 @@ private static Point getScreenDimension() {
*/
public static Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int pxW, int pxH) {

Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH);
Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);

// Rotate image, obeying exif tag
int orientation = getExifOrientation(path);
Expand Down Expand Up @@ -235,13 +231,8 @@ public static void addBitmapToCache(String key, Bitmap bitmap) {
return;
}

// Check if the bitmap is already cached
Bitmap cachedBitmap = mThumbnailCache.getBitmap(key);
if (cachedBitmap == null) {
cachedBitmap = mThumbnailCache.getScaledBitmap(key, bitmap.getWidth(), bitmap.getHeight());
}

if (cachedBitmap != null && BitmapExtensionsKt.allocationKilobyte(cachedBitmap) <= THUMBNAIL_SIZE_IN_KB) {
Integer cachedSizeInKB = mThumbnailCache.getDecodedSizeInKB(key);
if (cachedSizeInKB != null && cachedSizeInKB <= THUMBNAIL_SIZE_IN_KB) {
Log_OC.d(TAG, "Cached version is already within size limits, no need to scale: " + key);
return;
}
Expand Down Expand Up @@ -622,7 +613,7 @@ private Bitmap doThumbnailFromOCFileInBackground() {

if (bitmap != null) {
if (PNG_MIMETYPE.equalsIgnoreCase(ocFile.getMimeType())) {
bitmap = handlePNG(bitmap, pxW, pxH);
bitmap = BitmapUtils.centerCropOnPngBackground(bitmap, pxW, pxH);
}

thumbnail = addThumbnailToCache(imageKey, bitmap, ocFile.getStoragePath(), pxW, pxH);
Expand All @@ -641,7 +632,7 @@ private Bitmap doThumbnailFromOCFileInBackground() {
}

if (resizedImage != null) {
thumbnail = ThumbnailUtils.extractThumbnail(resizedImage, pxW, pxH);
thumbnail = ThumbnailUtils.extractThumbnail(resizedImage, pxW, pxH, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
Log_OC.d(TAG, "Thumbnail generated from resized image cache for file: " + file.getFileName());
} else {
Log_OC.d(TAG, "No resized image cache available for file: " + file.getFileName());
Expand Down Expand Up @@ -682,7 +673,7 @@ private Bitmap doThumbnailFromOCFileInBackground() {
try (InputStream inputStream = getMethod.getResponseBodyAsStream()) {
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH);
thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
Log_OC.d(TAG, "Thumbnail downloaded and extracted for file: " + file.getFileName());
} else {
Log_OC.w(TAG, "Downloaded thumbnail bitmap is null for file: " + file.getFileName());
Expand All @@ -694,7 +685,7 @@ private Bitmap doThumbnailFromOCFileInBackground() {
}

if (thumbnail != null && PNG_MIMETYPE.equalsIgnoreCase(file.getMimeType())) {
thumbnail = handlePNG(thumbnail, pxW, pxH);
thumbnail = BitmapUtils.centerCropOnPngBackground(thumbnail, pxW, pxH);
Log_OC.d(TAG, "Handled PNG thumbnail for downloaded file: " + file.getFileName());
}
} catch (Exception e) {
Expand Down Expand Up @@ -830,37 +821,6 @@ private ResizedImageGenerationTask getBitmapWorkerTask() {
}
}

/**
* adapted from <a href="https://stackoverflow.com/a/8113368">...</a>
*/
public static Bitmap handlePNG(Bitmap source, int newWidth, int newHeight) {
Bitmap softwareBitmap = source.copy(Bitmap.Config.ARGB_8888, false);

int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();

float xScale = (float) newWidth / sourceWidth;
float yScale = (float) newHeight / sourceHeight;
float scale = Math.max(xScale, yScale);

float scaledWidth = scale * sourceWidth;
float scaledHeight = scale * sourceHeight;

float left = (newWidth - scaledWidth) / 2;
float top = (newHeight - scaledHeight) / 2;

RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);

Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);

Canvas canvas = new Canvas(dest);
int color = ContextCompat.getColor(MainApp.getAppContext(),R.color.background_color_png);
canvas.drawColor(color);
canvas.drawBitmap(softwareBitmap, null, targetRect, null);

return dest;
}

public static void generateResizedImage(OCFile file) {
Point p = getScreenDimension();
int pxW = p.x;
Expand All @@ -872,7 +832,7 @@ public static void generateResizedImage(OCFile file) {
if (bitmap != null) {
// Handle PNG
if (PNG_MIMETYPE.equalsIgnoreCase(file.getMimeType())) {
bitmap = handlePNG(bitmap, pxW, pxH);
bitmap = BitmapUtils.centerCropOnPngBackground(bitmap, pxW, pxH);
}

addThumbnailToCache(imageKey, bitmap, file.getStoragePath(), pxW, pxH);
Expand Down Expand Up @@ -910,7 +870,7 @@ public static void generateThumbnailFromOCFile(OCFile file, User user, Context c
if (status == HttpStatus.SC_OK) {
InputStream inputStream = getMethod.getResponseBodyAsStream();
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH);
thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
} else {
client.exhaustResponse(getMethod.getResponseBodyAsStream());
}
Expand All @@ -919,7 +879,7 @@ public static void generateThumbnailFromOCFile(OCFile file, User user, Context c
if (thumbnail != null) {
// Handle PNG
if (PNG_MIMETYPE.equalsIgnoreCase(file.getMimeType())) {
thumbnail = handlePNG(thumbnail, pxW, pxH);
thumbnail = BitmapUtils.centerCropOnPngBackground(thumbnail, pxW, pxH);
}

Log_OC.d(TAG, "add thumbnail to cache: " + file.getFileName());
Expand Down Expand Up @@ -1013,7 +973,7 @@ public static Bitmap doResizedImageInBackground(OCFile file, FileDataStorageMana
Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.getStoragePath(), pxW, pxH);
if (bitmap != null) {
if (OCFileExtensionsKt.isPNG(file)) {
bitmap = handlePNG(bitmap, pxW, pxH);
bitmap = BitmapUtils.centerCropOnPngBackground(bitmap, pxW, pxH);
}
thumbnail = addThumbnailToCache(imageKey, bitmap, file.getStoragePath(), pxW, pxH);
file.setUpdateThumbnailNeeded(false);
Expand All @@ -1028,7 +988,7 @@ public static Bitmap doResizedImageInBackground(OCFile file, FileDataStorageMana
pxH));

if (thumbnail != null && PNG_MIMETYPE.equalsIgnoreCase(file.getMimeType())) {
thumbnail = handlePNG(thumbnail, thumbnail.getWidth(), thumbnail.getHeight());
thumbnail = BitmapUtils.centerCropOnPngBackground(thumbnail, thumbnail.getWidth(), thumbnail.getHeight());
}

if (thumbnail != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ constructor(
null
}

fun getDecodedSizeInKB(key: String): Int? = try {
readBounds(key.toValidKey())
?.takeIf { it.outWidth > 0 && it.outHeight > 0 }
?.let { it.outWidth * it.outHeight * ARGB_8888_BYTES_PER_PIXEL / BYTES_PER_KB }
} catch (e: IOException) {
Log_OC.e(TAG, e.message, e)
null
}

fun containsKey(key: String): Boolean = try {
diskCache.get(key.toValidKey())?.use { true } == true
} catch (e: IOException) {
Expand Down Expand Up @@ -125,6 +134,8 @@ constructor(
private const val VALUE_COUNT = 1
private const val VALUE_INDEX = 0
private const val IO_BUFFER_SIZE = 8 * 1024
private const val ARGB_8888_BYTES_PER_PIXEL = 4
private const val BYTES_PER_KB = 1024

private val TAG = DiskLruImageCache::class.java.simpleName
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,17 @@ class PreviewImageFragment :
Log_OC.d(TAG, "onStop starts")
loadBitmapTask?.cancel(true)
loadBitmapTask = null
releaseOwnedBitmap()
super.onStop()
}

private fun releaseOwnedBitmap() {
val bitmap = ownedBitmap ?: return
binding.image.setImageDrawable(null)
bitmap.recycle()
ownedBitmap = null
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val menuHost: MenuHost = requireActivity()
addMenuProvider(menuHost)
Expand Down
Loading
Loading