Skip to content

Commit

Permalink
Online browsing for Android Auto
Browse files Browse the repository at this point in the history
  • Loading branch information
maxrave-dev committed Feb 14, 2024
1 parent 3299eac commit 861dac5
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 13 deletions.
4 changes: 2 additions & 2 deletions app/release/output-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 15,
"versionName": "0.1.8",
"versionCode": 16,
"versionName": "0.2.0",
"outputFile": "app-release.apk"
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,7 @@ class MainRepository @Inject constructor(private val localDataSource: LocalDataS
}.flowOn(Dispatchers.IO)

fun getFullMetadata(videoId: String): Flow<YouTubeInitialPage?> = flow {
Log.w("getFullMetadata", "videoId: $videoId")
YouTube.getFullMetadata(videoId).onSuccess {
emit(it)
}.onFailure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class SimpleMediaService : MediaLibraryService() {
setMediaNotificationProvider(
DefaultMediaNotificationProvider(this, { MEDIA_NOTIFICATION.NOTIFICATION_ID }, MEDIA_NOTIFICATION.NOTIFICATION_CHANNEL_ID, R.string.notification_channel_name)
.apply {
setSmallIcon(R.drawable.monochrome)
setSmallIcon(R.drawable.mono)
}
)
player = ExoPlayer.Builder(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ import com.google.common.util.concurrent.ListenableFuture
import com.maxrave.simpmusic.R
import com.maxrave.simpmusic.common.MEDIA_CUSTOM_COMMAND
import com.maxrave.simpmusic.data.db.entities.SongEntity
import com.maxrave.simpmusic.data.model.browse.album.Track
import com.maxrave.simpmusic.data.model.home.HomeItem
import com.maxrave.simpmusic.data.repository.MainRepository
import com.maxrave.simpmusic.extension.connectArtists
import com.maxrave.simpmusic.extension.toListName
import com.maxrave.simpmusic.extension.toListTrack
import com.maxrave.simpmusic.extension.toMediaItem
import com.maxrave.simpmusic.extension.toPlaylistEntity
import com.maxrave.simpmusic.extension.toSongEntity
import com.maxrave.simpmusic.extension.toTrack
import com.maxrave.simpmusic.utils.Resource
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -39,6 +48,8 @@ class SimpleMediaSessionCallback @Inject constructor(
) : MediaLibrarySession.Callback {
var toggleLike: () -> Unit = {}
private val scope = CoroutineScope(Dispatchers.Main + Job())
val searchTempList = mutableListOf<Track>()
val listHomeItem = mutableListOf<HomeItem>()

override fun onConnect(
session: MediaSession,
Expand Down Expand Up @@ -100,6 +111,51 @@ class SimpleMediaSessionCallback @Inject constructor(
)
)

override fun onSearch(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<Void>> = scope.future(Dispatchers.IO) {
val searchResult = mainRepository.getSearchDataSong(query).first().let { resource ->
when (resource) {
is Resource.Success -> {
resource.data?.let {
searchTempList.clear()
searchTempList.addAll(it.toListTrack())
}
resource.data
}

else -> emptyList()
}
}
if (searchResult != null) {
session.notifySearchResultChanged(browser, query, searchResult.size, params)
}
LibraryResult.ofVoid()
}

@UnstableApi
override fun onGetSearchResult(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
return scope.future(Dispatchers.IO) {
LibraryResult.ofItemList(
searchTempList.map {
it.toMediaItemWithoutPath()
},
params
)
}
}

@UnstableApi
override fun onGetChildren(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
Expand All @@ -108,9 +164,24 @@ class SimpleMediaSessionCallback @Inject constructor(
pageSize: Int,
params: MediaLibraryService.LibraryParams?,
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> = scope.future(Dispatchers.IO) {
LibraryResult.ofItemList(
val rootExtras = Bundle().apply {
putBoolean(
MEDIA_SEARCH_SUPPORTED,
true
)
}
val libraryParams =
MediaLibraryService.LibraryParams.Builder().setExtras(rootExtras).build()
return@future LibraryResult.ofItemList(
when (parentId) {
ROOT -> listOf(
browsableMediaItem(
HOME,
context.getString(R.string.home),
context.getString(R.string.available_online),
drawableUri(R.drawable.home_android_auto),
MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
),
browsableMediaItem(
SONG,
context.getString(R.string.songs),
Expand Down Expand Up @@ -142,8 +213,65 @@ class SimpleMediaSessionCallback @Inject constructor(
)
}

HOME -> {
val temp = mainRepository.getHomeData().first().data
listHomeItem.clear()
listHomeItem.addAll(temp ?: emptyList())
temp?.map {
browsableMediaItem(
"$HOME/${it.title}",
it.title,
it.subtitle,
null,
MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
)
} ?: emptyList()
}

else -> {
when {
parentId.startsWith("$HOME/") -> {
if (parentId.split("/").size == 2) {
val homeItem = listHomeItem.find {
it.title == parentId.split("/").getOrNull(1)
}
homeItem?.contents?.filter { it?.playlistId != null || it?.videoId != null }
?.mapNotNull {
if (it?.playlistId != null) {
browsableMediaItem(
id = "$HOME/${homeItem.title}/${PLAYLIST}/${it.playlistId}",
title = it.title,
subtitle = it.description,
iconUri = it.thumbnails.lastOrNull()?.url?.toUri(),
mediaType = MediaMetadata.MEDIA_TYPE_PLAYLIST
)
} else if (it?.videoId != null) {
it.toTrack().toSongEntity()
.toMediaItem("$HOME/${homeItem.title}/$SONG")
} else null
}
?: emptyList()
} else {
val playlistId = parentId.split("/").getOrNull(3)
if (playlistId != null) {
val playlist =
mainRepository.getPlaylistData(playlistId).first()
if (playlist.data?.tracks.isNullOrEmpty()) {
emptyList()
} else {
mainRepository.insertPlaylist(playlist.data!!.toPlaylistEntity())
playlist.data.tracks.map { track ->
track.toSongEntity().also {
mainRepository.insertSong(it).first()
}.toMediaItem(parentId)
}
}
} else {
emptyList()
}
}
}

parentId.startsWith("$PLAYLIST/") -> {
val playlistId = parentId.split("/").getOrNull(1)
if (playlistId != null) {
Expand All @@ -166,7 +294,7 @@ class SimpleMediaSessionCallback @Inject constructor(
}

},
params
libraryParams
)
}

Expand All @@ -176,8 +304,10 @@ class SimpleMediaSessionCallback @Inject constructor(
browser: MediaSession.ControllerInfo,
mediaId: String,
): ListenableFuture<LibraryResult<MediaItem>> = scope.future(Dispatchers.IO) {
mainRepository.getSongById(mediaId).first()?.toMediaItem()?.let {
LibraryResult.ofItem(it, null)
mainRepository.getSongById(mediaId).first()?.let {
LibraryResult.ofItem(it.toMediaItem(), null)
} ?: mainRepository.getFullMetadata(mediaId).first()?.let {
LibraryResult.ofItem(it.toTrack().toMediaItemWithoutPath(), null)
} ?: LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN)
}

Expand All @@ -198,11 +328,25 @@ class SimpleMediaSessionCallback @Inject constructor(
SONG -> {
val songId = path.getOrNull(1) ?: return@future defaultResult
val allSongs = mainRepository.getAllSongs().first().sortedBy { it.inLibrary }
MediaSession.MediaItemsWithStartPosition(
allSongs.map { it.toMediaItem() },
allSongs.indexOfFirst { it.videoId == songId }.takeIf { it != -1 } ?: 0,
startPositionMs
)
if (allSongs.find { it.videoId == songId } == null) {
val song = searchTempList.find { it.videoId == songId }
?: mainRepository.getFullMetadata(songId).first()?.toTrack()
?: return@future defaultResult
mainRepository.insertSong(song.toSongEntity()).first()
val cloneList = allSongs.toMutableList()
cloneList.add(0, song.toSongEntity())
MediaSession.MediaItemsWithStartPosition(
cloneList.map { it.toMediaItem() },
cloneList.indexOfFirst { it.videoId == songId }.takeIf { it != -1 } ?: 0,
startPositionMs
)
} else {
MediaSession.MediaItemsWithStartPosition(
allSongs.map { it.toMediaItem() },
allSongs.indexOfFirst { it.videoId == songId }.takeIf { it != -1 } ?: 0,
startPositionMs
)
}
}

PLAYLIST -> {
Expand All @@ -225,6 +369,49 @@ class SimpleMediaSessionCallback @Inject constructor(
}
}

HOME -> {
val type = path.getOrNull(2) ?: return@future defaultResult
val content = listHomeItem.find { it.title == path.getOrNull(1) }?.contents
if (type == SONG) {
val songId = path.getOrNull(3) ?: return@future defaultResult
val songs = content?.filter { it?.videoId != null }?.mapNotNull {
it?.toTrack()
}
if (songs.isNullOrEmpty()) {
defaultResult
} else {
songs.forEach {
mainRepository.insertSong(it.toSongEntity()).first()
}
MediaSession.MediaItemsWithStartPosition(
songs.map { it.toMediaItem() },
songs.indexOfFirst { it.videoId == songId }.takeIf { it != -1 } ?: 0,
startPositionMs
)
}
} else if (type == PLAYLIST) {
val songId = path.getOrNull(4) ?: return@future defaultResult
val playlistId = path.getOrNull(3) ?: return@future defaultResult
val playlistEntity = mainRepository.getPlaylist(playlistId).first()
if (playlistEntity?.tracks.isNullOrEmpty()) {
defaultResult
} else if (playlistEntity?.tracks?.isNotEmpty() == true) {
playlistEntity.tracks.let { it ->
mainRepository.getSongsByListVideoId(it).first()
.map { it.toMediaItem() }
}
.let { mediaItemList ->
MediaSession.MediaItemsWithStartPosition(
mediaItemList,
mediaItemList.indexOfFirst { it.mediaId == songId }
.takeIf { it != -1 } ?: 0,
startPositionMs
)
}
} else defaultResult
} else return@future defaultResult
}

else -> defaultResult
}
}
Expand Down Expand Up @@ -274,9 +461,28 @@ class SimpleMediaSessionCallback @Inject constructor(
)
.build()

private fun Track.toMediaItemWithoutPath(path: String? = SONG) =
MediaItem.Builder()
.setMediaId(if (path == null) this.videoId else "$path/${this.videoId}")
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(this.title)
.setSubtitle(this.artists?.toListName()?.connectArtists())
.setArtist(this.artists?.toListName()?.connectArtists())
.setArtworkUri(this.thumbnails?.lastOrNull()?.url?.toUri())
.setIsPlayable(true)
.setIsBrowsable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
.build()
)
.build()

companion object {
const val ROOT = "root"
const val SONG = "song"
const val HOME = "home"
const val ONLINE_PLAYLIST = "online_playlist"
const val PLAYLIST = "playlist"
const val MEDIA_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class MusicDownloadService : DownloadService(
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
downloadUtil.downloadNotificationHelper.buildProgressNotification(
this,
R.drawable.monochrome,
R.drawable.mono,
null,
if (downloads.size == 1) Util.fromUtf8Bytes(downloads[0].request.data)
else resources.getQuantityString(R.plurals.n_song, downloads.size, downloads.size),
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/home_android_auto.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M240,760L360,760L360,520L600,520L600,760L720,760L720,400L480,220L240,400L240,760ZM160,840L160,360L480,120L800,360L800,840L520,840L520,600L440,600L440,840L160,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z" />
</vector>
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/mono.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector android:height="48dp"
android:viewportHeight="258"
android:viewportWidth="258"
android:width="48dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#0AEAF8"
android:fillType="evenOdd"
android:pathData="M99.31,44.39C96.16,45.4 93.66,47.53 92.45,50.22L91.76,51.75L91.85,85.31C91.89,103.77 92,119.14 92.1,119.46C92.23,119.92 92.16,119.99 91.7,119.81C91.39,119.69 90.11,119.31 88.86,118.98C79.63,116.51 69.09,120.14 64.8,127.25C60.78,133.93 63.25,144.73 69.98,149.85C73.86,152.81 78.96,154.79 85.69,155.97C88.78,156.51 90.85,156.67 95.37,156.69C100.7,156.71 101.29,156.66 103.25,155.98C108.71,154.09 112.59,149.92 114.06,144.38C114.82,141.47 114.82,141.47 114.49,107.82C114.32,90.1 114.33,80.89 114.52,80.89C114.69,80.89 122.51,84.34 131.9,88.56C141.29,92.77 151.7,97.43 155.03,98.9C158.36,100.37 163.27,102.54 165.93,103.71L170.78,105.85L171,109.77C171.21,113.48 172.17,153.92 172.04,153.98C172.01,154 168.04,155.63 163.21,157.6C158.38,159.58 154.02,161.39 153.52,161.62C152.57,162.05 130.36,171.23 117.74,176.41C98.34,184.38 93.12,186.6 91.56,187.55C87.14,190.25 84.07,194.61 83.79,198.61C83.43,203.71 86.45,207.87 92.6,210.73C96.36,212.48 99.39,213.14 103.55,213.12C106.96,213.1 107.46,213.02 111.73,211.72C117.89,209.85 149.32,200.53 161.02,197.11C184.2,190.32 184.98,190.05 188.2,187.5C191.07,185.23 193.02,182.36 194.08,178.83C194.54,177.3 194.59,173.79 194.55,137.89C194.54,116.3 194.4,97.14 194.25,95.32C193.86,90.42 192.99,88.38 189.96,85.23C188.54,83.75 187.54,83.15 181.68,80.27C175.22,77.1 165.59,72.45 160.33,69.96C158.91,69.3 156.12,67.97 154.12,67.01C152.12,66.05 149.53,64.81 148.37,64.26C145.31,62.81 136.67,58.7 121.11,51.29C113.62,47.72 106.89,44.61 106.15,44.38C104.5,43.87 100.93,43.88 99.31,44.39Z" />
</vector>
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/changelogs/16.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
- Bypass restricted songs
- Load song faster
- Keep screen on when lyrics showing
- Online browsing for Android Auto
- Fixed some bugs. Full changelog: https://github.com/maxrave-dev/SimpMusic/compare/v0.1.8...v0.2.0
1 change: 1 addition & 0 deletions fastlane/metadata/android/vi-VN/changelogs/16.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
- Bỏ qua những bài hát bị hạn chế
- Tải bài hát nhanh hơn
- Giữ màn hình sáng khi hiển thị lời bài hát
- Duyệt trực tuyến trên Android Auto
- Sửa một số lỗi. Thay đổi đầy đủ: https://github.com/maxrave-dev/SimpMusic/compare/v0.1.8...v0.2.0

0 comments on commit 861dac5

Please sign in to comment.