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:
OxygenCobalt 2020-10-30 12:22:32 -06:00
parent 6fc034e376
commit ce96dd6e94
3 changed files with 82 additions and 11 deletions

View file

@ -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,

View file

@ -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)
}
}
}
} }

View file

@ -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()