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