From 44f60d428caf7e84c3e3b75c359c150c486d61f8 Mon Sep 17 00:00:00 2001
From: brahmkshatriya <69040506+brahmkshatriya@users.noreply.github.com>
Date: Sat, 17 Feb 2024 22:05:13 +0530
Subject: [PATCH] Added Google Assistant support, shifted player stuff to
`player`
---
app/src/main/AndroidManifest.xml | 24 +-
.../dev/brahmkshatriya/echo/MainActivity.kt | 11 +-
.../brahmkshatriya/echo/PlaybackService.kt | 140 -------
.../echo/data/clients/TrackClient.kt | 3 +-
.../echo/data/extensions/OfflineExtension.kt | 16 +-
.../models/{MediaItem.kt => EchoMediaItem.kt} | 14 +-
.../echo/data/models/MediaItemsContainer.kt | 2 +-
.../echo/data/models/QuickSearchItem.kt | 2 +-
.../echo/data/offline/LocalTrack.kt | 4 +-
.../echo/{ui => }/player/InitPlayer.kt | 16 +-
.../echo/player/PlaybackService.kt | 76 ++++
.../{ui => }/player/PlayerBackButtonHelper.kt | 2 +-
.../echo/{ui => }/player/PlayerHelper.kt | 6 +-
.../echo/{ui => }/player/PlayerListener.kt | 2 +-
.../echo/player/PlayerSessionCallback.kt | 360 ++++++++++++++++++
.../echo/{ui => }/player/PlayerUIViewModel.kt | 2 +-
.../echo/{ui => }/player/PlayerViewModel.kt | 2 +-
.../echo/ui/adapters/MediaItemAdapter.kt | 28 +-
.../ui/adapters/MediaItemsContainerAdapter.kt | 2 +-
.../echo/ui/home/HomeFragment.kt | 4 +-
.../echo/ui/library/LibraryFragment.kt | 2 +-
.../echo/ui/search/SearchFragment.kt | 4 +-
22 files changed, 520 insertions(+), 202 deletions(-)
delete mode 100644 app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
rename app/src/main/java/dev/brahmkshatriya/echo/data/models/{MediaItem.kt => EchoMediaItem.kt} (72%)
rename app/src/main/java/dev/brahmkshatriya/echo/{ui => }/player/InitPlayer.kt (95%)
create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/player/PlaybackService.kt
rename app/src/main/java/dev/brahmkshatriya/echo/{ui => }/player/PlayerBackButtonHelper.kt (98%)
rename app/src/main/java/dev/brahmkshatriya/echo/{ui => }/player/PlayerHelper.kt (92%)
rename app/src/main/java/dev/brahmkshatriya/echo/{ui => }/player/PlayerListener.kt (98%)
create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/player/PlayerSessionCallback.kt
rename app/src/main/java/dev/brahmkshatriya/echo/{ui => }/player/PlayerUIViewModel.kt (94%)
rename app/src/main/java/dev/brahmkshatriya/echo/{ui => }/player/PlayerViewModel.kt (98%)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 05295947..19dc5786 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,8 +5,10 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt b/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
index 9c289f9a..ca782d7c 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
@@ -6,16 +6,17 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
-import androidx.media3.session.MediaController
+import androidx.media3.session.MediaBrowser
import androidx.media3.session.SessionToken
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationBarView
-import com.google.common.util.concurrent.MoreExecutors
import dagger.hilt.android.AndroidEntryPoint
import dev.brahmkshatriya.echo.databinding.ActivityMainBinding
-import dev.brahmkshatriya.echo.ui.player.initPlayer
+import dev.brahmkshatriya.echo.player.PlaybackService
+import dev.brahmkshatriya.echo.player.initPlayer
import dev.brahmkshatriya.echo.ui.utils.checkPermissions
import dev.brahmkshatriya.echo.ui.utils.emit
import dev.brahmkshatriya.echo.ui.utils.updateBottomMarginWithSystemInsets
@@ -46,9 +47,9 @@ class MainActivity : AppCompatActivity() {
updateBottomMarginWithSystemInsets(binding.navHostFragment)
val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
- val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
+ val controllerFuture = MediaBrowser.Builder(this, sessionToken).buildAsync()
val listener = Runnable { initPlayer(this, controllerFuture.get()) }
- controllerFuture.addListener(listener, MoreExecutors.directExecutor())
+ controllerFuture.addListener(listener, ContextCompat.getMainExecutor(this))
}
override fun onNewIntent(intent: Intent?) {
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
deleted file mode 100644
index b232dcb2..00000000
--- a/app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-package dev.brahmkshatriya.echo
-
-import android.app.PendingIntent
-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.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() {
-
- @Inject lateinit var extension: OfflineExtension
-
- private var mediaLibrarySession: MediaLibrarySession? = null
-
- @OptIn(UnstableApi::class)
- override fun onCreate() {
- super.onCreate()
-
- val audioAttributes = AudioAttributes.Builder()
- .setUsage(C.USAGE_MEDIA)
- .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
- .build()
-
- val player = ExoPlayer.Builder(this)
- .setHandleAudioBecomingNoisy(true)
- .setWakeMode(C.WAKE_MODE_NETWORK)
- .setAudioAttributes(audioAttributes, true)
- .build()
-
- val intent = Intent(this, MainActivity::class.java)
- .putExtra("fromNotification", true)
-
- val pendingIntent = PendingIntent
- .getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
-
- mediaLibrarySession = MediaLibrarySession.Builder(this, player, Callback(extension))
- .setSessionActivity(pendingIntent)
- .build()
-
- val notificationProvider = DefaultMediaNotificationProvider
- .Builder(this)
- .setChannelName(R.string.app_name)
- .build()
- notificationProvider.setSmallIcon(R.drawable.ic_mono)
-
- setMediaNotificationProvider(notificationProvider)
- }
-
- override fun onDestroy() {
- mediaLibrarySession?.run {
- player.release()
- release()
- mediaLibrarySession = null
- }
- super.onDestroy()
- }
-
- 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 c92173ed..851eac16 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
@@ -1,10 +1,11 @@
package dev.brahmkshatriya.echo.data.clients
+import android.net.Uri
import dev.brahmkshatriya.echo.data.models.StreamableAudio
import dev.brahmkshatriya.echo.data.models.Track
interface TrackClient {
- suspend fun getTrack(uri: String): Track?
+ suspend fun getTrack(uri: Uri): 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 6236ed53..a2cd931f 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
@@ -1,6 +1,7 @@
package dev.brahmkshatriya.echo.data.extensions
import android.content.Context
+import android.net.Uri
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
@@ -9,8 +10,8 @@ import androidx.paging.PagingState
import dev.brahmkshatriya.echo.data.clients.HomeFeedClient
import dev.brahmkshatriya.echo.data.clients.SearchClient
import dev.brahmkshatriya.echo.data.clients.TrackClient
-import dev.brahmkshatriya.echo.data.models.MediaItem.Companion.toMediaItem
-import dev.brahmkshatriya.echo.data.models.MediaItem.Companion.toMediaItemsContainer
+import dev.brahmkshatriya.echo.data.models.EchoMediaItem.Companion.toMediaItem
+import dev.brahmkshatriya.echo.data.models.EchoMediaItem.Companion.toMediaItemsContainer
import dev.brahmkshatriya.echo.data.models.MediaItemsContainer
import dev.brahmkshatriya.echo.data.models.QuickSearchItem
import dev.brahmkshatriya.echo.data.models.StreamableAudio
@@ -28,11 +29,12 @@ class OfflineExtension(val context: Context) : SearchClient, TrackClient, HomeFe
override suspend fun quickSearch(query: String): List = listOf()
override suspend fun search(query: String): Flow> = flow {
- val albums = LocalAlbum.search(context, query, 1, 50)
+ val trimmed = query.trim()
+ val albums = LocalAlbum.search(context, trimmed, 1, 50)
.map { it.toMediaItem() }.ifEmpty { null }
- val tracks = LocalTrack.search(context, query, 1, 50)
+ val tracks = LocalTrack.search(context, trimmed, 1, 50)
.map { it.toMediaItem() }.ifEmpty { null }
- val artists = LocalArtist.search(context, query, 1, 50)
+ val artists = LocalArtist.search(context, trimmed, 1, 50)
.map { it.toMediaItem() }.ifEmpty { null }
val result = listOfNotNull(
@@ -88,8 +90,8 @@ 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 getTrack(uri: Uri): Track? {
+ return LocalTrack.get(context, uri)
}
override suspend fun getStreamable(track: Track): StreamableAudio {
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItem.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/models/EchoMediaItem.kt
similarity index 72%
rename from app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItem.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/data/models/EchoMediaItem.kt
index b95a0764..45202f42 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItem.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/data/models/EchoMediaItem.kt
@@ -1,10 +1,10 @@
package dev.brahmkshatriya.echo.data.models
-sealed class MediaItem {
- data class TrackItem(val track: Track) : MediaItem()
- data class AlbumItem(val album: Album.WithCover) : MediaItem()
- data class ArtistItem(val artist: Artist.WithCover) : MediaItem()
- data class PlaylistItem(val playlist: Playlist.WithCover) : MediaItem()
+sealed class EchoMediaItem {
+ data class TrackItem(val track: Track) : EchoMediaItem()
+ data class AlbumItem(val album: Album.WithCover) : EchoMediaItem()
+ data class ArtistItem(val artist: Artist.WithCover) : EchoMediaItem()
+ data class PlaylistItem(val playlist: Playlist.WithCover) : EchoMediaItem()
companion object {
fun Track.toMediaItem() = TrackItem(this)
@@ -12,12 +12,12 @@ sealed class MediaItem {
fun Artist.WithCover.toMediaItem() = ArtistItem(this)
fun Playlist.WithCover.toMediaItem() = PlaylistItem(this)
- fun List.toMediaItemsContainer(title: String, subtitle: String? = null)
+ fun List.toMediaItemsContainer(title: String, subtitle: String? = null)
= MediaItemsContainer.Category(title, this, subtitle)
}
override fun equals(other: Any?): Boolean {
- if(other is MediaItem) {
+ if(other is EchoMediaItem) {
return when(this) {
is TrackItem -> this.track.uri == (other as? TrackItem)?.track?.uri
is AlbumItem -> this.album.uri == (other as? AlbumItem)?.album?.uri
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItemsContainer.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItemsContainer.kt
index bafd3652..bb0eda31 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItemsContainer.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItemsContainer.kt
@@ -3,7 +3,7 @@ package dev.brahmkshatriya.echo.data.models
sealed class MediaItemsContainer {
data class Category(
val title: String,
- val list: List,
+ val list: List,
val subtitle: String? = null
) : MediaItemsContainer()
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/models/QuickSearchItem.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/models/QuickSearchItem.kt
index 61df17c2..66c8e6b6 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/data/models/QuickSearchItem.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/data/models/QuickSearchItem.kt
@@ -2,5 +2,5 @@ package dev.brahmkshatriya.echo.data.models
sealed class QuickSearchItem {
data class SearchQueryItem(val query: String) : QuickSearchItem()
- data class SearchMediaItem(val mediaItem: MediaItem) : QuickSearchItem()
+ data class SearchMediaItem(val mediaItem: EchoMediaItem) : QuickSearchItem()
}
\ No newline at end of file
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 e3918d31..ea0cbfad 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
@@ -127,8 +127,8 @@ interface LocalTrack {
return tracks
}
- fun get(context: Context, uri: String): Track? {
- val id = uri.substringAfterLast('/')
+ fun get(context: Context, uri: Uri): Track? {
+ val id = uri.lastPathSegment ?: return null
val whereCondition = "${MediaStore.Audio.Media._ID} = ?"
val selectionArgs = arrayOf(id)
return context.queryTracks(whereCondition, selectionArgs, 0, 1).firstOrNull()
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/InitPlayer.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/InitPlayer.kt
similarity index 95%
rename from app/src/main/java/dev/brahmkshatriya/echo/ui/player/InitPlayer.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/player/InitPlayer.kt
index f3735f0f..ff889238 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/InitPlayer.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/player/InitPlayer.kt
@@ -1,4 +1,4 @@
-package dev.brahmkshatriya.echo.ui.player
+package dev.brahmkshatriya.echo.player
import android.animation.ObjectAnimator
import android.content.res.Resources
@@ -12,7 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.media3.common.Player.REPEAT_MODE_ALL
import androidx.media3.common.Player.REPEAT_MODE_OFF
import androidx.media3.common.Player.REPEAT_MODE_ONE
-import androidx.media3.session.MediaController
+import androidx.media3.session.MediaBrowser
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
@@ -23,8 +23,8 @@ import com.google.android.material.checkbox.MaterialCheckBox.OnCheckedStateChang
import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED
import dev.brahmkshatriya.echo.MainActivity
import dev.brahmkshatriya.echo.R
-import dev.brahmkshatriya.echo.ui.player.PlayerHelper.Companion.mediaItemBuilder
-import dev.brahmkshatriya.echo.ui.player.PlayerHelper.Companion.toTimeString
+import dev.brahmkshatriya.echo.player.PlayerHelper.Companion.mediaItemBuilder
+import dev.brahmkshatriya.echo.player.PlayerHelper.Companion.toTimeString
import dev.brahmkshatriya.echo.ui.utils.dpToPx
import dev.brahmkshatriya.echo.ui.utils.emit
import dev.brahmkshatriya.echo.ui.utils.loadInto
@@ -35,7 +35,7 @@ import kotlin.math.max
fun initPlayer(
activity: MainActivity,
- player: MediaController
+ player: MediaBrowser
) {
val playerBinding = activity.binding.bottomPlayer
val container = activity.binding.bottomPlayerContainer as View
@@ -68,8 +68,10 @@ fun initPlayer(
bottomBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
PlayerBackButtonHelper.playerCollapsed.value = newState
- if (newState == STATE_HIDDEN)
- playerViewModel.clearQueue()
+ when (newState) {
+ STATE_HIDDEN -> playerViewModel.clearQueue()
+ else -> bottomBehavior.isHideable = newState != STATE_EXPANDED
+ }
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/PlaybackService.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlaybackService.kt
new file mode 100644
index 00000000..cec1234a
--- /dev/null
+++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlaybackService.kt
@@ -0,0 +1,76 @@
+package dev.brahmkshatriya.echo.player
+
+import android.app.Application
+import android.app.PendingIntent
+import android.content.Intent
+import androidx.annotation.OptIn
+import androidx.media3.common.AudioAttributes
+import androidx.media3.common.C
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.DefaultMediaNotificationProvider
+import androidx.media3.session.MediaLibraryService
+import androidx.media3.session.MediaSession
+import dagger.hilt.android.AndroidEntryPoint
+import dev.brahmkshatriya.echo.MainActivity
+import dev.brahmkshatriya.echo.R
+import dev.brahmkshatriya.echo.data.extensions.OfflineExtension
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class PlaybackService : MediaLibraryService() {
+
+ @Inject
+ lateinit var app: Application
+
+ @Inject
+ lateinit var extension: OfflineExtension
+
+ private var mediaLibrarySession: MediaLibrarySession? = null
+
+ @OptIn(UnstableApi::class)
+ override fun onCreate() {
+ super.onCreate()
+
+ val audioAttributes = AudioAttributes.Builder()
+ .setUsage(C.USAGE_MEDIA)
+ .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
+ .build()
+
+ val player = ExoPlayer.Builder(this)
+ .setHandleAudioBecomingNoisy(true)
+ .setWakeMode(C.WAKE_MODE_NETWORK)
+ .setAudioAttributes(audioAttributes, true)
+ .build()
+
+ val intent = Intent(this, MainActivity::class.java)
+ .putExtra("fromNotification", true)
+
+ val pendingIntent = PendingIntent
+ .getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+
+ mediaLibrarySession = MediaLibrarySession.Builder(this, player, PlayerSessionCallback(app, extension))
+ .setSessionActivity(pendingIntent)
+ .build()
+
+ val notificationProvider = DefaultMediaNotificationProvider
+ .Builder(this)
+ .setChannelName(R.string.app_name)
+ .build()
+ notificationProvider.setSmallIcon(R.drawable.ic_mono)
+
+ setMediaNotificationProvider(notificationProvider)
+ }
+
+ override fun onDestroy() {
+ mediaLibrarySession?.run {
+ player.release()
+ release()
+ mediaLibrarySession = null
+ }
+ super.onDestroy()
+ }
+
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaLibrarySession
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerBackButtonHelper.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerBackButtonHelper.kt
similarity index 98%
rename from app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerBackButtonHelper.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/player/PlayerBackButtonHelper.kt
index 6968f4d9..7863c994 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerBackButtonHelper.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerBackButtonHelper.kt
@@ -1,4 +1,4 @@
-package dev.brahmkshatriya.echo.ui.player
+package dev.brahmkshatriya.echo.player
import android.view.View
import androidx.activity.BackEventCompat
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerHelper.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerHelper.kt
similarity index 92%
rename from app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerHelper.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/player/PlayerHelper.kt
index 008856d7..fadebf82 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerHelper.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerHelper.kt
@@ -1,4 +1,4 @@
-package dev.brahmkshatriya.echo.ui.player
+package dev.brahmkshatriya.echo.player
import android.annotation.SuppressLint
import android.graphics.Bitmap
@@ -9,6 +9,7 @@ import androidx.media3.common.util.UnstableApi
import dev.brahmkshatriya.echo.data.models.ImageHolder
import dev.brahmkshatriya.echo.data.models.StreamableAudio
import dev.brahmkshatriya.echo.data.models.Track
+import dev.brahmkshatriya.echo.player.PlayerListener.Companion.tracks
import java.nio.ByteBuffer
@@ -34,6 +35,7 @@ interface PlayerHelper {
item.setMediaMetadata(metadata)
item.setMediaId(track.uri.toString())
item.setTag(track)
+ tracks[track.uri.toString()] = track
return item.build()
}
@@ -42,6 +44,8 @@ interface PlayerHelper {
.setTitle(title)
.setArtist(artists.firstOrNull()?.name)
.setArtwork(cover)
+ .setIsPlayable(true)
+ .setIsBrowsable(false)
.build()
@UnstableApi
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerListener.kt
similarity index 98%
rename from app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerListener.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/player/PlayerListener.kt
index 4ddd607e..dd458f81 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerListener.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerListener.kt
@@ -1,4 +1,4 @@
-package dev.brahmkshatriya.echo.ui.player
+package dev.brahmkshatriya.echo.player
import android.annotation.SuppressLint
import android.os.Handler
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerSessionCallback.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerSessionCallback.kt
new file mode 100644
index 00000000..2ce40f81
--- /dev/null
+++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerSessionCallback.kt
@@ -0,0 +1,360 @@
+package dev.brahmkshatriya.echo.player
+
+import android.app.Application
+import android.widget.Toast
+import androidx.media3.common.MediaItem
+import androidx.media3.session.MediaLibraryService
+import androidx.media3.session.MediaSession
+import androidx.paging.AsyncPagingDataDiffer
+import com.google.common.util.concurrent.ListenableFuture
+import dev.brahmkshatriya.echo.data.clients.SearchClient
+import dev.brahmkshatriya.echo.data.clients.TrackClient
+import dev.brahmkshatriya.echo.data.models.EchoMediaItem
+import dev.brahmkshatriya.echo.data.models.MediaItemsContainer
+import dev.brahmkshatriya.echo.ui.adapters.MediaItemsContainerAdapter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.guava.future
+import kotlinx.coroutines.plus
+
+
+class PlayerSessionCallback(
+ private val context: Application,
+ private val extension: Any
+) : MediaLibraryService.MediaLibrarySession.Callback {
+
+ private val scope = CoroutineScope(Dispatchers.IO) + Job()
+
+ private val differ = AsyncPagingDataDiffer(
+ MediaItemsContainerAdapter,
+ MediaItemsContainerAdapter.ListCallback(),
+ )
+
+ override fun onAddMediaItems(
+ mediaSession: MediaSession,
+ controller: MediaSession.ControllerInfo,
+ mediaItems: MutableList
+ ): ListenableFuture> = scope.future {
+
+ fun default(reason: String): MutableList {
+ println(reason)
+ Toast.makeText(context.applicationContext, reason, Toast.LENGTH_SHORT).show()
+ return mediaItems
+ }
+
+ val query =
+ mediaItems.firstOrNull()?.requestMetadata?.searchQuery
+ ?: return@future default("No search query")
+
+ if (extension !is TrackClient) return@future default("Extension is not a TrackClient")
+ if (extension !is SearchClient) return@future default("Extension is not a SearchClient")
+
+ println(query)
+
+ differ.submitData(extension.search(query).first())
+ val list = differ.snapshot().items.map {
+ when (it) {
+ is MediaItemsContainer.Category -> {
+ it.list.mapNotNull { item ->
+ if (item is EchoMediaItem.TrackItem) {
+ val track = item.track
+ val stream = extension.getStreamable(track)
+ println(track.title)
+ PlayerHelper.mediaItemBuilder(track, stream)
+ } else null
+ }
+ }
+
+ is MediaItemsContainer.TrackItem -> {
+ val track = it.track
+ val stream = extension.getStreamable(track)
+ listOf(PlayerHelper.mediaItemBuilder(track, stream))
+ }
+ }
+ }.flatten()
+ if (list.isEmpty()) return@future default("Couldn't find anything related to $query")
+ list.toMutableList()
+ }
+
+
+ //CAN BE USED FOR ANDROID AUTO SUPPORT, BUT IDK I DONT WANT TO ADD IT RN
+ //HALF OF IT IS KANGED FROM INNERTUNE
+
+//
+// override fun onGetLibraryRoot(
+// session: MediaLibraryService.MediaLibrarySession,
+// browser: MediaSession.ControllerInfo,
+// params: MediaLibraryService.LibraryParams?
+// ): ListenableFuture> = Futures.immediateFuture(
+// LibraryResult.ofItem(
+// MediaItem.Builder()
+// .setMediaId(ROOT)
+// .setMediaMetadata(
+// MediaMetadata.Builder()
+// .setIsPlayable(false)
+// .setIsBrowsable(false)
+// .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
+// .build()
+// )
+// .build(),
+// params
+// )
+// ).also {
+// println("onGetLibraryRoot")
+// }
+//
+// override fun onGetChildren(
+// session: MediaLibraryService.MediaLibrarySession,
+// browser: MediaSession.ControllerInfo,
+// parentId: String,
+// page: Int,
+// pageSize: Int,
+// params: MediaLibraryService.LibraryParams?,
+// ): ListenableFuture>> =
+// scope.future(Dispatchers.IO) {
+// if (extension !is HomeFeedClient) LibraryResult.ofItemList(emptyList(), params)
+// else
+// LibraryResult.ofItemList(
+// when (parentId) {
+// ROOT -> {
+// extension.getHomeFeed(null).collectLatest {
+// differ.submitData(it)
+// }
+// listOf(
+// browsableMediaItem(
+// SONG,
+// context.getString(R.string.tracks),
+// drawableUri(R.drawable.ic_heart_filled_40dp),
+// MediaMetadata.MEDIA_TYPE_PLAYLIST
+// ),
+// browsableMediaItem(
+// ARTIST,
+// context.getString(R.string.artists),
+// drawableUri(R.drawable.ic_more_horiz),
+// MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
+// ),
+// browsableMediaItem(
+// ALBUM,
+// context.getString(R.string.albums),
+// drawableUri(R.drawable.ic_home_filled),
+// MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
+// ),
+// browsableMediaItem(
+// PLAYLIST,
+// context.getString(R.string.playlists),
+// drawableUri(R.drawable.ic_repeat_40dp),
+// MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
+// )
+// )
+// }
+//
+// SONG -> {
+// if (extension is TrackClient) {
+// differ.snapshot().items.map {
+// when (it) {
+// is MediaItemsContainer.Category -> it.list.mapNotNull {
+// if (it is EchoMediaItem.TrackItem)
+// PlayerHelper.mediaItemBuilder(
+// it.track,
+// extension.getStreamable(it.track)
+// )
+// else null
+// }
+//
+// is MediaItemsContainer.TrackItem -> listOf(
+// PlayerHelper.mediaItemBuilder(
+// it.track,
+// extension.getStreamable(it.track)
+// )
+// )
+// }
+// }.flatten()
+// } else emptyList()
+// }
+//
+// ARTIST -> {
+// differ.snapshot().items.map {
+// if (it is MediaItemsContainer.Category) {
+// it.list.mapNotNull {
+// if (it is EchoMediaItem.ArtistItem) browsableMediaItem(
+// "${ARTIST}/${it.artist.uri}",
+// it.artist.name,
+// null,
+// MediaMetadata.MEDIA_TYPE_ARTIST
+// )
+// else null
+// }
+// } else emptyList()
+// }.flatten()
+// }
+//
+// ALBUM -> differ.snapshot().items.map {
+// if (it is MediaItemsContainer.Category) {
+// it.list.mapNotNull {
+// if (it is EchoMediaItem.AlbumItem) browsableMediaItem(
+// "${ALBUM}/${it.album.uri}",
+// it.album.title,
+// null,
+// MediaMetadata.MEDIA_TYPE_ALBUM
+// )
+// else null
+// }
+// } else emptyList()
+// }.flatten()
+//
+// else -> when {
+// parentId.startsWith("${ARTIST}/") -> {
+// if (extension is ArtistClient) {
+// val artist = extension.loadArtist(
+// Artist.Small(
+// parentId.removePrefix("${ARTIST}/").toUri(), ""
+// )
+// )
+// extension.getMediaItems(artist).collectLatest {
+// differ.submitData(it)
+// }
+// emptyList()
+// } else emptyList()
+// }
+//
+// parentId.startsWith("${ALBUM}/") -> {
+// if (extension is AlbumClient && extension is TrackClient) {
+// val album = extension.loadAlbum(
+// Album.Small(
+// parentId.removePrefix("${ALBUM}/").toUri(), ""
+// )
+// )
+// album.tracks.map {
+// PlayerHelper.mediaItemBuilder(
+// it,
+// extension.getStreamable(it)
+// )
+// }
+// } else emptyList()
+// }
+//
+// else -> emptyList()
+// }
+// },
+// params
+// )
+// }.also {
+// println("onGetChildren")
+// }
+
+
+
+
+// private fun browsableMediaItem(
+// id: String,
+// title: String,
+// iconUri: Uri?,
+// mediaType: Int = MediaMetadata.MEDIA_TYPE_MUSIC
+// ) =
+// MediaItem.Builder()
+// .setMediaId(id)
+// .setMediaMetadata(
+// MediaMetadata.Builder()
+// .setTitle(title)
+// .setArtworkUri(iconUri)
+// .setIsPlayable(false)
+// .setIsBrowsable(true)
+// .setMediaType(mediaType)
+// .build()
+// )
+// .build()
+//
+// private fun drawableUri(@DrawableRes id: Int) = Uri.Builder()
+// .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+// .authority(context.resources.getResourcePackageName(id))
+// .appendPath(context.resources.getResourceTypeName(id))
+// .appendPath(context.resources.getResourceEntryName(id))
+// .build()
+//
+// private fun notSupported() = Futures.immediateFuture(
+// LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)
+// )
+
+
+// override fun onSearch(
+// session: MediaLibraryService.MediaLibrarySession,
+// browser: MediaSession.ControllerInfo,
+// query: String,
+// params: MediaLibraryService.LibraryParams?
+// ): ListenableFuture> {
+// if (extension !is SearchClient) return notSupported()
+// println("onSearch")
+// return scope.future(Dispatchers.IO) {
+// Log.d("BRUH", query)
+// launch {
+// extension.search(query).collectLatest {
+// differ.submitData(it)
+// }
+// }
+// delay(1000)
+// val list = differ.snapshot().items.map {
+// val track = (it as? MediaItemsContainer.TrackItem)?.track ?: return@map null
+// Log.d("BRUH", track.toString())
+// track
+// }.filterNotNull()
+// session.notifySearchResultChanged(browser, query, list.count(), params)
+// LibraryResult.ofVoid(params)
+// }
+// }
+//
+// override fun onGetSearchResult(
+// session: MediaLibraryService.MediaLibrarySession,
+// browser: MediaSession.ControllerInfo,
+// query: String,
+// page: Int,
+// pageSize: Int,
+// params: MediaLibraryService.LibraryParams?
+// ): ListenableFuture>> {
+// println("onGetSearchResult")
+// if (extension !is TrackClient) return notSupported()
+// if (extension !is SearchClient) return notSupported()
+//
+// return scope.future(Dispatchers.IO) {
+// Log.d("BRUH", query)
+// launch {
+// extension.search(query).collectLatest {
+// differ.submitData(it)
+// }
+// }
+// delay(1000)
+// val list = differ.snapshot().items.map {
+// val track = (it as? MediaItemsContainer.TrackItem)?.track ?: return@map null
+// val stream = extension.getStreamable(track)
+// PlayerHelper.mediaItemBuilder(track, stream)
+// }.filterNotNull()
+// LibraryResult.ofItemList(list, params)
+// }
+// }
+//
+// override fun onGetItem(
+// session: MediaLibraryService.MediaLibrarySession,
+// browser: MediaSession.ControllerInfo,
+// mediaId: String
+// ): ListenableFuture> {
+// println("onGetItem")
+// if (extension !is TrackClient) return notSupported()
+// return scope.future(Dispatchers.IO) {
+// val uri = mediaId.removePrefix("${SONG}/").toUri()
+// val track = extension.getTrack(uri)
+// ?: return@future LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN)
+// val stream = extension.getStreamable(track)
+// val item = PlayerHelper.mediaItemBuilder(track, stream)
+// LibraryResult.ofItem(item, null)
+// }
+// }
+//
+// companion object {
+// const val ROOT = "root"
+// const val SONG = "song"
+// const val ARTIST = "artist"
+// const val ALBUM = "album"
+// const val PLAYLIST = "playlist"
+// }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerUIViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerUIViewModel.kt
similarity index 94%
rename from app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerUIViewModel.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/player/PlayerUIViewModel.kt
index 85e8a789..fe5533ba 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerUIViewModel.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerUIViewModel.kt
@@ -1,4 +1,4 @@
-package dev.brahmkshatriya.echo.ui.player
+package dev.brahmkshatriya.echo.player
import androidx.lifecycle.ViewModel
import androidx.media3.session.MediaController
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt
similarity index 98%
rename from app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerViewModel.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt
index 1503dbd0..0b750c91 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerViewModel.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt
@@ -1,4 +1,4 @@
-package dev.brahmkshatriya.echo.ui.player
+package dev.brahmkshatriya.echo.player
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemAdapter.kt
index d37e5277..6304f14e 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemAdapter.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemAdapter.kt
@@ -7,7 +7,7 @@ import androidx.paging.PagingData
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
-import dev.brahmkshatriya.echo.data.models.MediaItem
+import dev.brahmkshatriya.echo.data.models.EchoMediaItem
import dev.brahmkshatriya.echo.data.models.Track
import dev.brahmkshatriya.echo.databinding.ItemMediaBinding
import dev.brahmkshatriya.echo.ui.utils.loadInto
@@ -15,7 +15,7 @@ import dev.brahmkshatriya.echo.ui.utils.loadInto
class MediaItemAdapter(
private val listener: ClickListener