Refactor notification management
Remove PlaybackNotificationHolder and replace it with a util file that is easier to work with.
This commit is contained in:
parent
08bd0ece3a
commit
5da3fa866b
6 changed files with 231 additions and 230 deletions
|
@ -16,6 +16,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustPan">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -26,7 +27,8 @@
|
||||||
</activity>
|
</activity>
|
||||||
<service
|
<service
|
||||||
android:name=".playback.PlaybackService"
|
android:name=".playback.PlaybackService"
|
||||||
android:icon="@drawable/ic_launcher_foreground"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:foregroundServiceType="mediaPlayback"
|
android:foregroundServiceType="mediaPlayback"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|
|
@ -14,6 +14,7 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
||||||
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||||
|
|
||||||
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
|
@ -27,6 +28,13 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
|
// If the music was already loaded, then don't do it again.
|
||||||
|
if (MusicStore.getInstance().loaded) {
|
||||||
|
findNavController().navigate(
|
||||||
|
LoadingFragmentDirections.actionToMain()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val binding = FragmentLoadingBinding.inflate(inflater)
|
val binding = FragmentLoadingBinding.inflate(inflater)
|
||||||
|
|
||||||
// Set up the permission launcher, as its disallowed outside of onCreate.
|
// Set up the permission launcher, as its disallowed outside of onCreate.
|
||||||
|
|
|
@ -22,6 +22,9 @@ class MusicStore private constructor() {
|
||||||
private var mSongs = listOf<Song>()
|
private var mSongs = listOf<Song>()
|
||||||
val songs: List<Song> get() = mSongs
|
val songs: List<Song> get() = mSongs
|
||||||
|
|
||||||
|
var loaded = false
|
||||||
|
private set
|
||||||
|
|
||||||
// Load/Sort the entire library.
|
// Load/Sort the entire library.
|
||||||
// ONLY CALL THIS FROM AN IO THREAD.
|
// ONLY CALL THIS FROM AN IO THREAD.
|
||||||
fun load(app: Application): MusicLoaderResponse {
|
fun load(app: Application): MusicLoaderResponse {
|
||||||
|
@ -69,6 +72,10 @@ class MusicStore private constructor() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loader.response == MusicLoaderResponse.DONE) {
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
return loader.response
|
return loader.response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
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.core.app.NotificationCompat
|
||||||
|
import androidx.media.app.NotificationCompat.MediaStyle
|
||||||
|
import org.oxycblt.auxio.MainActivity
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.coil.getBitmap
|
||||||
|
import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
|
||||||
|
object NotificationUtils {
|
||||||
|
const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
||||||
|
const val NOTIFICATION_ID = 0xA0A0
|
||||||
|
const val REQUEST_CODE = 0xA0C0
|
||||||
|
|
||||||
|
const val ACTION_LOOP = "ACTION_AUXIO_LOOP"
|
||||||
|
const val ACTION_SKIP_PREV = "ACTION_AUXIO_SKIP_PREV"
|
||||||
|
const val ACTION_PLAY_PAUSE = "ACTION_AUXIO_PLAY_PAUSE"
|
||||||
|
const val ACTION_SKIP_NEXT = "ACTION_AUXIO_SKIP_NEXT"
|
||||||
|
const val ACTION_SHUFFLE = "ACTION_AUXIO_SHUFFLE"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NotificationManager.createMediaNotification(
|
||||||
|
context: Context,
|
||||||
|
mediaSession: MediaSessionCompat
|
||||||
|
): NotificationCompat.Builder {
|
||||||
|
|
||||||
|
// Create a notification channel if required
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
NotificationUtils.CHANNEL_ID,
|
||||||
|
context.getString(R.string.label_notification_playback),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mainIntent = PendingIntent.getActivity(
|
||||||
|
context, NotificationUtils.REQUEST_CODE,
|
||||||
|
Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: It would be cool if the notification intent took you to the now playing screen.
|
||||||
|
return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_song)
|
||||||
|
.setStyle(
|
||||||
|
MediaStyle()
|
||||||
|
.setMediaSession(mediaSession.sessionToken)
|
||||||
|
.setShowActionsInCompactView(1, 2, 3)
|
||||||
|
)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
.setChannelId(NotificationUtils.CHANNEL_ID)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setTicker(context.getString(R.string.title_playback))
|
||||||
|
.addAction(newAction(NotificationUtils.ACTION_LOOP, context))
|
||||||
|
.addAction(newAction(NotificationUtils.ACTION_SKIP_PREV, context))
|
||||||
|
.addAction(newAction(NotificationUtils.ACTION_PLAY_PAUSE, context))
|
||||||
|
.addAction(newAction(NotificationUtils.ACTION_SKIP_NEXT, context))
|
||||||
|
.addAction(newAction(NotificationUtils.ACTION_SHUFFLE, context))
|
||||||
|
.setSubText(context.getString(R.string.title_playback))
|
||||||
|
.setContentIntent(mainIntent)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NotificationCompat.Builder.setMetadata(song: Song, context: Context, onDone: () -> Unit) {
|
||||||
|
setContentTitle(song.name)
|
||||||
|
setContentText(
|
||||||
|
song.album.artist.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
getBitmap(song, context) {
|
||||||
|
setLargeIcon(it)
|
||||||
|
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
fun NotificationCompat.Builder.updateLoop(context: Context) {
|
||||||
|
mActions[0] = newAction(NotificationUtils.ACTION_LOOP, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
fun NotificationCompat.Builder.updatePlaying(context: Context) {
|
||||||
|
mActions[2] = newAction(NotificationUtils.ACTION_PLAY_PAUSE, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
fun NotificationCompat.Builder.updateShuffle(context: Context) {
|
||||||
|
mActions[4] = newAction(NotificationUtils.ACTION_SHUFFLE, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newAction(action: String, context: Context): NotificationCompat.Action {
|
||||||
|
val playbackManager = PlaybackStateManager.getInstance()
|
||||||
|
|
||||||
|
val drawable = when (action) {
|
||||||
|
NotificationUtils.ACTION_LOOP -> {
|
||||||
|
when (playbackManager.loopMode) {
|
||||||
|
LoopMode.NONE -> R.drawable.ic_loop_disabled
|
||||||
|
LoopMode.ONCE -> R.drawable.ic_loop_one
|
||||||
|
LoopMode.INFINITE -> R.drawable.ic_loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationUtils.ACTION_SKIP_PREV -> {
|
||||||
|
R.drawable.ic_skip_prev
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationUtils.ACTION_PLAY_PAUSE -> {
|
||||||
|
if (playbackManager.isPlaying) {
|
||||||
|
R.drawable.ic_pause
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_play
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationUtils.ACTION_SKIP_NEXT -> {
|
||||||
|
R.drawable.ic_skip_next
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationUtils.ACTION_SHUFFLE -> {
|
||||||
|
if (playbackManager.isShuffling) {
|
||||||
|
R.drawable.ic_shuffle
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_shuffle_disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> R.drawable.ic_play
|
||||||
|
}
|
||||||
|
|
||||||
|
return NotificationCompat.Action.Builder(
|
||||||
|
drawable, action, newPlaybackIntent(action, context)
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newPlaybackIntent(action: String, context: Context): PendingIntent {
|
||||||
|
return PendingIntent.getBroadcast(
|
||||||
|
context, NotificationUtils.REQUEST_CODE, Intent(action), PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,208 +0,0 @@
|
||||||
package org.oxycblt.auxio.playback
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ServiceInfo
|
|
||||||
import android.os.Build
|
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.media.app.NotificationCompat.MediaStyle
|
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.music.Song
|
|
||||||
import org.oxycblt.auxio.music.coil.getBitmap
|
|
||||||
import org.oxycblt.auxio.playback.state.LoopMode
|
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
|
||||||
|
|
||||||
// Holder for the playback notification, should only be used by PlaybackService.
|
|
||||||
// TODO: You really need to rewrite this class. Christ.
|
|
||||||
// TODO: Disable skip prev/next buttons when you cant do those actions
|
|
||||||
// TODO: Implement a way to exit the notification
|
|
||||||
class PlaybackNotificationHolder {
|
|
||||||
private lateinit var mNotification: Notification
|
|
||||||
|
|
||||||
private lateinit var notificationManager: NotificationManager
|
|
||||||
private lateinit var baseNotification: NotificationCompat.Builder
|
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
|
||||||
|
|
||||||
private var isForeground = false
|
|
||||||
|
|
||||||
fun init(context: Context, session: MediaSessionCompat, playbackService: PlaybackService) {
|
|
||||||
// Never run if the notification has already been created
|
|
||||||
if (!::mNotification.isInitialized) {
|
|
||||||
notificationManager =
|
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
|
|
||||||
// Create a notification channel if required
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
context.getString(R.string.label_notification_playback),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
)
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseNotification = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_song)
|
|
||||||
.setStyle(
|
|
||||||
MediaStyle()
|
|
||||||
.setMediaSession(session.sessionToken)
|
|
||||||
.setShowActionsInCompactView(1, 2, 3)
|
|
||||||
)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
||||||
.setChannelId(CHANNEL_ID)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setTicker(playbackService.getString(R.string.title_playback))
|
|
||||||
.addAction(createAction(ACTION_LOOP, playbackService))
|
|
||||||
.addAction(createAction(ACTION_SKIP_PREV, playbackService))
|
|
||||||
.addAction(createAction(ACTION_PLAY_PAUSE, playbackService))
|
|
||||||
.addAction(createAction(ACTION_SKIP_NEXT, playbackService))
|
|
||||||
.addAction(createAction(ACTION_SHUFFLE, playbackService))
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
|
|
||||||
mNotification = baseNotification.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMetadata(song: Song, playbackService: PlaybackService) {
|
|
||||||
// Set the basic metadata since MediaStyle wont do it yourself.
|
|
||||||
// Fun Fact: The documentation still says that MediaStyle will handle metadata changes
|
|
||||||
// from MediaSession, even though it doesn't. Its been 6 years. Fun.
|
|
||||||
baseNotification
|
|
||||||
.setContentTitle(song.name)
|
|
||||||
.setContentText(
|
|
||||||
playbackService.getString(
|
|
||||||
R.string.format_info,
|
|
||||||
song.album.artist.name,
|
|
||||||
song.album.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
getBitmap(song, playbackService) {
|
|
||||||
baseNotification.setLargeIcon(it)
|
|
||||||
|
|
||||||
startForegroundOrNotify(playbackService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun updatePlaying(playbackService: PlaybackService) {
|
|
||||||
baseNotification.mActions[2] = createAction(ACTION_PLAY_PAUSE, playbackService)
|
|
||||||
|
|
||||||
Log.d(this::class.simpleName, baseNotification.mActions[1].iconCompat?.resId.toString())
|
|
||||||
|
|
||||||
startForegroundOrNotify(playbackService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun updateLoop(playbackService: PlaybackService) {
|
|
||||||
baseNotification.mActions[0] = createAction(ACTION_LOOP, playbackService)
|
|
||||||
|
|
||||||
startForegroundOrNotify(playbackService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun updateShuffle(playbackService: PlaybackService) {
|
|
||||||
baseNotification.mActions[4] = createAction(ACTION_SHUFFLE, playbackService)
|
|
||||||
|
|
||||||
startForegroundOrNotify(playbackService)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop(playbackService: PlaybackService) {
|
|
||||||
playbackService.stopForeground(true)
|
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
|
||||||
|
|
||||||
isForeground = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createAction(action: String, playbackService: PlaybackService): NotificationCompat.Action {
|
|
||||||
val drawable = when (action) {
|
|
||||||
ACTION_LOOP -> {
|
|
||||||
when (playbackManager.loopMode) {
|
|
||||||
LoopMode.NONE -> R.drawable.ic_loop_disabled
|
|
||||||
LoopMode.ONCE -> R.drawable.ic_loop_one
|
|
||||||
LoopMode.INFINITE -> R.drawable.ic_loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTION_SKIP_PREV -> {
|
|
||||||
R.drawable.ic_skip_prev
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTION_PLAY_PAUSE -> {
|
|
||||||
if (playbackManager.isPlaying) {
|
|
||||||
R.drawable.ic_pause
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_play
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTION_SKIP_NEXT -> {
|
|
||||||
R.drawable.ic_skip_next
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTION_SHUFFLE -> {
|
|
||||||
if (playbackManager.isShuffling) {
|
|
||||||
R.drawable.ic_shuffle
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_shuffle_disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> R.drawable.ic_play
|
|
||||||
}
|
|
||||||
|
|
||||||
return NotificationCompat.Action.Builder(
|
|
||||||
drawable, action, createPlaybackAction(action, playbackService)
|
|
||||||
).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPlaybackAction(action: String, playbackService: PlaybackService): PendingIntent {
|
|
||||||
val intent = Intent()
|
|
||||||
intent.action = action
|
|
||||||
|
|
||||||
return PendingIntent.getBroadcast(
|
|
||||||
playbackService,
|
|
||||||
REQUEST_CODE,
|
|
||||||
intent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startForegroundOrNotify(playbackService: PlaybackService) {
|
|
||||||
mNotification = baseNotification.build()
|
|
||||||
|
|
||||||
// Start the service in the foreground if haven't already.
|
|
||||||
if (!isForeground) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
playbackService.startForeground(
|
|
||||||
NOTIFICATION_ID, mNotification,
|
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
playbackService.startForeground(NOTIFICATION_ID, mNotification)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, mNotification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
|
||||||
const val NOTIFICATION_ID = 0xA0A0
|
|
||||||
const val REQUEST_CODE = 0xA0C0
|
|
||||||
|
|
||||||
const val ACTION_LOOP = "ACTION_AUXIO_LOOP"
|
|
||||||
const val ACTION_SKIP_PREV = "ACTION_AUXIO_SKIP_PREV"
|
|
||||||
const val ACTION_PLAY_PAUSE = "ACTION_AUXIO_PLAY_PAUSE"
|
|
||||||
const val ACTION_SKIP_NEXT = "ACTION_AUXIO_SKIP_NEXT"
|
|
||||||
const val ACTION_SHUFFLE = "ACTION_AUXIO_SHUFFLE"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,13 @@
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.content.BroadcastReceiver
|
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.content.IntentFilter
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
@ -14,6 +16,7 @@ import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException
|
import com.google.android.exoplayer2.ExoPlaybackException
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
|
@ -47,9 +50,12 @@ 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 lateinit var systemReceiver: SystemEventReceiver
|
||||||
private lateinit var notificationHolder: PlaybackNotificationHolder
|
|
||||||
|
private lateinit var notificationManager: NotificationManager
|
||||||
|
private lateinit var notification: NotificationCompat.Builder
|
||||||
|
|
||||||
private var changeIsFromAudioFocus = true
|
private var changeIsFromAudioFocus = true
|
||||||
|
private var isForeground = false
|
||||||
|
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val serviceScope = CoroutineScope(
|
private val serviceScope = CoroutineScope(
|
||||||
|
@ -103,11 +109,12 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
// Set up callback for system events
|
// Set up callback for system events
|
||||||
systemReceiver = SystemEventReceiver()
|
systemReceiver = SystemEventReceiver()
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(PlaybackNotificationHolder.ACTION_LOOP)
|
addAction(NotificationUtils.ACTION_LOOP)
|
||||||
addAction(PlaybackNotificationHolder.ACTION_SKIP_PREV)
|
addAction(NotificationUtils.ACTION_SKIP_PREV)
|
||||||
addAction(PlaybackNotificationHolder.ACTION_PLAY_PAUSE)
|
addAction(NotificationUtils.ACTION_PLAY_PAUSE)
|
||||||
addAction(PlaybackNotificationHolder.ACTION_SKIP_NEXT)
|
addAction(NotificationUtils.ACTION_SKIP_NEXT)
|
||||||
addAction(PlaybackNotificationHolder.ACTION_SHUFFLE)
|
addAction(NotificationUtils.ACTION_SHUFFLE)
|
||||||
|
|
||||||
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||||
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||||
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
||||||
|
@ -118,9 +125,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
|
|
||||||
// --- NOTIFICATION SETUP ---
|
// --- NOTIFICATION SETUP ---
|
||||||
|
|
||||||
notificationHolder = PlaybackNotificationHolder()
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
notificationHolder.init(applicationContext, mediaSession, this)
|
notification = notificationManager.createMediaNotification(this, mediaSession)
|
||||||
|
|
||||||
// --- PLAYBACKSTATEMANAGER SETUP ---
|
// --- PLAYBACKSTATEMANAGER SETUP ---
|
||||||
|
|
||||||
|
@ -134,7 +141,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
notificationHolder.stop(this)
|
stopForegroundAndNotification()
|
||||||
unregisterReceiver(systemReceiver)
|
unregisterReceiver(systemReceiver)
|
||||||
|
|
||||||
// Release everything that could cause a memory leak if left around
|
// Release everything that could cause a memory leak if left around
|
||||||
|
@ -199,14 +206,16 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
uploadMetadataToSession(it)
|
uploadMetadataToSession(it)
|
||||||
notificationHolder.setMetadata(playbackManager.song!!, this)
|
notification.setMetadata(playbackManager.song!!, this) {
|
||||||
|
startForegroundOrNotify()
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop playing/the notification if there's nothing to play.
|
// Stop playing/the notification if there's nothing to play.
|
||||||
player.stop()
|
player.stop()
|
||||||
notificationHolder.stop(this)
|
stopForegroundAndNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayingUpdate(isPlaying: Boolean) {
|
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||||
|
@ -214,18 +223,23 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
|
|
||||||
if (isPlaying && !player.isPlaying) {
|
if (isPlaying && !player.isPlaying) {
|
||||||
player.play()
|
player.play()
|
||||||
notificationHolder.updatePlaying(this)
|
notification.updatePlaying(this)
|
||||||
|
startForegroundOrNotify()
|
||||||
|
|
||||||
startPollingPosition()
|
startPollingPosition()
|
||||||
} else {
|
} else {
|
||||||
player.pause()
|
player.pause()
|
||||||
notificationHolder.updatePlaying(this)
|
|
||||||
|
notification.updatePlaying(this)
|
||||||
|
startForegroundOrNotify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShuffleUpdate(isShuffling: Boolean) {
|
override fun onShuffleUpdate(isShuffling: Boolean) {
|
||||||
changeIsFromAudioFocus = false
|
changeIsFromAudioFocus = false
|
||||||
|
|
||||||
notificationHolder.updateShuffle(this)
|
notification.updateShuffle(this)
|
||||||
|
startForegroundOrNotify()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoopUpdate(mode: LoopMode) {
|
override fun onLoopUpdate(mode: LoopMode) {
|
||||||
|
@ -240,7 +254,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationHolder.updateLoop(this)
|
notification.updateLoop(this)
|
||||||
|
startForegroundOrNotify()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSeekConfirm(position: Long) {
|
override fun onSeekConfirm(position: Long) {
|
||||||
|
@ -258,7 +273,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.seekTo(playbackManager.position)
|
player.seekTo(playbackManager.position)
|
||||||
|
|
||||||
notificationHolder.setMetadata(it, this)
|
notification.setMetadata(it, this) {
|
||||||
|
startForegroundOrNotify()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,6 +314,29 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startForegroundOrNotify() {
|
||||||
|
// Start the service in the foreground if haven't already.
|
||||||
|
if (!isForeground) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
NotificationUtils.NOTIFICATION_ID, notification.build(),
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(NotificationUtils.NOTIFICATION_ID, notification.build())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notificationManager.notify(NotificationUtils.NOTIFICATION_ID, notification.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopForegroundAndNotification() {
|
||||||
|
stopForeground(true)
|
||||||
|
notificationManager.cancel(NotificationUtils.NOTIFICATION_ID)
|
||||||
|
|
||||||
|
isForeground = false
|
||||||
|
}
|
||||||
|
|
||||||
// Handle a media button event.
|
// Handle a media button event.
|
||||||
private fun handleMediaButtonEvent(event: Intent): Boolean {
|
private fun handleMediaButtonEvent(event: Intent): Boolean {
|
||||||
val item = event
|
val item = event
|
||||||
|
@ -344,13 +384,13 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
|
|
||||||
action?.let {
|
action?.let {
|
||||||
when (it) {
|
when (it) {
|
||||||
PlaybackNotificationHolder.ACTION_LOOP ->
|
NotificationUtils.ACTION_LOOP ->
|
||||||
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
||||||
PlaybackNotificationHolder.ACTION_SKIP_PREV -> playbackManager.prev()
|
NotificationUtils.ACTION_SKIP_PREV -> playbackManager.prev()
|
||||||
PlaybackNotificationHolder.ACTION_PLAY_PAUSE ->
|
NotificationUtils.ACTION_PLAY_PAUSE ->
|
||||||
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
|
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
|
||||||
PlaybackNotificationHolder.ACTION_SKIP_NEXT -> playbackManager.next()
|
NotificationUtils.ACTION_SKIP_NEXT -> playbackManager.next()
|
||||||
PlaybackNotificationHolder.ACTION_SHUFFLE ->
|
NotificationUtils.ACTION_SHUFFLE ->
|
||||||
playbackManager.setShuffleStatus(!playbackManager.isShuffling)
|
playbackManager.setShuffleStatus(!playbackManager.isShuffling)
|
||||||
|
|
||||||
BluetoothDevice.ACTION_ACL_CONNECTED -> resume()
|
BluetoothDevice.ACTION_ACL_CONNECTED -> resume()
|
||||||
|
|
Loading…
Reference in a new issue