widgets: move size fixing into a transform

This commit is contained in:
Alexander Capehart 2024-08-07 21:23:31 -06:00
parent a33bbd9cec
commit f19cae0a59
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 70 additions and 44 deletions

View file

@ -107,7 +107,10 @@ class RoundedRectTransformation(
}
private fun calculateOutputSize(input: Bitmap, size: Size): Pair<Int, Int> {
// 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,

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -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(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())
}
return builder.size(Size.ORIGINAL).transformations(transformations)
}
override fun onCompleted(bitmap: Bitmap?) {

View file

@ -21,18 +21,15 @@ package org.oxycblt.auxio.widgets
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.res.Resources
import android.os.Build
import android.util.SizeF
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
import kotlin.math.min
/**
* Create a [RemoteViews] instance with the specified layout and an automatic click handler to open
@ -48,32 +45,6 @@ fun newRemoteViews(context: Context, @LayoutRes layoutRes: Int): RemoteViews {
return views
}
const val MINIMUM_OBSERVED_MAX_SAFE_BITMAP_SIZE = (6912000 * 0.95f).toInt() // 95% slack
/**
* 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(reduce: Float = 2f): Int {
val metrics = Resources.getSystem().displayMetrics
val sw = metrics.widthPixels
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 have to also end up just capping it.
val maxWidgetBitmapMemory = min(6 * sw * sh, MINIMUM_OBSERVED_MAX_SAFE_BITMAP_SIZE)
val maxBitmapArea = (maxWidgetBitmapMemory / 4) / reduce
val maxBitmapSize = sqrt(maxBitmapArea).toInt()
return maxBitmapSize;
}
/**
* Set the background resource of a [RemoteViews] View.
*