diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8fc8419..bc119df 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -5,16 +5,14 @@ plugins {
id("dagger.hilt.android.plugin")
}
-var composeVersion: String = "1.5.4"
-
android {
namespace = "com.rcudev.simplemediaplayer"
- compileSdk = 34
+ compileSdk = 35
defaultConfig {
applicationId = "com.rcudev.simplemediaplayer"
minSdk = 26
- targetSdk = 34
+ targetSdk = 35
versionCode = 1
versionName = "1.0"
@@ -41,7 +39,7 @@ android {
compose = true
}
composeOptions {
- kotlinCompilerExtensionVersion = "1.4.4"
+ kotlinCompilerExtensionVersion = "1.5.13"
}
packagingOptions {
resources.excludes.add("META-INF/{AL2.0,LGPL2.1}")
@@ -51,33 +49,35 @@ android {
dependencies {
implementation(project(":player-service"))
- implementation("androidx.core:core-ktx:1.12.0")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+ implementation("androidx.core:core-ktx:1.13.1")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
// Compose
- implementation("androidx.activity:activity-compose:1.8.1")
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
- implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
- implementation("androidx.compose.ui:ui:1.5.4")
- implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
- implementation("androidx.compose.material3:material3:1.1.2")
- implementation("androidx.navigation:navigation-compose:2.7.5")
- implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
+ implementation(platform("androidx.compose:compose-bom:2024.09.02"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+
+ implementation("androidx.activity:activity-compose:1.9.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.6")
+ implementation("androidx.navigation:navigation-compose:2.8.1")
+ implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// Hilt
- implementation("com.google.dagger:hilt-android:2.46.1")
- kapt("com.google.dagger:hilt-compiler:2.46.1")
+ implementation("com.google.dagger:hilt-android:2.52")
+ kapt("com.google.dagger:hilt-compiler:2.52")
// Media3
- implementation("androidx.media3:media3-session:1.2.0")
+ implementation("androidx.media3:media3-session:1.4.1")
// Coil
- implementation("io.coil-kt:coil-compose:2.4.0")
+ implementation("io.coil-kt:coil-compose:2.7.0")
testImplementation("junit:junit:4.13.2")
- androidTestImplementation("androidx.test.ext:junit:1.1.5")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
- androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion")
- debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
- debugImplementation("androidx.compose.ui:ui-test-manifest:$composeVersion")
+ androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9ac715..7317f1c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,8 +3,6 @@
xmlns:tools="http://schemas.android.com/tools">
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/Destination.kt b/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/Destination.kt
index 542fe39..31c1273 100644
--- a/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/Destination.kt
+++ b/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/Destination.kt
@@ -1,6 +1,6 @@
package com.rcudev.simplemediaplayer.common.ui
sealed class Destination(val route: String) {
- object Main: Destination("main")
- object Secondary: Destination("secondary")
+ data object Main: Destination("main")
+ data object Secondary: Destination("secondary")
}
\ No newline at end of file
diff --git a/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/SimpleMediaActivity.kt b/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/SimpleMediaActivity.kt
index 8f28962..42015e8 100644
--- a/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/SimpleMediaActivity.kt
+++ b/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/SimpleMediaActivity.kt
@@ -1,13 +1,17 @@
package com.rcudev.simplemediaplayer.common.ui
+import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
+import androidx.media3.session.MediaController
+import androidx.media3.session.SessionToken
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import com.google.common.util.concurrent.MoreExecutors
import com.rcudev.player_service.service.SimpleMediaService
import com.rcudev.simplemediaplayer.common.ui.theme.SimpleMediaPlayerTheme
import com.rcudev.simplemediaplayer.main.SimpleMediaScreen
@@ -18,7 +22,6 @@ import dagger.hilt.android.AndroidEntryPoint
class SimpleMediaActivity : ComponentActivity() {
private val viewModel: SimpleMediaViewModel by viewModels()
- private var isServiceRunning = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -32,7 +35,6 @@ class SimpleMediaActivity : ComponentActivity() {
SimpleMediaScreen(
vm = viewModel,
navController = navController,
- startService = ::startService
)
}
composable(Destination.Secondary.route) {
@@ -42,18 +44,4 @@ class SimpleMediaActivity : ComponentActivity() {
}
}
}
-
- override fun onDestroy() {
- super.onDestroy()
- stopService(Intent(this, SimpleMediaService::class.java))
- isServiceRunning = false
- }
-
- private fun startService() {
- if (!isServiceRunning) {
- val intent = Intent(this, SimpleMediaService::class.java)
- startForegroundService(intent)
- isServiceRunning = true
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/SimpleMediaViewModel.kt b/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/SimpleMediaViewModel.kt
index d1f633d..1ef7c06 100644
--- a/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/SimpleMediaViewModel.kt
+++ b/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/SimpleMediaViewModel.kt
@@ -1,6 +1,8 @@
package com.rcudev.simplemediaplayer.common.ui
import android.net.Uri
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
@@ -26,8 +28,8 @@ class SimpleMediaViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : ViewModel() {
- var duration by savedStateHandle.saveable { mutableStateOf(0L) }
- var progress by savedStateHandle.saveable { mutableStateOf(0f) }
+ var duration by savedStateHandle.saveable { mutableLongStateOf(0L) }
+ var progress by savedStateHandle.saveable { mutableFloatStateOf(0f) }
var progressString by savedStateHandle.saveable { mutableStateOf("00:00") }
var isPlaying by savedStateHandle.saveable { mutableStateOf(false) }
@@ -35,13 +37,28 @@ class SimpleMediaViewModel @Inject constructor(
val uiState = _uiState.asStateFlow()
init {
- viewModelScope.launch {
- loadData()
+ simpleMediaServiceHandler.connect(
+ callBack = { connected, playing ->
+ when {
+ connected && playing -> {
+ println("VM.init - simpleMediaServiceHandler is connected and is playing")
+ }
+ connected && !playing -> {
+ println("VM.init - simpleMediaServiceHandler is connected. Load data")
+ loadData()
+ }
+ else -> {
+ println("VM.init - simpleMediaServiceHandler failed to connect")
+ }
+ }
+ }
+ )
+ viewModelScope.launch {
simpleMediaServiceHandler.simpleMediaState.collect { mediaState ->
when (mediaState) {
- is SimpleMediaState.Buffering -> calculateProgressValues(mediaState.progress)
SimpleMediaState.Initial -> _uiState.value = UIState.Initial
+ is SimpleMediaState.Buffering -> calculateProgressValues(mediaState.progress)
is SimpleMediaState.Playing -> isPlaying = mediaState.isPlaying
is SimpleMediaState.Progress -> calculateProgressValues(mediaState.progress)
is SimpleMediaState.Ready -> {
@@ -54,9 +71,8 @@ class SimpleMediaViewModel @Inject constructor(
}
override fun onCleared() {
- viewModelScope.launch {
- simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Stop)
- }
+ println("VM.onCleared")
+ simpleMediaServiceHandler.release()
}
fun onUIEvent(uiEvent: UIEvent) = viewModelScope.launch {
@@ -95,7 +111,7 @@ class SimpleMediaViewModel @Inject constructor(
.setFolderType(MediaMetadata.FOLDER_TYPE_ALBUMS)
.setArtworkUri(Uri.parse("https://i.pinimg.com/736x/4b/02/1f/4b021f002b90ab163ef41aaaaa17c7a4.jpg"))
.setAlbumTitle("SoundHelix")
- .setDisplayTitle("Song 1")
+ .setTitle("Song 1")
.build()
).build()
@@ -117,17 +133,16 @@ class SimpleMediaViewModel @Inject constructor(
simpleMediaServiceHandler.addMediaItem(mediaItem)
//simpleMediaServiceHandler.addMediaItemList(mediaItemList)
}
-
}
sealed class UIEvent {
- object PlayPause : UIEvent()
- object Backward : UIEvent()
- object Forward : UIEvent()
+ data object PlayPause : UIEvent()
+ data object Backward : UIEvent()
+ data object Forward : UIEvent()
data class UpdateProgress(val newProgress: Float) : UIEvent()
}
sealed class UIState {
- object Initial : UIState()
- object Ready : UIState()
-}
\ No newline at end of file
+ data object Initial : UIState()
+ data object Ready : UIState()
+}
diff --git a/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/components/PlayerBar.kt b/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/components/PlayerBar.kt
index 8763a7c..9c05421 100644
--- a/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/components/PlayerBar.kt
+++ b/app/src/main/java/com/rcudev/simplemediaplayer/common/ui/components/PlayerBar.kt
@@ -4,8 +4,11 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.rcudev.simplemediaplayer.common.ui.UIEvent
@@ -17,21 +20,21 @@ internal fun PlayerBar(
progressString: String,
onUiEvent: (UIEvent) -> Unit
) {
- val newProgressValue = remember { mutableStateOf(0f) }
- val useNewProgressValue = remember { mutableStateOf(false) }
+ var newProgressValue by remember { mutableFloatStateOf(0f) }
+ var useNewProgressValue by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxWidth()
) {
Slider(
- value = if (useNewProgressValue.value) newProgressValue.value else progress,
+ value = if (useNewProgressValue) newProgressValue else progress,
onValueChange = { newValue ->
- useNewProgressValue.value = true
- newProgressValue.value = newValue
+ useNewProgressValue = true
+ newProgressValue = newValue
onUiEvent(UIEvent.UpdateProgress(newProgress = newValue))
},
onValueChangeFinished = {
- useNewProgressValue.value = false
+ useNewProgressValue = false
},
modifier = Modifier
.padding(horizontal = 8.dp)
@@ -46,4 +49,4 @@ internal fun PlayerBar(
Text(text = durationString)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/rcudev/simplemediaplayer/main/MainScreen.kt b/app/src/main/java/com/rcudev/simplemediaplayer/main/MainScreen.kt
index d312d4f..81ce073 100644
--- a/app/src/main/java/com/rcudev/simplemediaplayer/main/MainScreen.kt
+++ b/app/src/main/java/com/rcudev/simplemediaplayer/main/MainScreen.kt
@@ -22,7 +22,6 @@ import com.rcudev.simplemediaplayer.common.ui.components.SimpleMediaPlayerUI
internal fun SimpleMediaScreen(
vm: SimpleMediaViewModel,
navController: NavController,
- startService: () -> Unit,
) {
val state = vm.uiState.collectAsStateWithLifecycle()
@@ -38,10 +37,6 @@ internal fun SimpleMediaScreen(
.align(Alignment.Center)
)
is UIState.Ready -> {
- LaunchedEffect(true) { // This is only call first time
- startService()
- }
-
ReadyContent(vm = vm, navController = navController)
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index b8bcbfc..bf9c9f8 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,11 +1,11 @@
buildscript {
dependencies {
- classpath("com.google.dagger:hilt-android-gradle-plugin:2.46.1")
+ classpath("com.google.dagger:hilt-android-gradle-plugin:2.52")
}
}
plugins {
- id("com.android.application") version "8.1.1" apply false
- id("com.android.library") version "8.1.1" apply false
- id("org.jetbrains.kotlin.android") version "1.8.10" apply false
+ id("com.android.application") version "8.6.1" apply false
+ id("com.android.library") version "8.6.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.0.20" apply false
}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index d26eeb8..b9d5e99 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Fri Mar 24 10:06:16 CET 2023
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/player-service/build.gradle.kts b/player-service/build.gradle.kts
index c77f6f1..6fac940 100644
--- a/player-service/build.gradle.kts
+++ b/player-service/build.gradle.kts
@@ -33,23 +33,26 @@ android {
dependencies {
- implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.legacy:legacy-support-v4:1.0.0") // Needed MediaSessionCompat.Token
- implementation("com.google.android.material:material:1.10.0")
+ implementation("com.google.android.material:material:1.12.0")
// Hilt
- implementation("com.google.dagger:hilt-android:2.46.1")
- kapt("com.google.dagger:hilt-compiler:2.46.1")
+ implementation("com.google.dagger:hilt-android:2.51.1")
+ kapt("com.google.dagger:hilt-compiler:2.51.1")
// Media3
- implementation("androidx.media3:media3-exoplayer:1.2.0")
- implementation("androidx.media3:media3-ui:1.2.0")
- implementation("androidx.media3:media3-session:1.2.0")
+ implementation("androidx.media3:media3-exoplayer:1.3.1")
+ implementation("androidx.media3:media3-ui:1.3.1")
+ implementation("androidx.media3:media3-session:1.3.1")
// Glide
implementation("com.github.bumptech.glide:glide:4.15.1")
+
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.8.1")
+
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
diff --git a/player-service/src/main/AndroidManifest.xml b/player-service/src/main/AndroidManifest.xml
index a5918e6..d110442 100644
--- a/player-service/src/main/AndroidManifest.xml
+++ b/player-service/src/main/AndroidManifest.xml
@@ -1,4 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/player-service/src/main/java/com/rcudev/player_service/di/SimpleMediaModule.kt b/player-service/src/main/java/com/rcudev/player_service/di/SimpleMediaModule.kt
index 22d4779..f1dc722 100644
--- a/player-service/src/main/java/com/rcudev/player_service/di/SimpleMediaModule.kt
+++ b/player-service/src/main/java/com/rcudev/player_service/di/SimpleMediaModule.kt
@@ -1,72 +1,30 @@
package com.rcudev.player_service.di
import android.content.Context
-import androidx.media3.common.AudioAttributes
-import androidx.media3.common.C
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
-import androidx.media3.session.MediaSession
import com.rcudev.player_service.service.SimpleMediaServiceHandler
-import com.rcudev.player_service.service.notification.SimpleMediaNotificationManager
import dagger.Module
import dagger.Provides
-import dagger.Reusable
import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.android.scopes.ViewModelScoped
import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
-@InstallIn(SingletonComponent::class)
+@InstallIn(ViewModelComponent::class)
class SimpleMediaModule {
-
- @Provides
- @Singleton
- fun provideAudioAttributes(): AudioAttributes =
- AudioAttributes.Builder()
- .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
- .setUsage(C.USAGE_MEDIA)
- .build()
-
- @Provides
- @Singleton
- @UnstableApi
- fun providePlayer(
- @ApplicationContext context: Context,
- audioAttributes: AudioAttributes
- ): ExoPlayer =
- ExoPlayer.Builder(context)
- .setAudioAttributes(audioAttributes, true)
- .setHandleAudioBecomingNoisy(true)
- .setTrackSelector(DefaultTrackSelector(context))
- .build()
-
- @Provides
- @Singleton
- fun provideNotificationManager(
- @ApplicationContext context: Context,
- player: ExoPlayer
- ): SimpleMediaNotificationManager =
- SimpleMediaNotificationManager(
- context = context,
- player = player
- )
-
- @Provides
- @Singleton
- fun provideMediaSession(
- @ApplicationContext context: Context,
- player: ExoPlayer
- ): MediaSession =
- MediaSession.Builder(context, player).build()
-
@Provides
- @Singleton
+ @ViewModelScoped
fun provideServiceHandler(
- player: ExoPlayer
- ): SimpleMediaServiceHandler =
- SimpleMediaServiceHandler(
- player = player
+ @ApplicationContext appContext: Context,
+ ): SimpleMediaServiceHandler {
+ return SimpleMediaServiceHandler(
+ appContext = appContext,
+ scope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
)
-}
\ No newline at end of file
+ }
+}
diff --git a/player-service/src/main/java/com/rcudev/player_service/service/SimpleMediaService.kt b/player-service/src/main/java/com/rcudev/player_service/service/SimpleMediaService.kt
index 14334f1..a46561b 100644
--- a/player-service/src/main/java/com/rcudev/player_service/service/SimpleMediaService.kt
+++ b/player-service/src/main/java/com/rcudev/player_service/service/SimpleMediaService.kt
@@ -1,45 +1,100 @@
package com.rcudev.player_service.service
+import android.content.Context
import android.content.Intent
+import androidx.annotation.OptIn
+import androidx.media3.common.AudioAttributes
+import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
-import com.rcudev.player_service.service.notification.SimpleMediaNotificationManager
import dagger.hilt.android.AndroidEntryPoint
+import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
@AndroidEntryPoint
class SimpleMediaService : MediaSessionService() {
@Inject
- lateinit var mediaSession: MediaSession
+ @ApplicationContext
+ lateinit var appContext: Context
- @Inject
- lateinit var notificationManager: SimpleMediaNotificationManager
+ private var mediaSession: MediaSession? = null
+ //private var notificationManager: SimpleMediaNotificationManager? = null
+
+ @OptIn(UnstableApi::class)
+ override fun onCreate() {
+ println("SimpleMediaService.onCreate")
+ super.onCreate()
+ val audioAttributes = AudioAttributes.Builder()
+ .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
+ .setUsage(C.USAGE_MEDIA)
+ .build()
+
+ val player = ExoPlayer.Builder(appContext)
+ .setAudioAttributes(audioAttributes, true)
+ .setHandleAudioBecomingNoisy(true)
+ .setTrackSelector(DefaultTrackSelector(appContext))
+ .build()
+
+ //notificationManager = SimpleMediaNotificationManager(
+ // context = appContext,
+ // player = player
+ //)
- @UnstableApi
+ mediaSession = MediaSession.Builder(appContext, player).build()
+ }
+
+ /*@UnstableApi
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- notificationManager.startNotificationService(
- mediaSessionService = this,
- mediaSession = mediaSession
- )
+ println("SimpleMediaService.onStartCommand")
+ val session = mediaSession
+ if (session != null) {
+ println("SimpleMediaService.onStartCommand - startNotificationService")
+ notificationManager?.startNotificationService(
+ mediaSessionService = this,
+ mediaSession = session
+ )
+ }
return super.onStartCommand(intent, flags, startId)
+ }*/
+
+ // The user dismissed the app from the recent tasks
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ println("SimpleMediaService.onTaskRemoved")
+ val player = mediaSession?.player
+ if (player == null ||
+ !player.playWhenReady ||
+ player.mediaItemCount == 0 ||
+ player.playbackState == Player.STATE_ENDED
+ ) {
+ println("SimpleMediaService.onTaskRemoved - stopSelf")
+ // Stop the service if not playing, continue playing in the background otherwise.
+ stopSelf()
+ }
}
override fun onDestroy() {
- super.onDestroy()
- mediaSession.run {
- release()
+ println("SimpleMediaService.onDestroy")
+ mediaSession?.run {
if (player.playbackState != Player.STATE_IDLE) {
player.seekTo(0)
player.playWhenReady = false
player.stop()
}
+ println("SimpleMediaService.onDestroy - release player and session")
+ player.release()
+ release()
+ mediaSession = null
+ //notificationManager = null
}
+ super.onDestroy()
}
- override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession =
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
mediaSession
-}
\ No newline at end of file
+}
diff --git a/player-service/src/main/java/com/rcudev/player_service/service/SimpleMediaServiceHandler.kt b/player-service/src/main/java/com/rcudev/player_service/service/SimpleMediaServiceHandler.kt
index 1608698..c3ab15a 100644
--- a/player-service/src/main/java/com/rcudev/player_service/service/SimpleMediaServiceHandler.kt
+++ b/player-service/src/main/java/com/rcudev/player_service/service/SimpleMediaServiceHandler.kt
@@ -1,102 +1,189 @@
package com.rcudev.player_service.service
import android.annotation.SuppressLint
+import android.app.ActivityManager
+import android.app.Service
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.MediaController
+import androidx.media3.session.SessionToken
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.guava.await
import javax.inject.Inject
class SimpleMediaServiceHandler @Inject constructor(
- private val player: ExoPlayer
+ private val appContext: Context,
+ private val scope: CoroutineScope,
) : Player.Listener {
private val _simpleMediaState = MutableStateFlow(SimpleMediaState.Initial)
val simpleMediaState = _simpleMediaState.asStateFlow()
+ private var controller: MediaController? = null
private var job: Job? = null
init {
- player.addListener(this)
- job = Job()
+ if (!appContext.isServiceRunning(SimpleMediaService::class.java)) {
+ println("SimpleMediaServiceHandler.init service not running, start")
+ Intent(appContext, SimpleMediaService::class.java).also {
+ appContext.startForegroundService(it)
+ }
+ }
+ }
+
+ fun connect(
+ callBack: (Boolean, Boolean) -> Unit = { _, _ -> },
+ ) {
+ job?.cancel()
+ job = scope.launch {
+ println("SimpleMediaServiceHandler.connect")
+ kotlin.runCatching {
+ val sessionToken = SessionToken(
+ appContext,
+ ComponentName(appContext, SimpleMediaService::class.java)
+ )
+ controller = MediaController.Builder(appContext, sessionToken)
+ .buildAsync()
+ .await()
+ controller?.addListener(this@SimpleMediaServiceHandler)
+ val connected = controller?.isConnected ?: false
+ val playing = controller?.isPlaying ?: false
+ callBack(connected, playing)
+ _simpleMediaState.update {
+ SimpleMediaState.Ready(controller?.duration ?: 0)
+ }
+ onIsPlayingChanged(playing)
+ }.onSuccess {
+ println("SimpleMediaServiceHandler.connect session token created")
+ }.onFailure {
+ println("SimpleMediaServiceHandler.connect failed to get handle to controller. err ${it.message}")
+ callBack(false, false)
+ }
+ }
+ }
+
+ fun release(stopPlayback: Boolean = false) {
+ println("SimpleMediaServiceHandler.release called, cleanup")
+ controller ?: return
+ job?.cancel()
+ stopProgressUpdate()
+ controller?.release()
+ controller = null
+ if (stopPlayback && appContext.isServiceRunning(SimpleMediaService::class.java)) {
+ println("SimpleMediaServiceHandler.release stop service")
+ appContext.stopService(Intent(appContext, SimpleMediaService::class.java))
+ }
}
fun addMediaItem(mediaItem: MediaItem) {
- player.setMediaItem(mediaItem)
- player.prepare()
+ println("SimpleMediaServiceHandler.addMediaItem (controller connected: ${controller?.isConnected})")
+ controller?.setMediaItem(mediaItem)
+ controller?.prepare()
}
fun addMediaItemList(mediaItemList: List) {
- player.setMediaItems(mediaItemList)
- player.prepare()
+ println("SimpleMediaServiceHandler.addMediaItemList (controller connected: ${controller?.isConnected})")
+ controller?.setMediaItems(mediaItemList)
+ controller?.prepare()
}
- suspend fun onPlayerEvent(playerEvent: PlayerEvent) {
+ fun onPlayerEvent(playerEvent: PlayerEvent) {
+ println("SimpleMediaServiceHandler.onPlayerEvent $playerEvent")
+ val controller = this.controller ?: return
when (playerEvent) {
- PlayerEvent.Backward -> player.seekBack()
- PlayerEvent.Forward -> player.seekForward()
+ PlayerEvent.Backward -> controller.seekBack()
+ PlayerEvent.Forward -> controller.seekForward()
+ PlayerEvent.Stop -> stopProgressUpdate()
PlayerEvent.PlayPause -> {
- if (player.isPlaying) {
- player.pause()
+ if (controller.isPlaying) {
+ controller.pause()
stopProgressUpdate()
} else {
- player.play()
- _simpleMediaState.value = SimpleMediaState.Playing(isPlaying = true)
+ controller.play()
+ _simpleMediaState.update {
+ SimpleMediaState.Playing(isPlaying = true)
+ }
startProgressUpdate()
}
}
- PlayerEvent.Stop -> stopProgressUpdate()
- is PlayerEvent.UpdateProgress -> player.seekTo((player.duration * playerEvent.newProgress).toLong())
+ is PlayerEvent.UpdateProgress -> {
+ controller.seekTo((controller.duration * playerEvent.newProgress).toLong())
+ }
}
}
@SuppressLint("SwitchIntDef")
override fun onPlaybackStateChanged(playbackState: Int) {
+ println("SimpleMediaServiceHandler.onPlaybackStateChanged $playbackState")
+ val controller = this.controller ?: return
when (playbackState) {
- ExoPlayer.STATE_BUFFERING -> _simpleMediaState.value =
- SimpleMediaState.Buffering(player.currentPosition)
- ExoPlayer.STATE_READY -> _simpleMediaState.value =
- SimpleMediaState.Ready(player.duration)
+ ExoPlayer.STATE_BUFFERING -> {
+ _simpleMediaState.update {
+ SimpleMediaState.Buffering(controller.currentPosition)
+ }
+ }
+ ExoPlayer.STATE_READY -> {
+ _simpleMediaState.update {
+ SimpleMediaState.Ready(controller.duration)
+ }
+ }
}
}
- @OptIn(DelicateCoroutinesApi::class)
override fun onIsPlayingChanged(isPlaying: Boolean) {
- _simpleMediaState.value = SimpleMediaState.Playing(isPlaying = isPlaying)
+ println("SimpleMediaServiceHandler.onIsPlayingChanged $isPlaying")
+ _simpleMediaState.update { SimpleMediaState.Playing(isPlaying = isPlaying) }
if (isPlaying) {
- GlobalScope.launch(Dispatchers.Main) {
- startProgressUpdate()
- }
+ startProgressUpdate()
} else {
stopProgressUpdate()
}
}
- private suspend fun startProgressUpdate() = job.run {
- while (true) {
- delay(500)
- _simpleMediaState.value = SimpleMediaState.Progress(player.currentPosition)
+ private fun startProgressUpdate() {
+ job?.cancel()
+ job = scope.launch {
+ while (true) {
+ delay(500)
+ _simpleMediaState.update {
+ SimpleMediaState.Progress(controller?.currentPosition ?: 0)
+ }
+ }
}
}
private fun stopProgressUpdate() {
job?.cancel()
- _simpleMediaState.value = SimpleMediaState.Playing(isPlaying = false)
+ job = null
+ _simpleMediaState.update { SimpleMediaState.Playing(isPlaying = false) }
+ }
+
+ private fun Context.isServiceRunning(serviceClass: Class) = try {
+ (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)
+ .getRunningServices(Int.MAX_VALUE)
+ .any { it.service.className == serviceClass.name }
+ } catch (e: Exception) {
+ false
}
}
sealed class PlayerEvent {
- object PlayPause : PlayerEvent()
- object Backward : PlayerEvent()
- object Forward : PlayerEvent()
- object Stop : PlayerEvent()
+ data object PlayPause : PlayerEvent()
+ data object Backward : PlayerEvent()
+ data object Forward : PlayerEvent()
+ data object Stop : PlayerEvent()
data class UpdateProgress(val newProgress: Float) : PlayerEvent()
}
sealed class SimpleMediaState {
- object Initial : SimpleMediaState()
+ data object Initial : SimpleMediaState()
data class Ready(val duration: Long) : SimpleMediaState()
data class Progress(val progress: Long) : SimpleMediaState()
data class Buffering(val progress: Long) : SimpleMediaState()
diff --git a/player-service/src/main/java/com/rcudev/player_service/service/notification/SimpleMediaNotificationManager.kt b/player-service/src/main/java/com/rcudev/player_service/service/notification/SimpleMediaNotificationManager.kt
index 57025b5..b72c5ee 100644
--- a/player-service/src/main/java/com/rcudev/player_service/service/notification/SimpleMediaNotificationManager.kt
+++ b/player-service/src/main/java/com/rcudev/player_service/service/notification/SimpleMediaNotificationManager.kt
@@ -6,22 +6,20 @@ import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
+import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.ui.PlayerNotificationManager
import com.rcudev.player_service.R
-import dagger.hilt.android.qualifiers.ApplicationContext
-import javax.inject.Inject
private const val NOTIFICATION_ID = 200
private const val NOTIFICATION_CHANNEL_NAME = "notification channel 1"
private const val NOTIFICATION_CHANNEL_ID = "notification channel id 1"
-class SimpleMediaNotificationManager @Inject constructor(
- @ApplicationContext private val context: Context,
- private val player: ExoPlayer
+class SimpleMediaNotificationManager(
+ private val context: Context,
+ private val player: Player,
) {
private var notificationManager: NotificationManagerCompat =