Extend Headset Functionality
Add callbacks for when rewind/close buttons are pressed on a headset, and when headsets are connected/disconnected.
This commit is contained in:
parent
6fc034e376
commit
ce96dd6e94
3 changed files with 82 additions and 11 deletions
|
@ -22,7 +22,7 @@ import java.io.InputStream
|
||||||
const val MOSAIC_BITMAP_SIZE = 512
|
const val MOSAIC_BITMAP_SIZE = 512
|
||||||
const val MOSAIC_BITMAP_INCREMENT = 256
|
const val MOSAIC_BITMAP_INCREMENT = 256
|
||||||
|
|
||||||
// A Fetcher that takes multiple cover uris and turns them into a NxN mosaic image.
|
// A Fetcher that takes multiple cover uris and turns them into a 2x2 mosaic image.
|
||||||
class MosaicFetcher(private val context: Context) : Fetcher<List<Uri>> {
|
class MosaicFetcher(private val context: Context) : Fetcher<List<Uri>> {
|
||||||
override suspend fun fetch(
|
override suspend fun fetch(
|
||||||
pool: BitmapPool,
|
pool: BitmapPool,
|
||||||
|
|
|
@ -4,8 +4,12 @@ import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
@ -35,9 +39,11 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
|
||||||
private const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
private const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
||||||
private const val NOTIF_ID = 0xA0A0
|
private const val NOTIF_ID = 0xA0A0
|
||||||
|
private const val CONNECTED = 1
|
||||||
|
private const val DISCONNECTED = 0
|
||||||
|
|
||||||
// A Service that manages the single ExoPlayer instance and [attempts] to keep
|
// A Service that manages the single ExoPlayer instance and manages the system-side
|
||||||
// persistence if the app closes.
|
// aspects of playback.
|
||||||
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
// TODO: Use the ExoPlayer queue functionality [To an extent]? Could make things faster.
|
// TODO: Use the ExoPlayer queue functionality [To an extent]? Could make things faster.
|
||||||
private val player: SimpleExoPlayer by lazy {
|
private val player: SimpleExoPlayer by lazy {
|
||||||
|
@ -51,14 +57,13 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
|
private lateinit var systemReceiver: SystemEventReceiver
|
||||||
|
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val serviceScope = CoroutineScope(
|
private val serviceScope = CoroutineScope(
|
||||||
serviceJob + Dispatchers.Main
|
serviceJob + Dispatchers.Main
|
||||||
)
|
)
|
||||||
|
|
||||||
private var isForeground = false
|
|
||||||
|
|
||||||
private lateinit var notification: Notification
|
private lateinit var notification: Notification
|
||||||
|
|
||||||
// --- SERVICE OVERRIDES ---
|
// --- SERVICE OVERRIDES ---
|
||||||
|
@ -87,6 +92,17 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
|
|
||||||
notification = createNotification()
|
notification = createNotification()
|
||||||
|
|
||||||
|
// Set up callback for system events
|
||||||
|
systemReceiver = SystemEventReceiver()
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||||
|
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||||
|
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
||||||
|
addAction(Intent.ACTION_HEADSET_PLUG)
|
||||||
|
|
||||||
|
registerReceiver(systemReceiver, this)
|
||||||
|
}
|
||||||
|
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,6 +110,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
|
unregisterReceiver(systemReceiver)
|
||||||
|
|
||||||
// Release everything that could cause a memory leak if left around
|
// Release everything that could cause a memory leak if left around
|
||||||
player.release()
|
player.release()
|
||||||
|
@ -138,6 +155,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
} else {
|
} else {
|
||||||
player.pause()
|
player.pause()
|
||||||
|
|
||||||
|
// Be a polite service and stop being foreground if nothing is playing.
|
||||||
stopForeground(false)
|
stopForeground(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,24 +194,29 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
|
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
|
||||||
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
|
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
||||||
playbackManager.prev()
|
playbackManager.prev()
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement the other callbacks for
|
KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
// CLOSE/STOP & REWIND
|
player.seekTo(0)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP, KeyEvent.KEYCODE_MEDIA_CLOSE -> {
|
||||||
|
stopSelf()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,4 +252,50 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
|
|
||||||
return notif
|
return notif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broadcast Receiver for receiving system events [E.G Headphones connecte/disconnected
|
||||||
|
inner class SystemEventReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val action = intent.action
|
||||||
|
|
||||||
|
action?.let {
|
||||||
|
when (it) {
|
||||||
|
BluetoothDevice.ACTION_ACL_CONNECTED -> resume()
|
||||||
|
BluetoothDevice.ACTION_ACL_DISCONNECTED -> pause()
|
||||||
|
|
||||||
|
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
|
||||||
|
when (intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)) {
|
||||||
|
AudioManager.SCO_AUDIO_STATE_CONNECTED -> resume()
|
||||||
|
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pause()
|
||||||
|
|
||||||
|
Intent.ACTION_HEADSET_PLUG -> {
|
||||||
|
when (intent.getIntExtra("state", -1)) {
|
||||||
|
CONNECTED -> resume()
|
||||||
|
DISCONNECTED -> pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resume() {
|
||||||
|
if (playbackManager.song != null) {
|
||||||
|
Log.d(this::class.simpleName, "Device connected, resuming...")
|
||||||
|
|
||||||
|
playbackManager.setPlayingStatus(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pause() {
|
||||||
|
if (playbackManager.song != null) {
|
||||||
|
Log.d(this::class.simpleName, "Device disconnected, pausing...")
|
||||||
|
|
||||||
|
playbackManager.setPlayingStatus(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import kotlin.random.Random
|
||||||
|
|
||||||
// The manager of the current playback state [Current Song, Queue, Shuffling]
|
// The manager of the current playback state [Current Song, Queue, Shuffling]
|
||||||
// Never use this for ANYTHING UI related, that's what PlaybackViewModel is for.
|
// Never use this for ANYTHING UI related, that's what PlaybackViewModel is for.
|
||||||
|
// Yes, I know MediaSessionCompat and friends exist, but I like having full control over the
|
||||||
|
// playback state instead of dealing with android's likely buggy code.
|
||||||
class PlaybackStateManager {
|
class PlaybackStateManager {
|
||||||
// Playback
|
// Playback
|
||||||
private var mSong: Song? = null
|
private var mSong: Song? = null
|
||||||
|
@ -259,7 +261,7 @@ class PlaybackStateManager {
|
||||||
// Generate a new shuffled queue.
|
// Generate a new shuffled queue.
|
||||||
private fun genShuffle(keepSong: Boolean) {
|
private fun genShuffle(keepSong: Boolean) {
|
||||||
// Take a random seed and then shuffle the current queue based off of that.
|
// Take a random seed and then shuffle the current queue based off of that.
|
||||||
// This seed will be saved in a bundle if the app closes, so that the shuffle mode
|
// This seed will be saved in a database, so that the shuffle mode
|
||||||
// can be restored when its started again.
|
// can be restored when its started again.
|
||||||
val newSeed = Random.Default.nextLong()
|
val newSeed = Random.Default.nextLong()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue