diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 142f9647..527aa74f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -45,6 +45,7 @@ android {
}
dependencies {
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.7.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.core:core-ktx:1.12.0")
@@ -52,8 +53,6 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.recyclerview:recyclerview:1.3.2")
- implementation("androidx.media3:media3-exoplayer:1.2.1")
-
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
@@ -62,12 +61,14 @@ dependencies {
implementation("com.google.android.material:material:1.12.0-alpha03")
implementation("com.google.dagger:hilt-android:2.48.1")
+
+ implementation("androidx.media3:media3-exoplayer:1.2.1")
implementation("androidx.media3:media3-session:1.2.1")
+
kapt("com.google.dagger:hilt-android-compiler:2.48.1")
implementation("com.github.bumptech.glide:glide:4.16.0")
- implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.paging:paging-runtime-ktx:3.2.1")
implementation("androidx.paging:paging-common-ktx:3.2.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1b0359c6..05295947 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,6 +38,9 @@
android:permission="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK">
+
+
+
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
index b9060259..b232dcb2 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
@@ -5,15 +5,38 @@ import android.content.Intent
import androidx.annotation.OptIn
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
+import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.DefaultMediaNotificationProvider
+import androidx.media3.session.LibraryResult
+import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
-import androidx.media3.session.MediaSessionService
+import androidx.paging.AsyncPagingDataDiffer
+import com.google.common.collect.ImmutableList
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import dagger.hilt.android.AndroidEntryPoint
+import dev.brahmkshatriya.echo.data.clients.SearchClient
+import dev.brahmkshatriya.echo.data.clients.TrackClient
+import dev.brahmkshatriya.echo.data.extensions.OfflineExtension
+import dev.brahmkshatriya.echo.data.models.MediaItemsContainer
+import dev.brahmkshatriya.echo.ui.adapters.MediaItemsContainerAdapter
+import dev.brahmkshatriya.echo.ui.player.PlayerHelper.Companion.mediaItemBuilder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.guava.future
+import kotlinx.coroutines.plus
+import javax.inject.Inject
+@AndroidEntryPoint
+class PlaybackService : MediaLibraryService() {
-class PlaybackService : MediaSessionService() {
- private var mediaSession: MediaSession? = null
+ @Inject lateinit var extension: OfflineExtension
+
+ private var mediaLibrarySession: MediaLibrarySession? = null
@OptIn(UnstableApi::class)
override fun onCreate() {
@@ -36,7 +59,7 @@ class PlaybackService : MediaSessionService() {
val pendingIntent = PendingIntent
.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
- mediaSession = MediaSession.Builder(this, player)
+ mediaLibrarySession = MediaLibrarySession.Builder(this, player, Callback(extension))
.setSessionActivity(pendingIntent)
.build()
@@ -50,14 +73,68 @@ class PlaybackService : MediaSessionService() {
}
override fun onDestroy() {
- mediaSession?.run {
+ mediaLibrarySession?.run {
player.release()
release()
- mediaSession = null
+ mediaLibrarySession = null
}
super.onDestroy()
}
- override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
- mediaSession
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? =
+ mediaLibrarySession
+
+ inner class Callback(
+ private val extension: Any
+ ) : MediaLibrarySession.Callback {
+
+ private val scope = CoroutineScope(Dispatchers.IO) + Job()
+
+ private fun notSupported() = Futures.immediateFuture(
+ LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)
+ )
+
+ override fun onGetSearchResult(
+ session: MediaLibrarySession,
+ browser: MediaSession.ControllerInfo,
+ query: String,
+ page: Int,
+ pageSize: Int,
+ params: LibraryParams?
+ ): ListenableFuture>> {
+ if (extension !is SearchClient) return notSupported()
+ if (extension !is TrackClient) return notSupported()
+
+ return scope.future(Dispatchers.IO) {
+ val differ = AsyncPagingDataDiffer(
+ MediaItemsContainerAdapter.MediaItemsContainerComparator,
+ MediaItemsContainerAdapter.ListCallback(),
+ )
+ extension.search(query).map {
+ differ.submitData(it)
+ }
+ val list = differ.snapshot().items.map {
+ val track = (it as? MediaItemsContainer.TrackItem)?.track ?: return@map null
+ val stream = extension.getStreamable(track)
+ mediaItemBuilder(track, stream)
+ }.filterNotNull()
+ LibraryResult.ofItemList(list, params)
+ }
+ }
+
+ override fun onGetItem(
+ session: MediaLibrarySession,
+ browser: MediaSession.ControllerInfo,
+ mediaId: String
+ ): ListenableFuture> {
+ if (extension !is TrackClient) return notSupported()
+ return scope.future(Dispatchers.IO) {
+ val track = extension.getTrack(mediaId)
+ ?: return@future LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN)
+ val stream = extension.getStreamable(track)
+ val item = mediaItemBuilder(track, stream)
+ LibraryResult.ofItem(item, null)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/clients/TrackClient.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/clients/TrackClient.kt
index ec8db892..c92173ed 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/data/clients/TrackClient.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/data/clients/TrackClient.kt
@@ -4,6 +4,7 @@ import dev.brahmkshatriya.echo.data.models.StreamableAudio
import dev.brahmkshatriya.echo.data.models.Track
interface TrackClient {
+ suspend fun getTrack(uri: String): Track?
suspend fun getStreamable(track: Track): StreamableAudio
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/extensions/OfflineExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/extensions/OfflineExtension.kt
index 175c7637..6236ed53 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/data/extensions/OfflineExtension.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/data/extensions/OfflineExtension.kt
@@ -88,6 +88,10 @@ class OfflineExtension(val context: Context) : SearchClient, TrackClient, HomeFe
}
}
+ override suspend fun getTrack(uri: String): Track {
+ return LocalTrack.get(context, uri) ?: throw IOException("Track not found")
+ }
+
override suspend fun getStreamable(track: Track): StreamableAudio {
return LocalStream.getFromTrack(context, track)?.toAudio()
?: throw IOException("Track not found")
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalTrack.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalTrack.kt
index 1d74d98d..e3918d31 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalTrack.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalTrack.kt
@@ -37,13 +37,23 @@ interface LocalTrack {
return context.queryTracks(whereCondition, selectionArgs, page, pageSize).shuffled()
}
- fun getByArtist(context: Context, artist: Artist.Small, page: Int, pageSize: Int): List