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_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>> {
override suspend fun fetch(
pool: BitmapPool,

View file

@ -4,8 +4,12 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.os.Build
import android.os.IBinder
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 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
// persistence if the app closes.
// A Service that manages the single ExoPlayer instance and manages the system-side
// aspects of playback.
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
// TODO: Use the ExoPlayer queue functionality [To an extent]? Could make things faster.
private val player: SimpleExoPlayer by lazy {
@ -51,14 +57,13 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var mediaSession: MediaSessionCompat
private lateinit var systemReceiver: SystemEventReceiver
private val serviceJob = Job()
private val serviceScope = CoroutineScope(
serviceJob + Dispatchers.Main
)
private var isForeground = false
private lateinit var notification: Notification
// --- SERVICE OVERRIDES ---
@ -87,6 +92,17 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
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)
}
@ -94,6 +110,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
super.onDestroy()
stopForeground(true)
unregisterReceiver(systemReceiver)
// Release everything that could cause a memory leak if left around
player.release()
@ -138,6 +155,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
} else {
player.pause()
// Be a polite service and stop being foreground if nothing is playing.
stopForeground(false)
}
}
@ -176,24 +194,29 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
true
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playbackManager.next()
true
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playbackManager.prev()
true
}
// TODO: Implement the other callbacks for
// CLOSE/STOP & REWIND
KeyEvent.KEYCODE_MEDIA_REWIND -> {
player.seekTo(0)
true
}
KeyEvent.KEYCODE_MEDIA_STOP, KeyEvent.KEYCODE_MEDIA_CLOSE -> {
stopSelf()
true
}
else -> false
}
}
@ -229,4 +252,50 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
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]
// 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 {
// Playback
private var mSong: Song? = null
@ -259,7 +261,7 @@ class PlaybackStateManager {
// Generate a new shuffled queue.
private fun genShuffle(keepSong: Boolean) {
// 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.
val newSeed = Random.Default.nextLong()