Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: controller #45

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
Expand Down Expand Up @@ -94,6 +94,11 @@
</intent-filter>
</service>

<service
android:name=".extensions.ControllerExtensionService"
android:exported="false"
android:foregroundServiceType="mediaPlayback"/>

<receiver
android:name="androidx.media3.session.MediaButtonReceiver"
android:exported="true">
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import dagger.hilt.android.AndroidEntryPoint
import dev.brahmkshatriya.echo.common.MusicExtension
import dev.brahmkshatriya.echo.common.clients.CloseableClient
import dev.brahmkshatriya.echo.common.models.Streamable
import dev.brahmkshatriya.echo.extensions.ExtensionLoader
import dev.brahmkshatriya.echo.playback.Current
import dev.brahmkshatriya.echo.playback.PlayerCallback
import dev.brahmkshatriya.echo.playback.ResumptionUtils
import dev.brahmkshatriya.echo.playback.listeners.AudioFocusListener
import dev.brahmkshatriya.echo.playback.listeners.ControllerListener
import dev.brahmkshatriya.echo.playback.listeners.PlayerEventListener
import dev.brahmkshatriya.echo.playback.listeners.Radio
import dev.brahmkshatriya.echo.playback.listeners.TrackingListener
Expand All @@ -40,6 +42,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject

