widgets: backport to <v31
Modify the minimal wdiget so that it can work on Android 11 and below. Was not actually that hard, mostly changing layouts around all things considered. This also splits up the default view and the main widget views, which makes managing UI state much easier.
This commit is contained in:
parent
ffebbc2839
commit
8673995630
18 changed files with 136 additions and 82 deletions
|
@ -57,9 +57,9 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round" />
|
android:roundIcon="@mipmap/ic_launcher_round" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
|
android:label="@string/info_widget_minimal"
|
||||||
android:name=".widgets.MinimalWidgetProvider"
|
android:name=".widgets.MinimalWidgetProvider"
|
||||||
android:exported="false"
|
android:exported="false">
|
||||||
android:enabled="@bool/widgets_supported">
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
|
|
@ -20,7 +20,6 @@ import org.oxycblt.auxio.ui.isNight
|
||||||
/**
|
/**
|
||||||
* The single [AppCompatActivity] for Auxio.
|
* The single [AppCompatActivity] for Auxio.
|
||||||
* TODO: Port widgets to non-12 android
|
* TODO: Port widgets to non-12 android
|
||||||
* TODO: Fix intent issues
|
|
||||||
*/
|
*/
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val playbackModel: PlaybackViewModel by viewModels()
|
private val playbackModel: PlaybackViewModel by viewModels()
|
||||||
|
|
|
@ -78,7 +78,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
// Widgets
|
// Widgets
|
||||||
private val minimalWidget = MinimalWidgetProvider.new()
|
private val minimalWidget = MinimalWidgetProvider()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
private var isForeground = false
|
private var isForeground = false
|
||||||
|
@ -179,6 +179,10 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
audioReactor.release()
|
audioReactor.release()
|
||||||
releaseWakelock()
|
releaseWakelock()
|
||||||
|
|
||||||
|
// Technically the widgets don't *have* to be reset, but any commands from them
|
||||||
|
// won't work if the service is dead, so we do it anyway
|
||||||
|
minimalWidget.stop(this)
|
||||||
|
|
||||||
playbackManager.removeCallback(this)
|
playbackManager.removeCallback(this)
|
||||||
settingsManager.removeCallback(this)
|
settingsManager.removeCallback(this)
|
||||||
|
|
||||||
|
@ -246,14 +250,14 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
this, song, settingsManager.colorizeNotif, ::startForegroundOrNotify
|
this, song, settingsManager.colorizeNotif, ::startForegroundOrNotify
|
||||||
)
|
)
|
||||||
|
|
||||||
minimalWidget?.update(this, playbackManager)
|
minimalWidget.update(this, playbackManager)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear if there's nothing to play.
|
// Clear if there's nothing to play.
|
||||||
player.stop()
|
player.stop()
|
||||||
minimalWidget?.stop(this)
|
minimalWidget.stop(this)
|
||||||
stopForegroundAndNotification()
|
stopForegroundAndNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.widget.RemoteViews
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.MainActivity
|
import org.oxycblt.auxio.MainActivity
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.logD
|
import org.oxycblt.auxio.logD
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
|
||||||
|
@ -112,11 +113,3 @@ abstract class BaseWidget : AppWidgetProvider() {
|
||||||
const val KEY_WIDGET_TYPE = BuildConfig.APPLICATION_ID + ".key.WIDGET_TYPE"
|
const val KEY_WIDGET_TYPE = BuildConfig.APPLICATION_ID + ".key.WIDGET_TYPE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PLAN:
|
|
||||||
// - Service calls methods on the widgets, think a subset of PlaybackStateManager.Callback
|
|
||||||
// - Widgets send BACK broadcasts to control playback, as other parts of the code do
|
|
||||||
// - Since widgets are broadcastrecievers, this is okay, shouldn't cause memory leaks
|
|
||||||
// - Can't use playbackstatemanager here since that would make the app unkillable
|
|
||||||
// - Callbacks also need to handle PlaybackService dying and being unable to send any
|
|
||||||
// more updates, and PlaybackService starting up as well
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.oxycblt.auxio.widgets
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.View
|
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.coil.loadBitmap
|
import org.oxycblt.auxio.coil.loadBitmap
|
||||||
|
@ -17,14 +16,7 @@ class MinimalWidgetProvider : BaseWidget() {
|
||||||
override val type: Int get() = TYPE
|
override val type: Int get() = TYPE
|
||||||
|
|
||||||
override fun getDefaultViews(context: Context): RemoteViews {
|
override fun getDefaultViews(context: Context): RemoteViews {
|
||||||
val views = getRemoteViews(context, LAYOUT)
|
return getRemoteViews(context, R.layout.widget_default)
|
||||||
|
|
||||||
views.setInt(R.id.widget_cover, "setImageResource", R.drawable.ic_song_clear)
|
|
||||||
views.setInt(R.id.widget_cover, "setVisibility", View.VISIBLE)
|
|
||||||
views.setInt(R.id.widget_placeholder_msg, "setVisibility", View.VISIBLE)
|
|
||||||
views.setInt(R.id.widget_meta, "setVisibility", View.GONE)
|
|
||||||
|
|
||||||
return views
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateViews(
|
override fun updateViews(
|
||||||
|
@ -32,15 +24,12 @@ class MinimalWidgetProvider : BaseWidget() {
|
||||||
playbackManager: PlaybackStateManager,
|
playbackManager: PlaybackStateManager,
|
||||||
onDone: (RemoteViews) -> Unit
|
onDone: (RemoteViews) -> Unit
|
||||||
) {
|
) {
|
||||||
val views = getRemoteViews(context, LAYOUT)
|
|
||||||
val song = playbackManager.song
|
val song = playbackManager.song
|
||||||
|
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
logD("updating view to ${song.name}")
|
logD("updating view to ${song.name}")
|
||||||
|
|
||||||
// Show the proper widget views
|
val views = getRemoteViews(context, R.layout.widget_minimal)
|
||||||
views.setInt(R.id.widget_placeholder_msg, "setVisibility", View.GONE)
|
|
||||||
views.setInt(R.id.widget_meta, "setVisibility", View.VISIBLE)
|
|
||||||
|
|
||||||
// Update the metadata
|
// Update the metadata
|
||||||
views.setTextViewText(R.id.widget_song, song.name)
|
views.setTextViewText(R.id.widget_song, song.name)
|
||||||
|
@ -51,24 +40,18 @@ class MinimalWidgetProvider : BaseWidget() {
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
views.setBitmap(R.id.widget_cover, "setImageBitmap", bitmap)
|
views.setBitmap(R.id.widget_cover, "setImageBitmap", bitmap)
|
||||||
} else {
|
} else {
|
||||||
views.setInt(R.id.widget_cover, "setImageResource", R.drawable.ic_song_clear)
|
views.setInt(R.id.widget_cover, "setImageResource", R.drawable.ic_song)
|
||||||
}
|
}
|
||||||
|
|
||||||
onDone(views)
|
onDone(views)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
views.setInt(R.id.widget_cover, "setImageResource", R.drawable.ic_song_clear)
|
onDone(getDefaultViews(context))
|
||||||
onDone(views)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TYPE = 0xA0D0
|
const val TYPE = 0xA0D0
|
||||||
|
|
||||||
// Workaround to make studio shut up about perfectly valid layouts somehow
|
|
||||||
// being invalid for remote views.
|
|
||||||
const val LAYOUT = R.layout.widget_minimal
|
|
||||||
|
|
||||||
fun new(): MinimalWidgetProvider? {
|
fun new(): MinimalWidgetProvider? {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
MinimalWidgetProvider()
|
MinimalWidgetProvider()
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<corners />
|
|
||||||
<solid android:color="@color/surface_black" />
|
|
||||||
</shape>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<corners />
|
|
||||||
<solid android:color="@color/surface_day" />
|
|
||||||
</shape>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<corners />
|
|
||||||
<solid android:color="@color/surface_night" />
|
|
||||||
</shape>
|
|
|
@ -15,7 +15,7 @@
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
tools:ignore="contentDescription"
|
tools:ignore="contentDescription"
|
||||||
tools:src="@drawable/ic_song_clear" />
|
tools:src="@drawable/ic_song" />
|
||||||
|
|
||||||
<android.widget.LinearLayout
|
<android.widget.LinearLayout
|
||||||
android:id="@+id/widget_meta"
|
android:id="@+id/widget_meta"
|
||||||
|
@ -31,6 +31,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
@ -49,14 +51,4 @@
|
||||||
|
|
||||||
</android.widget.LinearLayout>
|
</android.widget.LinearLayout>
|
||||||
|
|
||||||
<android.widget.TextView
|
|
||||||
android:id="@+id/widget_placeholder_msg"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="@dimen/spacing_medium"
|
|
||||||
android:text="@string/placeholder_playback"
|
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
32
app/src/main/res/layout/widget_default.xml
Normal file
32
app/src/main/res/layout/widget_default.xml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@android:id/background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?android:attr/colorBackground"
|
||||||
|
android:theme="@style/Theme.Widget">
|
||||||
|
|
||||||
|
<android.widget.ImageView
|
||||||
|
android:id="@+id/widget_icon"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:alpha="0.3"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:ignore="contentDescription"
|
||||||
|
android:src="@drawable/ic_song" />
|
||||||
|
|
||||||
|
<android.widget.TextView
|
||||||
|
android:id="@+id/widget_msg"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:padding="@dimen/spacing_medium"
|
||||||
|
android:text="@string/placeholder_playback"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
54
app/src/main/res/layout/widget_minimal.xml
Normal file
54
app/src/main/res/layout/widget_minimal.xml
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@android:id/background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?android:attr/colorBackground"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:theme="@style/Theme.Widget">
|
||||||
|
|
||||||
|
<android.widget.ImageView
|
||||||
|
android:id="@+id/widget_cover"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:ignore="contentDescription"
|
||||||
|
tools:src="@drawable/ic_song" />
|
||||||
|
|
||||||
|
<android.widget.LinearLayout
|
||||||
|
android:id="@+id/widget_meta"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="?android:attr/colorBackground"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/spacing_medium">
|
||||||
|
|
||||||
|
<android.widget.TextView
|
||||||
|
android:id="@+id/widget_song"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="Song Name" />
|
||||||
|
|
||||||
|
<android.widget.TextView
|
||||||
|
android:id="@+id/widget_artist"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
tools:text="Artist Name" />
|
||||||
|
|
||||||
|
</android.widget.LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
14
app/src/main/res/values-v29/styles_core.xml
Normal file
14
app/src/main/res/values-v29/styles_core.xml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.Widget" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
|
<!-- I really don't care enough to make Auxio's theming correspond to widget theming.
|
||||||
|
It doesn't happen on Android 12 anyway, so why bother backporting it when its so
|
||||||
|
useless. -->
|
||||||
|
<item name="colorPrimary">?android:attr/colorAccent</item>
|
||||||
|
<item name="colorSecondary">?android:attr/colorAccent</item>
|
||||||
|
|
||||||
|
<item name="colorSurface">@color/surface_color</item>
|
||||||
|
<item name="android:windowBackground">?attr/colorSurface</item>
|
||||||
|
<item name="android:colorBackground">?attr/colorSurface</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!--
|
|
||||||
Widgets are limited to android 12 until I can find an okay-ish way to backport them.
|
|
||||||
-->
|
|
||||||
<bool name="widgets_supported">true</bool>
|
|
||||||
</resources>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="Theme.Widget" parent="@android:style/Theme.DeviceDefault.DayNight">
|
|
||||||
<item name="colorPrimary">?android:attr/colorAccent</item>
|
|
||||||
<item name="colorSecondary">?android:attr/colorAccent</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<bool name="widgets_supported">false</bool>
|
|
||||||
</resources>
|
|
|
@ -4,6 +4,7 @@
|
||||||
<!-- Info namespace | App labels -->
|
<!-- Info namespace | App labels -->
|
||||||
<string name="info_app_desc">A simple, rational music player for android.</string>
|
<string name="info_app_desc">A simple, rational music player for android.</string>
|
||||||
<string name="info_channel_name">Music Playback</string>
|
<string name="info_channel_name">Music Playback</string>
|
||||||
|
<string name="info_widget_minimal">Minimal</string>
|
||||||
|
|
||||||
<!-- Label Namespace | Static Labels -->
|
<!-- Label Namespace | Static Labels -->
|
||||||
<string name="label_retry">Retry</string>
|
<string name="label_retry">Retry</string>
|
||||||
|
|
|
@ -42,4 +42,10 @@
|
||||||
<item name="colorSurface">@color/surface_black</item>
|
<item name="colorSurface">@color/surface_black</item>
|
||||||
<item name="materialAlertDialogTheme">@style/Theme.CustomDialog.Black</item>
|
<item name="materialAlertDialogTheme">@style/Theme.CustomDialog.Black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Theming widgets is technically possible below Android 10, but its too much of a hassle.
|
||||||
|
Default to a light blue theme.
|
||||||
|
-->
|
||||||
|
<style name="Theme.Widget" parent="@style/Theme.Blue" />
|
||||||
</resources>
|
</resources>
|
12
app/src/main/res/xml/widget_minimal.xml
Normal file
12
app/src/main/res/xml/widget_minimal.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:initialLayout="@layout/widget_minimal"
|
||||||
|
android:minWidth="110dp"
|
||||||
|
android:minHeight="110dp"
|
||||||
|
android:minResizeWidth="110dp"
|
||||||
|
android:minResizeHeight="110dp"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:updatePeriodMillis="0"
|
||||||
|
android:widgetCategory="home_screen">
|
||||||
|
|
||||||
|
</appwidget-provider>
|
Loading…
Reference in a new issue