diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt index c8d3ee145..0d32d20de 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt @@ -107,7 +107,10 @@ class RoundedRectTransformation( } private fun calculateOutputSize(input: Bitmap, size: Size): Pair { - // MODIFICATION: Remove short-circuiting for original size and input size + if (size == Size.ORIGINAL) { + // This path only runs w/the widget code, which already normalizes widget sizes + return input.width to input.height + } val multiplier = DecodeUtils.computeSizeMultiplier( srcWidth = input.width, diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt new file mode 100644 index 000000000..ac44e418d --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 Auxio Project + * WidgetBitmapTransformation.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.widgets + +import android.content.res.Resources +import android.graphics.Bitmap +import coil.size.Size +import coil.transform.Transformation +import kotlin.math.sqrt + +class WidgetBitmapTransformation(private val reduce: Float) : Transformation { + private val metrics = Resources.getSystem().displayMetrics + private val sw = metrics.widthPixels + private val sh = metrics.heightPixels + // Cap memory usage at 1.5 times the size of the display + // 1.5 * 4 bytes/pixel * w * h ==> 6 * w * h + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java + // Of course since OEMs randomly patch this check, we give a lot of slack. + private val maxBitmapArea = (1.5 * sw * sh / reduce).toInt() + + override val cacheKey: String + get() = "WidgetBitmapTransformation:${maxBitmapArea}" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + if (size !== Size.ORIGINAL) { + // The widget loading stack basically discards the size parameter since there's no + // sane value from the get-go, all this transform does is actually dynamically apply + // the size cap so this transform must always be zero. + throw IllegalArgumentException("WidgetBitmapTransformation requires original size.") + } + val inputArea = input.width * input.height + if (inputArea != maxBitmapArea) { + val scale = sqrt(maxBitmapArea / inputArea.toDouble()) + val newWidth = (input.width * scale).toInt() + val newHeight = (input.height * scale).toInt() + return Bitmap.createScaledBitmap(input, newWidth, newHeight, true) + } + return input + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 09ac5e8f1..8631845a5 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -22,6 +22,7 @@ import android.content.Context import android.graphics.Bitmap import android.os.Build import coil.request.ImageRequest +import coil.size.Size import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R @@ -96,24 +97,19 @@ constructor( 0 } - return if (cornerRadius > 0) { - // If rounded, reduce the bitmap size further to obtain more pronounced - // rounded corners. - builder.size(getSafeRemoteViewsImageSize(context, 10f)) - val cornersTransformation = - RoundedRectTransformation(cornerRadius.toFloat()) + val transformations = buildList { if (imageSettings.forceSquareCovers) { - builder.transformations( - SquareCropTransformation.INSTANCE, cornersTransformation) + add(SquareCropTransformation.INSTANCE) + } + if (cornerRadius > 0) { + add(WidgetBitmapTransformation(10f)) + add(RoundedRectTransformation(cornerRadius.toFloat())) } else { - builder.transformations(cornersTransformation) + add(WidgetBitmapTransformation(2f)) } - } else { - if (imageSettings.forceSquareCovers) { - builder.transformations(SquareCropTransformation.INSTANCE) - } - builder.size(getSafeRemoteViewsImageSize(context)) } + + return builder.size(Size.ORIGINAL).transformations(transformations) } override fun onCompleted(bitmap: Bitmap?) { diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt index 799aa8a67..953af14c8 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -27,7 +27,6 @@ import android.widget.RemoteViews import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.LayoutRes -import kotlin.math.sqrt import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newMainPendingIntent @@ -46,24 +45,6 @@ fun newRemoteViews(context: Context, @LayoutRes layoutRes: Int): RemoteViews { return views } -/** - * Get an image size guaranteed to not exceed the [RemoteViews] bitmap memory limit, assuming that - * there is only one image. - * - * @param context [Context] required to perform calculation. - * @param reduce Optional multiplier to reduce the image size. Recommended value is 2 to avoid - * device-specific variations in memory limit. - * @return The dimension of a bitmap that can be safely used in [RemoteViews]. - */ -fun getSafeRemoteViewsImageSize(context: Context, reduce: Float = 2f): Int { - val metrics = context.resources.displayMetrics - val sw = metrics.widthPixels - val sh = metrics.heightPixels - // Maximum size is 1/3 total screen area * 4 bytes per pixel. Reverse - // that to obtain the image size. - return sqrt((6f / 4f / reduce) * sw * sh).toInt() -} - /** * Set the background resource of a [RemoteViews] View. *