widgets: rework tiny widget

Rework the tiny widget to cram even more information into it.

The tiny widget is nominally meant for edge cases like exceptionally
small screens or landscape mode, but apparently it's triggered on some
devices in normal use because of platform fragmentation and OEM insanity.

Update the tiny widget layout with some new buttons in order to make it
more usable in mnormal use. This is still nowehre near ideal. For
example, when triggering the layout on my device, it ends up squishing
the buttons. But it should probably work better outside of those edge
cases.
This commit is contained in:
OxygenCobalt 2022-05-24 10:08:12 -06:00
parent 485c35d74c
commit eb293022e8
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
16 changed files with 85 additions and 91 deletions

View file

@ -39,7 +39,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
/**
* The single [AppCompatActivity] for Auxio.
*
* TODO: Add a new view for crashes with a stack trace
* TODO: Add crash reporting and error screens. This likely has to be an external activity, so it is
* blocked by eliminating exitProcess from the app.
*
* TODO: Custom language support
*

View file

@ -44,8 +44,6 @@ import org.oxycblt.auxio.util.logD
* A wrapper around the home fragment that shows the playback fragment and controls the more
* high-level navigation features.
* @author OxygenCobalt
*
* TODO: Add a new view with a stack trace whenever the music loading process fails.
*/
class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
private val playbackModel: PlaybackViewModel by activityViewModels()

View file

