widgets: add controls to minimal widget
Backtrack and add controls to the minimal widget, mostly for usability.
This commit is contained in:
parent
8673995630
commit
d3e738b973
15 changed files with 260 additions and 95 deletions
|
@ -5,20 +5,19 @@ import android.app.NotificationChannel
|
|||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media.app.NotificationCompat.MediaStyle
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.coil.loadBitmap
|
||||
import org.oxycblt.auxio.music.Parent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.ui.newBroadcastIntent
|
||||
import org.oxycblt.auxio.ui.newMainIntent
|
||||
|
||||
/**
|
||||
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have
|
||||
|
@ -35,24 +34,18 @@ class PlaybackNotification private constructor(
|
|||
else 0
|
||||
|
||||
init {
|
||||
val activityIntent = PendingIntent.getActivity(
|
||||
context, REQUEST_CODE,
|
||||
Intent(context, MainActivity::class.java),
|
||||
pendingIntentFlags
|
||||
)
|
||||
|
||||
setSmallIcon(R.drawable.ic_song)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
setShowWhen(false)
|
||||
setSilent(true)
|
||||
setContentIntent(activityIntent)
|
||||
setContentIntent(context.newMainIntent())
|
||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
|
||||
addAction(buildLoopAction(context, LoopMode.NONE))
|
||||
addAction(buildAction(context, ACTION_SKIP_PREV, R.drawable.ic_skip_prev))
|
||||
addAction(buildAction(context, PlaybackService.ACTION_SKIP_PREV, R.drawable.ic_skip_prev))
|
||||
addAction(buildPlayPauseAction(context, true))
|
||||
addAction(buildAction(context, ACTION_SKIP_NEXT, R.drawable.ic_skip_next))
|
||||
addAction(buildAction(context, ACTION_EXIT, R.drawable.ic_exit))
|
||||
addAction(buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next))
|
||||
addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_exit))
|
||||
|
||||
setStyle(
|
||||
MediaStyle()
|
||||
|
@ -130,7 +123,7 @@ class PlaybackNotification private constructor(
|
|||
): NotificationCompat.Action {
|
||||
val drawableRes = if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play
|
||||
|
||||
return buildAction(context, ACTION_PLAY_PAUSE, drawableRes)
|
||||
return buildAction(context, PlaybackService.ACTION_PLAY_PAUSE, drawableRes)
|
||||
}
|
||||
|
||||
private fun buildLoopAction(
|
||||
|
@ -143,7 +136,7 @@ class PlaybackNotification private constructor(
|
|||
LoopMode.TRACK -> R.drawable.ic_loop_one
|
||||
}
|
||||
|
||||
return buildAction(context, ACTION_LOOP, drawableRes)
|
||||
return buildAction(context, PlaybackService.ACTION_LOOP, drawableRes)
|
||||
}
|
||||
|
||||
private fun buildShuffleAction(
|
||||
|
@ -152,7 +145,7 @@ class PlaybackNotification private constructor(
|
|||
): NotificationCompat.Action {
|
||||
val drawableRes = if (isShuffled) R.drawable.ic_shuffle else R.drawable.ic_shuffle_inactive
|
||||
|
||||
return buildAction(context, ACTION_SHUFFLE, drawableRes)
|
||||
return buildAction(context, PlaybackService.ACTION_SHUFFLE, drawableRes)
|
||||
}
|
||||
|
||||
private fun buildAction(
|
||||
|
@ -162,10 +155,7 @@ class PlaybackNotification private constructor(
|
|||
): NotificationCompat.Action {
|
||||
val action = NotificationCompat.Action.Builder(
|
||||
iconRes, actionName,
|
||||
PendingIntent.getBroadcast(
|
||||
context, REQUEST_CODE,
|
||||
Intent(actionName), pendingIntentFlags
|
||||
)
|
||||
context.newBroadcastIntent(actionName)
|
||||
)
|
||||
|
||||
return action.build()
|
||||
|
@ -174,14 +164,6 @@ class PlaybackNotification private constructor(
|
|||
companion object {
|
||||
const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
||||
const val NOTIFICATION_ID = 0xA0A0
|
||||
const val REQUEST_CODE = 0xA0C0
|
||||
|
||||
const val ACTION_LOOP = BuildConfig.APPLICATION_ID + ".action.LOOP"
|
||||
const val ACTION_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE"
|
||||
const val ACTION_SKIP_PREV = BuildConfig.APPLICATION_ID + ".action.PREV"
|
||||
const val ACTION_PLAY_PAUSE = BuildConfig.APPLICATION_ID + ".action.PLAY_PAUSE"
|
||||
const val ACTION_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT"
|
||||
const val ACTION_EXIT = BuildConfig.APPLICATION_ID + ".action.EXIT"
|
||||
|
||||
/**
|
||||
* Build a new instance of [PlaybackNotification].
|
||||
|
|
|
@ -43,7 +43,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
|||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.widgets.BaseWidget
|
||||
import org.oxycblt.auxio.widgets.MinimalWidgetProvider
|
||||
import org.oxycblt.auxio.widgets.WidgetController
|
||||
|
||||
/**
|
||||
* A service that manages the system-side aspects of playback, such as:
|
||||
|
@ -56,6 +56,8 @@ import org.oxycblt.auxio.widgets.MinimalWidgetProvider
|
|||
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback],
|
||||
* so therefore there's no need to bind to it to deliver commands.
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Try to split up this god object somewhat, such as making the notification state-aware.
|
||||
*/
|
||||
class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
|
||||
|
||||
|
@ -71,15 +73,13 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
// System backend components
|
||||
private lateinit var audioReactor: AudioReactor
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
private lateinit var widgets: WidgetController
|
||||
private val systemReceiver = SystemEventReceiver()
|
||||
|
||||
// Managers
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
// Widgets
|
||||
private val minimalWidget = MinimalWidgetProvider()
|
||||
|
||||
// State
|
||||
private var isForeground = false
|
||||
|
||||
|
@ -113,12 +113,14 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
false
|
||||
)
|
||||
|
||||
// --- SYSTEM SETUP ---
|
||||
|
||||
audioReactor = AudioReactor(this, player)
|
||||
wakeLock = getSystemServiceSafe(PowerManager::class).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, this::class.simpleName
|
||||
)
|
||||
|
||||
// --- CALLBACKS ---
|
||||
widgets = WidgetController(this)
|
||||
|
||||
// Set up the media button callbacks
|
||||
mediaSession = MediaSessionCompat(this, packageName).apply {
|
||||
|
@ -129,12 +131,12 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
|
||||
// Then the notif/headset callbacks
|
||||
IntentFilter().apply {
|
||||
addAction(PlaybackNotification.ACTION_LOOP)
|
||||
addAction(PlaybackNotification.ACTION_SHUFFLE)
|
||||
addAction(PlaybackNotification.ACTION_SKIP_PREV)
|
||||
addAction(PlaybackNotification.ACTION_PLAY_PAUSE)
|
||||
addAction(PlaybackNotification.ACTION_SKIP_NEXT)
|
||||
addAction(PlaybackNotification.ACTION_EXIT)
|
||||
addAction(ACTION_LOOP)
|
||||
addAction(ACTION_SHUFFLE)
|
||||
addAction(ACTION_SKIP_PREV)
|
||||
addAction(ACTION_PLAY_PAUSE)
|
||||
addAction(ACTION_SKIP_NEXT)
|
||||
addAction(ACTION_EXIT)
|
||||
|
||||
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||
|
@ -177,12 +179,9 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
connector.release()
|
||||
mediaSession.release()
|
||||
audioReactor.release()
|
||||
widgets.release()
|
||||
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)
|
||||
settingsManager.removeCallback(this)
|
||||
|
||||
|
@ -250,14 +249,11 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
this, song, settingsManager.colorizeNotif, ::startForegroundOrNotify
|
||||
)
|
||||
|
||||
minimalWidget.update(this, playbackManager)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Clear if there's nothing to play.
|
||||
player.stop()
|
||||
minimalWidget.stop(this)
|
||||
stopForegroundAndNotification()
|
||||
}
|
||||
|
||||
|
@ -458,22 +454,22 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
|
||||
// --- NOTIFICATION CASES ---
|
||||
|
||||
PlaybackNotification.ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
|
||||
ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
|
||||
!playbackManager.isPlaying
|
||||
)
|
||||
|
||||
PlaybackNotification.ACTION_LOOP -> playbackManager.setLoopMode(
|
||||
ACTION_LOOP -> playbackManager.setLoopMode(
|
||||
playbackManager.loopMode.increment()
|
||||
)
|
||||
|
||||
PlaybackNotification.ACTION_SHUFFLE -> playbackManager.setShuffling(
|
||||
ACTION_SHUFFLE -> playbackManager.setShuffling(
|
||||
!playbackManager.isShuffling, keepSong = true
|
||||
)
|
||||
|
||||
PlaybackNotification.ACTION_SKIP_PREV -> playbackManager.prev()
|
||||
PlaybackNotification.ACTION_SKIP_NEXT -> playbackManager.next()
|
||||
ACTION_SKIP_PREV -> playbackManager.prev()
|
||||
ACTION_SKIP_NEXT -> playbackManager.next()
|
||||
|
||||
PlaybackNotification.ACTION_EXIT -> {
|
||||
ACTION_EXIT -> {
|
||||
playbackManager.setPlaying(false)
|
||||
stopForegroundAndNotification()
|
||||
}
|
||||
|
@ -499,14 +495,9 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
}
|
||||
}
|
||||
|
||||
// Newly added widgets need PlaybackService to
|
||||
BaseWidget.ACTION_WIDGET_UPDATE -> {
|
||||
when (intent.getIntExtra(BaseWidget.KEY_WIDGET_TYPE, -1)) {
|
||||
MinimalWidgetProvider.TYPE -> minimalWidget?.update(
|
||||
this@PlaybackService, playbackManager
|
||||
)
|
||||
}
|
||||
}
|
||||
BaseWidget.ACTION_WIDGET_UPDATE -> widgets.initWidget(
|
||||
intent.getIntExtra(BaseWidget.ACTION_WIDGET_UPDATE, -1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -539,6 +530,11 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
private const val WAKELOCK_TIME = 25000L
|
||||
private const val POS_POLL_INTERVAL = 500L
|
||||
|
||||
const val BROADCAST_WIDGET_START = BuildConfig.APPLICATION_ID + ".key.WIDGETS_START"
|
||||
const val ACTION_LOOP = BuildConfig.APPLICATION_ID + ".action.LOOP"
|
||||
const val ACTION_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE"
|
||||
const val ACTION_SKIP_PREV = BuildConfig.APPLICATION_ID + ".action.PREV"
|
||||
const val ACTION_PLAY_PAUSE = BuildConfig.APPLICATION_ID + ".action.PLAY_PAUSE"
|
||||
const val ACTION_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT"
|
||||
const val ACTION_EXIT = BuildConfig.APPLICATION_ID + ".action.EXIT"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ class PlaybackSessionConnector(
|
|||
|
||||
override fun onStop() {
|
||||
// Get the service to shut down with the ACTION_EXIT intent
|
||||
context.sendBroadcast(Intent(PlaybackNotification.ACTION_EXIT))
|
||||
context.sendBroadcast(Intent(PlaybackService.ACTION_EXIT))
|
||||
}
|
||||
|
||||
// --- PLAYBACKSTATEMANAGER CALLBACKS ---
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package org.oxycblt.auxio.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
|
@ -24,6 +26,7 @@ import androidx.annotation.PluralsRes
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.logE
|
||||
import kotlin.reflect.KClass
|
||||
|
@ -151,6 +154,32 @@ fun Context.showToast(@StringRes str: Int) {
|
|||
Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
const val INTENT_REQUEST_CODE = 0xA0A0
|
||||
|
||||
/**
|
||||
* Create a broadcast [PendingIntent]
|
||||
*/
|
||||
fun Context.newBroadcastIntent(what: String): PendingIntent {
|
||||
return PendingIntent.getBroadcast(
|
||||
this, INTENT_REQUEST_CODE, Intent(what),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
else 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [PendingIntent] that leads to Auxio's [MainActivity]
|
||||
*/
|
||||
fun Context.newMainIntent(): PendingIntent {
|
||||
return PendingIntent.getActivity(
|
||||
this, INTENT_REQUEST_CODE, Intent(this, MainActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
else 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that we are on a background thread.
|
||||
*/
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
package org.oxycblt.auxio.widgets
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.annotation.SuppressLint
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.LayoutRes
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.logD
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.ui.newMainIntent
|
||||
|
||||
/**
|
||||
* The base widget class for all widget implementations in Auxio.
|
||||
|
@ -21,11 +20,16 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
|||
abstract class BaseWidget : AppWidgetProvider() {
|
||||
abstract val type: Int
|
||||
|
||||
/*
|
||||
* Returns the default view for this widget. This should be the "No music playing" screen
|
||||
* in pretty much all cases.
|
||||
*/
|
||||
protected abstract fun getDefaultViews(context: Context): RemoteViews
|
||||
protected open fun createViews(context: Context, @LayoutRes layout: Int): RemoteViews {
|
||||
val views = RemoteViews(context.packageName, layout)
|
||||
|
||||
views.setOnClickPendingIntent(
|
||||
android.R.id.background,
|
||||
context.newMainIntent()
|
||||
)
|
||||
|
||||
return views
|
||||
}
|
||||
|
||||
/*
|
||||
* Update views based off of the playback state. This job is asynchronous and should be
|
||||
|
@ -59,7 +63,7 @@ abstract class BaseWidget : AppWidgetProvider() {
|
|||
logD("Stopping widget")
|
||||
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
manager.applyViews(context, getDefaultViews(context))
|
||||
manager.applyViews(context, defaultViews(context))
|
||||
}
|
||||
|
||||
override fun onUpdate(
|
||||
|
@ -67,7 +71,7 @@ abstract class BaseWidget : AppWidgetProvider() {
|
|||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
appWidgetManager.applyViews(context, getDefaultViews(context))
|
||||
appWidgetManager.applyViews(context, defaultViews(context))
|
||||
|
||||
logD("Sending update intent to PlaybackService")
|
||||
|
||||
|
@ -78,20 +82,12 @@ abstract class BaseWidget : AppWidgetProvider() {
|
|||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
protected fun getRemoteViews(context: Context, @LayoutRes layout: Int): RemoteViews {
|
||||
val views = RemoteViews(context.packageName, layout)
|
||||
|
||||
views.setOnClickPendingIntent(
|
||||
android.R.id.background,
|
||||
PendingIntent.getActivity(
|
||||
context, 0xA0A0, Intent(context, MainActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
else 0
|
||||
)
|
||||
@SuppressLint("RemoteViewLayout")
|
||||
protected fun defaultViews(context: Context): RemoteViews {
|
||||
return RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.widget_default
|
||||
)
|
||||
|
||||
return views
|
||||
}
|
||||
|
||||
private fun AppWidgetManager.applyViews(context: Context, views: RemoteViews) {
|
||||
|
|
|
@ -7,16 +7,40 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.coil.loadBitmap
|
||||
import org.oxycblt.auxio.logD
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||
import org.oxycblt.auxio.ui.newBroadcastIntent
|
||||
|
||||
/**
|
||||
* The minimal widget. This widget only shows the album, song name, and artist without any
|
||||
* controls. Because you know. Minimalism.
|
||||
* The minimal widget, which shows the primary song controls and basic playback information.
|
||||
*/
|
||||
class MinimalWidgetProvider : BaseWidget() {
|
||||
override val type: Int get() = TYPE
|
||||
|
||||
override fun getDefaultViews(context: Context): RemoteViews {
|
||||
return getRemoteViews(context, R.layout.widget_default)
|
||||
override fun createViews(context: Context, layout: Int): RemoteViews {
|
||||
val views = super.createViews(context, layout)
|
||||
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.widget_skip_prev,
|
||||
context.newBroadcastIntent(
|
||||
PlaybackService.ACTION_SKIP_PREV
|
||||
)
|
||||
)
|
||||
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.widget_play_pause,
|
||||
context.newBroadcastIntent(
|
||||
PlaybackService.ACTION_PLAY_PAUSE
|
||||
)
|
||||
)
|
||||
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.widget_skip_next,
|
||||
context.newBroadcastIntent(
|
||||
PlaybackService.ACTION_SKIP_NEXT
|
||||
)
|
||||
)
|
||||
|
||||
return views
|
||||
}
|
||||
|
||||
override fun updateViews(
|
||||
|
@ -29,29 +53,48 @@ class MinimalWidgetProvider : BaseWidget() {
|
|||
if (song != null) {
|
||||
logD("updating view to ${song.name}")
|
||||
|
||||
val views = getRemoteViews(context, R.layout.widget_minimal)
|
||||
val views = createViews(context, R.layout.widget_minimal)
|
||||
|
||||
// Update the metadata
|
||||
views.setTextViewText(R.id.widget_song, song.name)
|
||||
views.setTextViewText(R.id.widget_artist, song.album.artist.name)
|
||||
|
||||
views.setInt(
|
||||
R.id.widget_play_pause,
|
||||
"setImageResource",
|
||||
if (playbackManager.isPlaying) {
|
||||
R.drawable.ic_pause
|
||||
} else {
|
||||
R.drawable.ic_play
|
||||
}
|
||||
)
|
||||
|
||||
// loadBitmap is async, hence the need for onDone
|
||||
loadBitmap(context, song) { bitmap ->
|
||||
if (bitmap != null) {
|
||||
views.setBitmap(R.id.widget_cover, "setImageBitmap", bitmap)
|
||||
views.setCharSequence(
|
||||
R.id.widget_cover, "setContentDescription",
|
||||
context.getString(R.string.description_album_cover, song.album.name)
|
||||
)
|
||||
} else {
|
||||
views.setInt(R.id.widget_cover, "setImageResource", R.drawable.ic_song)
|
||||
views.setCharSequence(
|
||||
R.id.widget_cover, "setContentDescription",
|
||||
context.getString(R.string.description_placeholder_cover)
|
||||
)
|
||||
}
|
||||
|
||||
onDone(views)
|
||||
}
|
||||
} else {
|
||||
onDone(getDefaultViews(context))
|
||||
onDone(defaultViews(context))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE = 0xA0D0
|
||||
|
||||
fun new(): MinimalWidgetProvider? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
MinimalWidgetProvider()
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package org.oxycblt.auxio.widgets
|
||||
|
||||
import android.content.Context
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
|
||||
class WidgetController(private val context: Context) : PlaybackStateManager.Callback {
|
||||
private val manager = PlaybackStateManager.getInstance()
|
||||
private val minimal = MinimalWidgetProvider()
|
||||
|
||||
init {
|
||||
manager.addCallback(this)
|
||||
}
|
||||
|
||||
fun initWidget(type: Int) {
|
||||
when (type) {
|
||||
MinimalWidgetProvider.TYPE -> minimal.update(context, manager)
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
minimal.stop(context)
|
||||
manager.removeCallback(this)
|
||||
}
|
||||
|
||||
override fun onSongUpdate(song: Song?) {
|
||||
minimal.update(context, manager)
|
||||
}
|
||||
|
||||
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||
minimal.update(context, manager)
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?android:attr/colorControlNormal"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
|
|
|
@ -49,6 +49,36 @@
|
|||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Artist Name" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/widget_skip_prev"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/description_skip_prev"
|
||||
android:src="@drawable/ic_skip_prev" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/widget_play_pause"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/description_play_pause"
|
||||
android:src="@drawable/ic_play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/widget_skip_next"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/description_skip_next"
|
||||
android:src="@drawable/ic_skip_next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.widget.LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?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"
|
||||
|
@ -12,8 +11,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.3"
|
||||
android:contentDescription="@string/description_placeholder_cover"
|
||||
android:scaleType="centerCrop"
|
||||
tools:ignore="contentDescription"
|
||||
android:src="@drawable/ic_song" />
|
||||
|
||||
<android.widget.TextView
|
||||
|
@ -21,8 +20,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:fontFamily="@font/inter"
|
||||
android:gravity="center"
|
||||
android:padding="@dimen/spacing_medium"
|
||||
android:text="@string/placeholder_playback"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_medium">
|
||||
|
||||
|
@ -49,6 +50,45 @@
|
|||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Artist Name" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!--
|
||||
Can't use a normal unbounded ripple here since it causes a weird bug
|
||||
where the ripples will have a fixed starting size. Default to the
|
||||
uglier system ripple instead.
|
||||
-->
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/widget_skip_prev"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/description_skip_prev"
|
||||
android:src="@drawable/ic_skip_prev" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/widget_play_pause"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/description_play_pause"
|
||||
android:src="@drawable/ic_play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/widget_skip_next"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/description_skip_next"
|
||||
android:src="@drawable/ic_skip_next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.widget.LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
useless. -->
|
||||
<item name="colorPrimary">?android:attr/colorAccent</item>
|
||||
<item name="colorSecondary">?android:attr/colorAccent</item>
|
||||
<item name="colorControlNormal">@color/control_color</item>
|
||||
<item name="colorControlHighlight">?android:attr/colorControlHighlight</item>
|
||||
|
||||
<item name="colorSurface">@color/surface_color</item>
|
||||
<item name="android:windowBackground">?attr/colorSurface</item>
|
||||
|
|
13
app/src/main/res/values-v31/styles_core.xml
Normal file
13
app/src/main/res/values-v31/styles_core.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?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>
|
||||
<item name="colorControlNormal">?android:attr/colorControlNormal</item>
|
||||
<item name="colorControlHighlight">?android:attr/colorControlHighlight</item>
|
||||
|
||||
<item name="colorSurface">@color/surface_color</item>
|
||||
<item name="android:windowBackground">?attr/colorSurface</item>
|
||||
<item name="android:colorBackground">?attr/colorSurface</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
<!-- Height Namespace | Height for UI elements -->
|
||||
<dimen name="height_compact_progress">2dp</dimen>
|
||||
<dimen name="height_minimal_btn">28dp</dimen>
|
||||
|
||||
<!-- Width Namespace | Width for UI elements -->
|
||||
<dimen name="width_track_number">32dp</dimen>
|
||||
|
|
|
@ -126,6 +126,7 @@
|
|||
|
||||
<string name="description_error">Error</string>
|
||||
<string name="description_auxio_icon">Auxio icon</string>
|
||||
<string name="description_placeholder_cover">Album cover</string>
|
||||
<string name="description_album_cover">Album Cover for %s</string>
|
||||
<string name="description_artist_image">Artist Image for %s</string>
|
||||
<string name="description_genre_image">Genre Image for %s</string>
|
||||
|
|
Loading…
Reference in a new issue