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 ) : - PagingDataAdapter( + PagingDataAdapter( MediaItemComparator ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MediaItemHolder( @@ -27,7 +27,7 @@ class MediaItemAdapter( val item = getItem(position) ?: return val binding = holder.binding when (item) { - is MediaItem.TrackItem -> { + is EchoMediaItem.TrackItem -> { binding.title.text = item.track.title item.track.cover?.loadInto(binding.imageView) binding.root.setOnClickListener { @@ -39,43 +39,43 @@ class MediaItemAdapter( } } - is MediaItem.AlbumItem -> { + is EchoMediaItem.AlbumItem -> { binding.title.text = item.album.title item.album.cover?.loadInto(binding.imageView) } - is MediaItem.ArtistItem -> { + is EchoMediaItem.ArtistItem -> { binding.title.text = item.artist.name item.artist.cover?.loadInto(binding.imageView) } - is MediaItem.PlaylistItem -> { + is EchoMediaItem.PlaylistItem -> { binding.title.text = item.playlist.title item.playlist.cover?.loadInto(binding.imageView) } } } - fun submitData(lifecycle: Lifecycle, list: List) { + fun submitData(lifecycle: Lifecycle, list: List) { submitData(lifecycle, PagingData.from(list)) } class MediaItemHolder(val binding: ItemMediaBinding) : RecyclerView.ViewHolder(binding.root) - companion object MediaItemComparator : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MediaItem, newItem: MediaItem): Boolean { - if (oldItem is MediaItem.TrackItem && newItem is MediaItem.TrackItem) + companion object MediaItemComparator : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: EchoMediaItem, newItem: EchoMediaItem): Boolean { + if (oldItem is EchoMediaItem.TrackItem && newItem is EchoMediaItem.TrackItem) return oldItem.track.uri == newItem.track.uri - if (oldItem is MediaItem.AlbumItem && newItem is MediaItem.AlbumItem) + if (oldItem is EchoMediaItem.AlbumItem && newItem is EchoMediaItem.AlbumItem) return oldItem.album.uri == newItem.album.uri - if (oldItem is MediaItem.ArtistItem && newItem is MediaItem.ArtistItem) + if (oldItem is EchoMediaItem.ArtistItem && newItem is EchoMediaItem.ArtistItem) return oldItem.artist.uri == newItem.artist.uri - if (oldItem is MediaItem.PlaylistItem && newItem is MediaItem.PlaylistItem) + if (oldItem is EchoMediaItem.PlaylistItem && newItem is EchoMediaItem.PlaylistItem) return oldItem.playlist.uri == newItem.playlist.uri return false } - override fun areContentsTheSame(oldItem: MediaItem, newItem: MediaItem): Boolean { + override fun areContentsTheSame(oldItem: EchoMediaItem, newItem: EchoMediaItem): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemsContainerAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemsContainerAdapter.kt index 4c7d52b4..279e062f 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemsContainerAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemsContainerAdapter.kt @@ -16,7 +16,7 @@ import dev.brahmkshatriya.echo.data.models.MediaItemsContainer import dev.brahmkshatriya.echo.data.models.Track import dev.brahmkshatriya.echo.databinding.ItemCategoryBinding import dev.brahmkshatriya.echo.databinding.ItemTrackBinding -import dev.brahmkshatriya.echo.ui.player.PlayerHelper.Companion.toTimeString +import dev.brahmkshatriya.echo.player.PlayerHelper.Companion.toTimeString import dev.brahmkshatriya.echo.ui.utils.loadInto class MediaItemsContainerAdapter( diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeFragment.kt index a7710142..9eb75f6e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeFragment.kt @@ -16,8 +16,8 @@ import dev.brahmkshatriya.echo.databinding.FragmentRecyclerBinding import dev.brahmkshatriya.echo.ui.adapters.ClickListener import dev.brahmkshatriya.echo.ui.adapters.HeaderAdapter import dev.brahmkshatriya.echo.ui.adapters.MediaItemsContainerAdapter -import dev.brahmkshatriya.echo.ui.player.PlayerBackButtonHelper -import dev.brahmkshatriya.echo.ui.player.PlayerViewModel +import dev.brahmkshatriya.echo.player.PlayerBackButtonHelper +import dev.brahmkshatriya.echo.player.PlayerViewModel import dev.brahmkshatriya.echo.ui.utils.autoCleared import dev.brahmkshatriya.echo.ui.utils.dpToPx import dev.brahmkshatriya.echo.ui.utils.observe diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt index c304ce1b..1b459741 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt @@ -11,7 +11,7 @@ import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.databinding.FragmentRecyclerBinding import dev.brahmkshatriya.echo.ui.adapters.ContainerLoadingAdapter import dev.brahmkshatriya.echo.ui.adapters.HeaderAdapter -import dev.brahmkshatriya.echo.ui.player.PlayerBackButtonHelper +import dev.brahmkshatriya.echo.player.PlayerBackButtonHelper import dev.brahmkshatriya.echo.ui.utils.autoCleared import dev.brahmkshatriya.echo.ui.utils.dpToPx import dev.brahmkshatriya.echo.ui.utils.updatePaddingWithSystemInsets diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/search/SearchFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/search/SearchFragment.kt index 299a4b03..25b01fc6 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/search/SearchFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/search/SearchFragment.kt @@ -14,8 +14,8 @@ import dev.brahmkshatriya.echo.databinding.FragmentSearchBinding import dev.brahmkshatriya.echo.ui.adapters.ClickListener import dev.brahmkshatriya.echo.ui.adapters.MediaItemsContainerAdapter import dev.brahmkshatriya.echo.ui.adapters.SearchHeaderAdapter -import dev.brahmkshatriya.echo.ui.player.PlayerBackButtonHelper -import dev.brahmkshatriya.echo.ui.player.PlayerViewModel +import dev.brahmkshatriya.echo.player.PlayerBackButtonHelper +import dev.brahmkshatriya.echo.player.PlayerViewModel import dev.brahmkshatriya.echo.ui.utils.observe import dev.brahmkshatriya.echo.ui.utils.updatePaddingWithSystemInsets