@ -41,6 +41,7 @@ import org.oxycblt.auxio.music.Song
class BitmapProvider(private val context: Context) {
private var currentRequest: Request? = null
/* If this provider is currently attempting to load something. */
val isBusy: Boolean
get() = currentRequest?.run { !disposable.isDisposed } ?: false
@ -53,7 +54,7 @@ class BitmapProvider(private val context: Context) {
currentRequest = null
val request =
target.setupRequest(
target.onConfigRequest(
ImageRequest.Builder(context)
.data(song)
.size(Size.ORIGINAL)
@ -76,8 +77,15 @@ class BitmapProvider(private val context: Context) {
private data class Request(val disposable: Disposable, val callback: Target)
/** Represents the target for a request. */
interface Target {
fun setupRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
/** Modify the default request with custom attributes. */
fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
/**
* Called when the loading process is completed. [bitmap] will be null if there was an
* error.
*/
fun onCompleted(bitmap: Bitmap?)
}
}

View file

@ -71,9 +71,8 @@ class AlbumArtFetcher private constructor(private val context: Context, private
}
class AlbumFactory : Fetcher.Factory<Album> {
override fun create(data: Album, options: Options, imageLoader: ImageLoader): Fetcher {
return AlbumArtFetcher(options.context, data)
}
override fun create(data: Album, options: Options, imageLoader: ImageLoader) =
AlbumArtFetcher(options.context, data)
}
}
@ -90,14 +89,12 @@ private constructor(
override suspend fun fetch(): FetchResult? {
val albums = Sort.ByName(true).albums(artist.albums)
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
return createMosaic(context, results, size)
}
class Factory : Fetcher.Factory<Artist> {
override fun create(data: Artist, options: Options, imageLoader: ImageLoader): Fetcher {
return ArtistImageFetcher(options.context, options.size, data)
}
override fun create(data: Artist, options: Options, imageLoader: ImageLoader) =
ArtistImageFetcher(options.context, options.size, data)
}
}
@ -115,14 +112,12 @@ private constructor(
// Don't sort here to preserve compatibility with previous versions of this image.
val albums = genre.songs.groupBy { it.album }.keys
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
return createMosaic(context, results, size)
}
class Factory : Fetcher.Factory<Genre> {
override fun create(data: Genre, options: Options, imageLoader: ImageLoader): Fetcher {
return GenreImageFetcher(options.context, options.size, data)
}
override fun create(data: Genre, options: Options, imageLoader: ImageLoader) =
GenreImageFetcher(options.context, options.size, data)
}
}

View file

@ -119,9 +119,7 @@ sealed class Sort(open val isAscending: Boolean) {
genres.sortWith(compareByDynamic(NameComparator()) { it })
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByName(newIsAscending)
}
override fun ascending(newIsAscending: Boolean) = ByName(newIsAscending)
}
/** Sort by the album of an item, only supported by [Song] */
@ -140,9 +138,7 @@ sealed class Sort(open val isAscending: Boolean) {
compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByAlbum(newIsAscending)
}
override fun ascending(newIsAscending: Boolean) = ByAlbum(newIsAscending)
}
/** Sort by the artist of an item, only supported by [Album] and [Song] */
@ -171,9 +167,7 @@ sealed class Sort(open val isAscending: Boolean) {
compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByArtist(newIsAscending)
}
override fun ascending(newIsAscending: Boolean) = ByArtist(newIsAscending)
}
/** Sort by the year of an item, only supported by [Album] and [Song] */
@ -200,9 +194,7 @@ sealed class Sort(open val isAscending: Boolean) {
compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByYear(newIsAscending)
}
override fun ascending(newIsAscending: Boolean) = ByYear(newIsAscending)
}
/** Sort by the duration of the item. Supports all items. */
@ -237,9 +229,7 @@ sealed class Sort(open val isAscending: Boolean) {
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByDuration(newIsAscending)
}
override fun ascending(newIsAscending: Boolean) = ByDuration(newIsAscending)
}
/** Sort by the amount of songs. Only applicable to music parents. */
@ -268,9 +258,7 @@ sealed class Sort(open val isAscending: Boolean) {
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByCount(newIsAscending)
}
override fun ascending(newIsAscending: Boolean) = ByCount(newIsAscending)
}
/**
@ -293,9 +281,7 @@ sealed class Sort(open val isAscending: Boolean) {
compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByDisc(newIsAscending)
}
override fun ascending(newIsAscending: Boolean) = ByDisc(newIsAscending)
}
/**
@ -317,9 +303,7 @@ sealed class Sort(open val isAscending: Boolean) {
compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByTrack(newIsAscending)
}
override fun ascending(newIsAscending: Boolean) = ByTrack(newIsAscending)
}
val intCode: Int

View file

@ -41,7 +41,6 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.getDrawableSafe
import org.oxycblt.auxio.util.logD
/**
* An [AppCompatImageView] that applies many of the stylistic choices that Auxio uses regarding
@ -130,7 +129,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
override fun draw(canvas: Canvas) {
logD(src.alpha)
src.bounds.set(canvas.clipBounds)
val adjustWidth = src.bounds.width() / 4
val adjustHeight = src.bounds.height() / 4

View file

@ -35,13 +35,13 @@ fun createDefaultWidget(context: Context): RemoteViews {
}
/**
* The tiny widget is for an edge-case situation where a 2xN widget happens to be smaller than
* 100dp. It just shows the cover, titles, and a button.
* The tiny widget is for an edge-case situation where a widget falls under the size class of the
* small widget, either via landscape mode or exceptionally small screens.
*/
fun createTinyWidget(context: Context, state: WidgetComponent.WidgetState): RemoteViews {
return createViews(context, R.layout.widget_tiny)
.applyMeta(context, state)
.applyPlayControls(context, state)
.applyBasicControls(context, state)
}
/**

View file

@ -50,6 +50,10 @@ class WidgetComponent(private val context: Context) :
init {
playbackManager.addCallback(this)
settingsManager.addCallback(this)
if (playbackManager.isInitialized) {
update()
}
}
/*
@ -76,7 +80,7 @@ class WidgetComponent(private val context: Context) :
provider.load(
song,
object : BitmapProvider.Target {
override fun setupRequest(builder: ImageRequest.Builder): ImageRequest.Builder {
override fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder {
// The widget has two distinct styles that we must transform the album art to
// accommodate:
// - Before Android 12, the widget has hard edges, so we don't need to round

View file

@ -122,6 +122,13 @@ class WidgetProvider : AppWidgetProvider() {
val name = ComponentName(context, WidgetProvider::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
for (id in getAppWidgetIds(name)) {
val options = getAppWidgetOptions(id)
logD(
options.getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES)
?: "no sizes")
}
// Widgets are automatically responsive on Android 12, no need to do anything.
updateAppWidget(name, RemoteViews(views))
} else {

View file

@ -7,5 +7,5 @@
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z" />
android:pathData="M 18.119999,7.0600006 12,13.166666 5.8800004,7.0600006 4.0000006,8.9400004 12,16.939999 19.999999,8.9400004 Z" />
</vector>

View file

@ -13,7 +13,7 @@
a terrible API.
-->
<path
android:fillColor="@android:color/white"
android:fillColor="?attr/colorPrimary"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z" />
<path
android:fillColor="?attr/colorSurface"

View file

@ -8,14 +8,7 @@
android:theme="@style/Theme.Widget">
<!--
For this widget form to work, we need to scale the ImageView across a 1:1 aspect ratio.
However, since we are working with a RemoteViews instance, we can't just use ConstraintLayout
to achieve this. We can use RelativeLayout, but there's no way to force an aspect ratio with
that layout. However, if we create an invisible ImageView that contains a massive fixed-size
drawable and then clamp our main ImageView to it, we can make the view scale on a 1:1 aspect
ratio.
This is easily one of the worst layout hacks I've done, but it seems to work.
See widget_small.xml for an explanation for the ImageView setup
-->
<android.widget.ImageView

View file

@ -8,14 +8,7 @@
android:theme="@style/Theme.Widget">
<!--
For this widget form to work, we need to scale the ImageView across a 1:1 aspect ratio.
However, since we are working with a RemoteViews instance, we can't just use ConstraintLayout
to achieve this. We can use RelativeLayout, but there's no way to force an aspect ratio with
that layout. However, if we create an invisible ImageView that contains a massive fixed-size
drawable and then clamp our main ImageView to it, we can make the view scale on a 1:1 aspect
ratio.
This is easily one of the worst layout hacks I've done, but it seems to work.
See widget_small.xml for an explanation for the ImageView setup
-->
<android.widget.ImageView

View file

@ -8,11 +8,6 @@
android:orientation="vertical"
android:theme="@style/Theme.Widget">
<!--
This is a throwaway layout designed for the rare edge-case where a 2xN widget is shown
in landscape mode.
-->
<android.widget.LinearLayout
style="@style/Widget.Auxio.AppWidget.Panel"
android:layout_width="match_parent"
@ -22,7 +17,8 @@
<android.widget.ImageView
android:id="@+id/widget_cover"
style="@style/Widget.Auxio.Image.Medium"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_marginEnd="@dimen/spacing_medium"
android:contentDescription="@string/desc_no_cover"
android:scaleType="centerCrop"
@ -37,28 +33,51 @@
<android.widget.TextView
android:id="@+id/widget_song"
style="@style/Widget.Auxio.TextView.Primary.AppWidget"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/def_widget_song" />
<android.widget.TextView
android:id="@+id/widget_artist"
style="@style/Widget.Auxio.TextView.Secondary.AppWidget"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/def_widget_artist" />
</android.widget.LinearLayout>
<android.widget.LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:orientation="horizontal">
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev" />
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_play_pause"
android:minWidth="@dimen/size_btn_small"
android:minHeight="@dimen/size_btn_small"
android:src="@drawable/sel_playing_state" />
android:src="@drawable/ic_play" />
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next" />
</android.widget.LinearLayout>
</android.widget.LinearLayout>
</android.widget.LinearLayout>
</android.widget.LinearLayout>

View file

@ -8,14 +8,7 @@
android:theme="@style/Theme.Widget">
<!--
For this widget form to work, we need to scale the ImageView across a 1:1 aspect ratio.
However, since we are working with a RemoteViews instance, we can't just use ConstraintLayout
to achieve this. We can use RelativeLayout, but there's no way to force an aspect ratio with
that layout. However, if we create an invisible ImageView that contains a massive fixed-size
drawable and then clamp our main ImageView to it, we can make the view scale on a 1:1 aspect
ratio.
This is easily one of the worst layout hacks I've done, but it seems to work.
See widget_small.xml for an explanation for the ImageView setup
-->
<android.widget.ImageView

View file

@ -178,6 +178,7 @@
<string name="clr_orange">Orange</string>
<string name="clr_brown">Brown</string>
<string name="clr_grey">Grey</string>
<string name="clr_dynamic">Dynamic</string>
<!-- Format Namespace | Value formatting/plurals -->
<string name="fmt_disc_no">Disc %d</string>