Compare commits

...

13 commits

Author SHA1 Message Date
Alexander Capehart
396893cd26
widgets: decrease bitmap reduction 2024-08-09 21:27:26 -06:00
Alexander Capehart
7db878dd73
widget: increase bitmap reduction 2024-08-08 19:10:44 -06:00
Alexander Capehart
716d6fff6a
home: temp debug info 2024-08-08 19:10:21 -06:00
Alexander Capehart
f19cae0a59
widgets: move size fixing into a transform 2024-08-07 21:24:27 -06:00
Alexander Capehart
a33bbd9cec
widget: aggressively clamp bitmap size anyway
Just a test
2024-08-07 17:41:49 -06:00
Alexander Capehart
c11e03b451
widget: use system display size for widget bitmap size
May have introduced some variation by accident by using the smaller limit.
2024-08-07 17:38:51 -06:00
Alexander Capehart
3e4b27d76b
widget: more logging 2024-08-06 19:02:24 -06:00
Alexander Capehart
b185914ba1
Revert "widgets: downsize widget images in round mode"
This reverts commit 272f7e4047.
2024-08-02 20:35:08 -06:00
Alexander Capehart
4ee2e81995
Revert "widgets: reduce cover size limit"
This reverts commit 2ecb94c97e.
2024-08-02 20:34:54 -06:00
Alexander Capehart
74e0da526d
Revert "widget: drastically shrink cover size"
This reverts commit 186c5547ba.
2024-08-02 20:34:43 -06:00
Alexander Capehart
90fbbf587b
widget: try mitigating widget bitmap bug further 2024-08-02 19:47:57 -06:00
Alexander Capehart
186c5547ba
widget: drastically shrink cover size
In a desparate attempt to accomodate OEMs that STILL don't have the
extra widget form bug.
2024-07-24 21:31:28 -06:00
Alexander Capehart
272f7e4047
widgets: downsize widget images in round mode 2024-07-20 21:42:00 -06:00
6 changed files with 118 additions and 41 deletions

View file

@ -19,6 +19,8 @@
package org.oxycblt.auxio.home package org.oxycblt.auxio.home
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.Resources
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
@ -99,6 +101,14 @@ class HomeFragment :
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
private var getContentLauncher: ActivityResultLauncher<String>? = null private var getContentLauncher: ActivityResultLauncher<String>? = null
private var pendingImportTarget: Playlist? = null private var pendingImportTarget: Playlist? = null
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 maxBitmapMemory = 6 * sw * sh
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -142,6 +152,9 @@ class HomeFragment :
MenuCompat.setGroupDividerEnabled(menu, true) MenuCompat.setGroupDividerEnabled(menu, true)
} }
binding.homeNormalToolbar.title = "${maxBitmapMemory}"
binding.homeNormalToolbar.subtitle = "${Build.MANUFACTURER} / ${Build.MODEL}"
// Load the track color in manually as it's unclear whether the track actually supports // Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources // using a ColorStateList in the resources
binding.homeIndexingProgress.trackColor = binding.homeIndexingProgress.trackColor =

View file

@ -107,7 +107,10 @@ class RoundedRectTransformation(
} }
private fun calculateOutputSize(input: Bitmap, size: Size): Pair<Int, Int> { 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 = val multiplier =
DecodeUtils.computeSizeMultiplier( DecodeUtils.computeSizeMultiplier(
srcWidth = input.width, 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.graphics.Bitmap
import android.os.Build import android.os.Build
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -96,24 +97,19 @@ constructor(
0 0
} }
return if (cornerRadius > 0) { val transformations = buildList {
// If rounded, reduce the bitmap size further to obtain more pronounced
// rounded corners.
builder.size(getSafeRemoteViewsImageSize(context, 10f))
val cornersTransformation =
RoundedRectTransformation(cornerRadius.toFloat())
if (imageSettings.forceSquareCovers) { if (imageSettings.forceSquareCovers) {
builder.transformations( add(SquareCropTransformation.INSTANCE)
SquareCropTransformation.INSTANCE, cornersTransformation) }
if (cornerRadius > 0) {
add(WidgetBitmapTransformation(15f))
add(RoundedRectTransformation(cornerRadius.toFloat()))
} else { } else {
builder.transformations(cornersTransformation) add(WidgetBitmapTransformation(3f))
} }
} else {
if (imageSettings.forceSquareCovers) {
builder.transformations(SquareCropTransformation.INSTANCE)
}
builder.size(getSafeRemoteViewsImageSize(context))
} }
return builder.size(Size.ORIGINAL).transformations(transformations)
} }
override fun onCompleted(bitmap: Bitmap?) { override fun onCompleted(bitmap: Bitmap?) {

View file

@ -101,17 +101,45 @@ class WidgetProvider : AppWidgetProvider() {
SizeF(180f, 272f) to newThinPaneLayout(context, uiSettings, state), SizeF(180f, 272f) to newThinPaneLayout(context, uiSettings, state),
SizeF(304f, 272f) to newWidePaneLayout(context, uiSettings, state)) SizeF(304f, 272f) to newWidePaneLayout(context, uiSettings, state))
// This is the order in which we will disable cover art layouts if they exceed the
// maximum bitmap memory usage. (See the comment in the loop below for more info.)
val victims =
mutableSetOf(
R.layout.widget_wafer_thin,
R.layout.widget_wafer_wide,
R.layout.widget_pane_thin,
R.layout.widget_pane_wide,
R.layout.widget_docked_thin,
R.layout.widget_docked_wide,
)
// Manually update AppWidgetManager with the new views. // Manually update AppWidgetManager with the new views.
val awm = AppWidgetManager.getInstance(context) val awm = AppWidgetManager.getInstance(context)
val component = ComponentName(context, this::class.java) val component = ComponentName(context, this::class.java)
try { while (victims.size > 0) {
awm.updateAppWidgetCompat(context, component, views) try {
logD("Successfully updated RemoteViews layout") awm.updateAppWidgetCompat(context, component, views)
} catch (e: Exception) { logD("Successfully updated RemoteViews layout")
// Layout update failed, gracefully degrade to the default widget. return
logW("Unable to update widget: $e") } catch (e: Exception) {
reset(context, uiSettings) logW("Encountered widget error: $e")
logW("Widget message: ${e.message}")
// Some android devices on Android 12-14 suffer from a bug where the maximum bitmap
// size calculation does not factor in bitmaps shared across multiple RemoteView
// forms.
// To mitigate an outright crash, progressively disable layouts that contain cover
// art
// in order of least to most commonly used until it actually works.
logW("Killing layout: ${victims.first()}, remaining: ${victims.size - 1}")
val victim = victims.first()
val view = views.entries.find { it.value.layoutId == victim } ?: continue
view.value.setImageViewBitmap(R.id.widget_cover, null)
victims.remove(victim)
}
} }
// We flat-out cannot fit the bitmap into the widget. Weird.
logW("Unable to update widget: Bitmap too large")
reset(context, uiSettings)
} }
/** /**

View file

@ -27,7 +27,6 @@ import android.widget.RemoteViews
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import kotlin.math.sqrt
import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent
@ -46,24 +45,6 @@ fun newRemoteViews(context: Context, @LayoutRes layoutRes: Int): RemoteViews {
return views 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 3 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 = 3f): 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. * Set the background resource of a [RemoteViews] View.
* *