diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 56bedad1..8e679dba 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -80,6 +80,7 @@ dependencies {
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.hilt.android)
+ implementation(libs.androidx.media3.session)
kapt(libs.hilt.android.compiler)
implementation(libs.androidx.core.ktx)
@@ -91,6 +92,11 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.runtime.livedata)
+
+ implementation("androidx.media3:media3-exoplayer:1.4.1")
+ implementation("androidx.media3:media3-exoplayer-dash:1.4.1")
+ implementation("androidx.media3:media3-ui:1.4.1")
+
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ae98dd7b..a28b9829 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,11 +3,12 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/grakovne/lissen/player/MediaModule.kt b/app/src/main/java/org/grakovne/lissen/player/MediaModule.kt
new file mode 100644
index 00000000..f13630db
--- /dev/null
+++ b/app/src/main/java/org/grakovne/lissen/player/MediaModule.kt
@@ -0,0 +1,43 @@
+package org.grakovne.lissen.player
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.MediaSession
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ServiceComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.android.scopes.ServiceScoped
+import org.grakovne.lissen.ui.activity.AppActivity
+
+@Module
+@InstallIn(ServiceComponent::class)
+object MediaModule {
+
+ @Provides
+ @ServiceScoped
+ fun provideExoPlayer(@ApplicationContext context: Context): ExoPlayer {
+ return ExoPlayer.Builder(context).build()
+ }
+
+ @Provides
+ @ServiceScoped
+ fun provideMediaSession(
+ @ApplicationContext context: Context,
+ exoPlayer: ExoPlayer
+ ): MediaSession {
+ val sessionActivityPendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, AppActivity::class.java),
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ return MediaSession.Builder(context, exoPlayer)
+ .setSessionActivity(sessionActivityPendingIntent)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/grakovne/lissen/player/service/PlayerService.kt b/app/src/main/java/org/grakovne/lissen/player/service/PlayerService.kt
new file mode 100644
index 00000000..4fea3044
--- /dev/null
+++ b/app/src/main/java/org/grakovne/lissen/player/service/PlayerService.kt
@@ -0,0 +1,115 @@
+package org.grakovne.lissen.player.service
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.session.MediaSession
+import androidx.media3.session.MediaSessionService
+import androidx.media3.ui.PlayerNotificationManager
+import dagger.hilt.android.AndroidEntryPoint
+import org.grakovne.lissen.ui.activity.AppActivity
+import javax.inject.Inject
+
+@UnstableApi
+@AndroidEntryPoint
+class AudioPlayerService : MediaSessionService() {
+
+ @Inject
+ lateinit var mediaSession: MediaSession
+
+ private lateinit var playerNotificationManager: PlayerNotificationManager
+
+ companion object {
+ private const val CHANNEL_ID = "audio_player_channel"
+ private const val NOTIFICATION_ID = 1
+
+ fun createNotificationChannel(context: Context) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "Audio Player",
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Channel for audio player notifications"
+ }
+ val notificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel(this)
+
+ playerNotificationManager = PlayerNotificationManager.Builder(
+ this,
+ NOTIFICATION_ID,
+ CHANNEL_ID
+ )
+ .setMediaDescriptionAdapter(MediaDescriptionAdapterImpl())
+ .setNotificationListener(NotificationListenerImpl())
+ .build()
+
+ playerNotificationManager.setPlayer(mediaSession.player)
+ }
+
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession {
+ return mediaSession
+ }
+
+ @UnstableApi
+ private inner class MediaDescriptionAdapterImpl :
+ PlayerNotificationManager.MediaDescriptionAdapter {
+ override fun getCurrentContentTitle(player: Player): String {
+ return player.mediaMetadata.title.toString()
+ }
+
+ override fun getCurrentContentText(player: Player): String {
+ return player.mediaMetadata.artist.toString()
+ }
+
+ override fun getCurrentLargeIcon(
+ player: Player,
+ callback: PlayerNotificationManager.BitmapCallback
+ ): Bitmap? {
+ return null
+ }
+
+ override fun createCurrentContentIntent(player: Player): PendingIntent? {
+ val intent = Intent(this@AudioPlayerService, AppActivity::class.java)
+ return PendingIntent.getActivity(
+ this@AudioPlayerService,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+ }
+
+ /**
+ * Внутренний класс для прослушивания событий уведомления.
+ */
+ private inner class NotificationListenerImpl : PlayerNotificationManager.NotificationListener {
+ override fun onNotificationPosted(
+ notificationId: Int,
+ notification: android.app.Notification,
+ ongoing: Boolean
+ ) {
+ if (ongoing) {
+ startForeground(notificationId, notification)
+ } else {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
+ }
+
+ override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d5ae80ed..d3e399c4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -23,6 +23,7 @@ navigationCompose = "2.8.2"
paletteKtx = "1.0.0"
retrofit = "2.9.0"
runtimeLivedata = "1.7.3"
+media3Session = "1.4.1"
[libraries]
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
@@ -56,6 +57,7 @@ androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtim
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
+androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3Session" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }