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" }