Skip to content

Commit

Permalink
Add MediaLibraryService Support for Google Assistant, but it doesnt w…
Browse files Browse the repository at this point in the history
…ork, help needed
  • Loading branch information
brahmkshatriya committed Feb 16, 2024
1 parent dfb9df3 commit 64b5a41
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 13 deletions.
7 changes: 4 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,14 @@ 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")
implementation("androidx.core:core-splashscreen:1.0.1")
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")

Expand All @@ -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")

Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
android:permission="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>

Expand Down
93 changes: 85 additions & 8 deletions app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()

Expand All @@ -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 <T> notSupported() = Futures.immediateFuture(
LibraryResult.ofError<T>(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)
)

override fun onGetSearchResult(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
page: Int,
pageSize: Int,
params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
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<LibraryResult<MediaItem>> {
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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Track> {
fun getByArtist(
context: Context,
artist: Artist.Small,
page: Int,
pageSize: Int
): List<Track> {
val whereCondition = "${MediaStore.Audio.Media.ARTIST_ID} = ?"
val selectionArgs = arrayOf(artist.uri.lastPathSegment!!)
return context.queryTracks(whereCondition, selectionArgs, page, pageSize)
}

fun getByAlbum(context: Context, album: Album.Small, page: Int, pageSize: Int): List<Track> {
fun getByAlbum(
context: Context,
album: Album.Small,
page: Int,
pageSize: Int
): List<Track> {
val whereCondition = "${MediaStore.Audio.Media.ALBUM_ID} = ?"
val selectionArgs = arrayOf(album.uri.lastPathSegment!!)
return context.queryTracks(whereCondition, selectionArgs, page, pageSize)
Expand Down Expand Up @@ -116,6 +126,13 @@ interface LocalTrack {
}
return tracks
}

fun get(context: Context, uri: String): Track? {
val id = uri.substringAfterLast('/')
val whereCondition = "${MediaStore.Audio.Media._ID} = ?"
val selectionArgs = arrayOf(id)
return context.queryTracks(whereCondition, selectionArgs, 0, 1).firstOrNull()
}
}

}
4 changes: 4 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/di/PluginModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class PluginModule {
offline = it
}

@Provides
@Singleton
fun provideExtension(app: Application) : OfflineExtension = getOfflineExtension(app)

@Provides
@Singleton
fun provideSearchClient(app: Application) : SearchClient = getOfflineExtension(app)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import dev.brahmkshatriya.echo.data.models.MediaItemsContainer
import dev.brahmkshatriya.echo.data.models.Track
Expand Down Expand Up @@ -169,4 +170,11 @@ class MediaItemsContainerAdapter(
return true
}
}

class ListCallback : ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) {}
override fun onMoved(fromPosition: Int, toPosition: Int) {}
override fun onInserted(position: Int, count: Int) {}
override fun onRemoved(position: Int, count: Int) {}
}
}

0 comments on commit 64b5a41

Please sign in to comment.