@AndroidEntryPoint
@UnstableApi
class PlayerService : MediaLibraryService() {
private var mediaSession: MediaLibrarySession? = null
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
Expand All @@ -56,6 +59,9 @@ class PlayerService : MediaLibraryService() {
@Inject
lateinit var stateFlow: MutableStateFlow<Radio.State>

@Inject
lateinit var closeableFlow: MutableStateFlow<List<CloseableClient>?>

@Inject
lateinit var settings: SharedPreferences

Expand All @@ -72,6 +78,8 @@ class PlayerService : MediaLibraryService() {
@Inject
lateinit var fftAudioProcessor: FFTAudioProcessor

lateinit var controllerListener: ControllerListener

private val scope = CoroutineScope(Dispatchers.Main)

@OptIn(UnstableApi::class)
Expand All @@ -98,6 +106,7 @@ class PlayerService : MediaLibraryService() {
.setWakeMode(C.WAKE_MODE_NETWORK)
.setSkipSilenceEnabled(settings.getBoolean(SKIP_SILENCE, true))
.setAudioAttributes(audioAttributes, true)
.setDeviceVolumeControlEnabled(true)
.build()
.also {
it.trackSelectionParameters = it.trackSelectionParameters
Expand All @@ -115,6 +124,7 @@ class PlayerService : MediaLibraryService() {
val extListFlow = extensionLoader.extensions
val extFlow = extensionLoader.current
val trackerList = extensionLoader.trackers
val controllerList = extensionLoader.controllers

val exoPlayer = createExoplayer(extListFlow)
exoPlayer.prepare()
Expand Down Expand Up @@ -149,6 +159,14 @@ class PlayerService : MediaLibraryService() {
exoPlayer.addListener(
TrackingListener(exoPlayer, scope, extListFlow, trackerList, throwFlow)
)
controllerListener = ControllerListener(
exoPlayer,
this,
scope,
controllerList,
throwFlow
)
exoPlayer.addListener(controllerListener)
settings.registerOnSharedPreferenceChangeListener { prefs, key ->
when (key) {
SKIP_SILENCE -> exoPlayer.skipSilenceEnabled = prefs.getBoolean(key, true)
Expand All @@ -168,6 +186,14 @@ class PlayerService : MediaLibraryService() {
release()
mediaSession = null
}
closeableFlow.value?.forEach {
try {
it.close()
} catch (e: Exception) {
throwFlow.tryEmit(e)
}
}
if (::controllerListener.isInitialized) controllerListener.onDestroy()
super.onDestroy()
}

Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dev.brahmkshatriya.echo.EchoDatabase
import dev.brahmkshatriya.echo.common.ControllerExtension
import dev.brahmkshatriya.echo.common.LyricsExtension
import dev.brahmkshatriya.echo.common.MusicExtension
import dev.brahmkshatriya.echo.common.TrackerExtension
import dev.brahmkshatriya.echo.common.clients.CloseableClient
import dev.brahmkshatriya.echo.db.models.UserEntity
import dev.brahmkshatriya.echo.extensions.ExtensionLoader
import dev.brahmkshatriya.echo.offline.OfflineExtension
import dev.brahmkshatriya.echo.viewmodels.SnackBar
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Singleton
Expand Down Expand Up @@ -50,36 +53,50 @@ class ExtensionModule {
@Singleton
fun provideTrackerListFlow() = MutableStateFlow<List<TrackerExtension>?>(null)

@Provides
@Singleton
fun provideControllerListFlow() = MutableStateFlow<List<ControllerExtension>?>(null)

@Provides
@Singleton
fun provideCloseableClientListFlow() = MutableStateFlow<List<CloseableClient>?>(null)

@Provides
@Singleton
fun provideExtensionLoader(
context: Application,
throwableFlow: MutableSharedFlow<Throwable>,
mutableMessageFlow: MutableSharedFlow<SnackBar.Message>,
database: EchoDatabase,
settings: SharedPreferences,
refresher: MutableSharedFlow<Boolean>,
userFlow: MutableSharedFlow<UserEntity?>,
offlineExtension: OfflineExtension,
extensionListFlow: MutableStateFlow<List<MusicExtension>?>,
trackerListFlow: MutableStateFlow<List<TrackerExtension>?>,
controllerListFlow: MutableStateFlow<List<ControllerExtension>?>,
lyricsListFlow: MutableStateFlow<List<LyricsExtension>?>,
extensionFlow: MutableStateFlow<MusicExtension?>,
closeableClientListFlow: MutableStateFlow<List<CloseableClient>?>,
) = run {
val extensionDao = database.extensionDao()
val userDao = database.userDao()
ExtensionLoader(
context,
offlineExtension,
throwableFlow,
mutableMessageFlow,
extensionDao,
userDao,
settings,
refresher,
userFlow,
extensionListFlow,
trackerListFlow,
controllerListFlow,
lyricsListFlow,
extensionFlow,
closeableClientListFlow
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package dev.brahmkshatriya.echo.extensions

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import dev.brahmkshatriya.echo.R
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.Player
import dev.brahmkshatriya.echo.common.clients.ControllerClient.RepeatMode

@UnstableApi
class ControllerExtensionService : Service() {
private val binder = LocalBinder()
private var player: Player? = null

inner class LocalBinder : Binder() {
fun getService(): ControllerExtensionService = this@ControllerExtensionService
}

override fun onCreate() {
super.onCreate()
createNotificationChannel()
}

override fun onBind(intent: Intent): IBinder {
return binder
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIFICATION_ID, createNotification())
return START_STICKY
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
getString(R.string.media_playback_controller),
NotificationManager.IMPORTANCE_LOW
).apply {
description = getString(R.string.media_playback_controller_description)
setSound(null, null)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}

private fun createNotification(): Notification {
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle(getString(R.string.media_playback_controller))
.setContentText(getString(R.string.media_playback_controller_running))
.setSmallIcon(android.R.drawable.ic_media_play)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
}

fun setPlayer(exoPlayer: Player) {
player = exoPlayer
}

fun play() {
player?.play()
}

fun pause() {
player?.pause()
}

fun seekToNext() {
player?.seekToNextMediaItem()
}

fun seekToPrevious() {
player?.seekToPreviousMediaItem()
}

fun seekTo(position: Long) {
player?.seekTo(position)
}

fun seekToMediaItem(index: Int) {
player?.seekTo(index, 0)
}

fun moveMediaItem(fromIndex: Int, toIndex: Int) {
player?.moveMediaItem(fromIndex, toIndex)
}

fun removeMediaItem(index: Int) {
player?.removeMediaItem(index)
}

fun setShuffleMode(enabled: Boolean) {
player?.shuffleModeEnabled = enabled
}

fun setRepeatMode(repeatMode: RepeatMode) {
player?.repeatMode = repeatMode.ordinal
}

override fun onDestroy() {
stopForeground(STOP_FOREGROUND_DETACH)
player = null
super.onDestroy()
}

companion object {
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_CHANNEL_ID = "media_playback_channel"
const val ACTION_START_SERVICE = "action.START_SERVICE"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package dev.brahmkshatriya.echo.extensions

import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.IBinder
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import dev.brahmkshatriya.echo.common.clients.ControllerClient.RepeatMode


@UnstableApi
class ControllerServiceHelper(private val parentService: Service) {
private var mediaService: ControllerExtensionService? = null
private var isServiceBound = false
private var player: Player? = null

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as ControllerExtensionService.LocalBinder
mediaService = binder.getService()
isServiceBound = true
player?.let { mediaService?.setPlayer(it) }
}

override fun onServiceDisconnected(name: ComponentName?) {
mediaService = null
isServiceBound = false
}
}

fun startService(player: Player) {
this.player = player

val intent = Intent(parentService, ControllerExtensionService::class.java).apply {
action = ControllerExtensionService.ACTION_START_SERVICE
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
parentService.startForegroundService(intent)
} else {
parentService.startService(intent)
}

parentService.bindService(
Intent(parentService, ControllerExtensionService::class.java),
serviceConnection,
Context.BIND_AUTO_CREATE
)
}

fun stopService() {
if (isServiceBound) {
parentService.unbindService(serviceConnection)
isServiceBound = false
}
parentService.stopService(Intent(parentService, ControllerExtensionService::class.java))
player = null
}
fun play() = mediaService?.play()
fun pause() = mediaService?.pause()
fun seekToNext() = mediaService?.seekToNext()
fun seekToPrevious() = mediaService?.seekToPrevious()
fun seekTo(position: Long) = mediaService?.seekTo(position)
fun seekToMediaItem(index: Int) = mediaService?.seekToMediaItem(index)
fun moveMediaItem(fromIndex: Int, toIndex: Int) =
mediaService?.moveMediaItem(fromIndex, toIndex)
fun removeMediaItem(index: Int) = mediaService?.removeMediaItem(index)
fun setShuffleMode(enabled: Boolean) = mediaService?.setShuffleMode(enabled)
fun setRepeatMode(repeatMode: RepeatMode) = mediaService?.setRepeatMode(repeatMode)
}
Loading
Loading