Compare commits
13 commits
dev
...
widget-fix
Author | SHA1 | Date | |
---|---|---|---|
![]() |
396893cd26 | ||
![]() |
7db878dd73 | ||
![]() |
716d6fff6a | ||
![]() |
f19cae0a59 | ||
![]() |
a33bbd9cec | ||
![]() |
c11e03b451 | ||
![]() |
3e4b27d76b | ||
![]() |
b185914ba1 | ||
![]() |
4ee2e81995 | ||
![]() |
74e0da526d | ||
![]() |
90fbbf587b | ||
![]() |
186c5547ba | ||
![]() |
272f7e4047 |
6 changed files with 118 additions and 41 deletions
|
@ -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 =
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?) {
|
||||||
|
|
|
@ -101,18 +101,46 @@ 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)
|
||||||
|
while (victims.size > 0) {
|
||||||
try {
|
try {
|
||||||
awm.updateAppWidgetCompat(context, component, views)
|
awm.updateAppWidgetCompat(context, component, views)
|
||||||
logD("Successfully updated RemoteViews layout")
|
logD("Successfully updated RemoteViews layout")
|
||||||
|
return
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Layout update failed, gracefully degrade to the default widget.
|
logW("Encountered widget error: $e")
|
||||||
logW("Unable to update widget: $e")
|
logW("Widget message: ${e.message}")
|
||||||
reset(context, uiSettings)
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revert to the default layout that displays "No music playing".
|
* Revert to the default layout that displays "No music playing".
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue