Skip to content

Commit

Permalink
Android Auto improvements
Browse files Browse the repository at this point in the history
[Android] Android Auto now shows more options for items to play, and
doesn't crash anymore
  • Loading branch information
Carlos Perez committed Jul 10, 2022
1 parent b029c03 commit c1a8d6e
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,53 @@ class KeyValueStorage {
}
}

fun getCachedPlaylists(): List<Playlist> {
val sharedPref =
App.context.getSharedPreferences("sonicLair", Context.MODE_PRIVATE)
val activeAccount = sharedPref.getString("cachedPlaylists", "")
return try {
val mediaItems: List<Playlist> =
Gson().fromJson(activeAccount, Array<Playlist>::class.java)
.toList()
mediaItems
} catch (exception: Exception) {
emptyList()
}
}

fun setCachedPlaylists(accounts: List<Playlist>) {
val sharedPref =
App.context.getSharedPreferences("sonicLair", Context.MODE_PRIVATE)
with(sharedPref.edit()) {
val value = Gson().toJson(accounts)
putString("cachedPlaylists", value)
apply()
}
}
fun getCachedAlbums(): List<Album> {
val sharedPref =
App.context.getSharedPreferences("sonicLair", Context.MODE_PRIVATE)
val activeAccount = sharedPref.getString("cachedAlbums", "")
return try {
val mediaItems: List<Album> =
Gson().fromJson(activeAccount, Array<Album>::class.java)
.toList()
mediaItems
} catch (exception: Exception) {
emptyList()
}
}

fun setCachedAlbums(accounts: List<Album>) {
val sharedPref =
App.context.getSharedPreferences("sonicLair", Context.MODE_PRIVATE)
with(sharedPref.edit()) {
val value = Gson().toJson(accounts)
putString("cachedAlbums", value)
apply()
}
}

fun getOfflineMode(): Boolean {
val sharedPref =
App.context.getSharedPreferences("sonicLair", Context.MODE_PRIVATE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class SubsonicClient(var initialAccount: Account) {
)
}

fun getAsMediaItems(songs: List<Song>): List<MediaBrowserCompat.MediaItem> {
fun getSongsAsMediaItems(songs: List<Song>): List<MediaBrowserCompat.MediaItem> {
val builder = MediaDescriptionCompat.Builder()
val ret = mutableListOf<MediaBrowserCompat.MediaItem>()
for (item in songs) {
Expand Down Expand Up @@ -112,7 +112,85 @@ class SubsonicClient(var initialAccount: Account) {
}
Log.i("BitmapMediaItem", "Loaded successfully")
builder.setIconBitmap(albumArtBitmap)
builder.setMediaId(item.id)
builder.setMediaId("s${item.id}")
ret.add(
MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
return ret
}

fun getPlaylistsAsMediaItems(playlists: List<Playlist>): List<MediaBrowserCompat.MediaItem> {
val builder = MediaDescriptionCompat.Builder()
val ret = mutableListOf<MediaBrowserCompat.MediaItem>()
for (item in playlists) {
builder.setTitle(item.name)
builder.setSubtitle(item.comment)
val albumArtBitmap: Bitmap = if (File(getLocalCoverArtUri(item.coverArt ?: "")).exists()) {
Log.i("BitmapMediaItem", "Loading from disk")
BitmapFactory.decodeFile(getLocalCoverArtUri(item.coverArt ?: ""))
} else {
Log.i("BitmapMediaItem", "Loading from server")
val albumArtUri = Uri.parse(getAlbumArt(item.coverArt ?: ""))
try {
val futureBitmap = Glide.with(App.context)
.asBitmap()
.load(albumArtUri)
.submit()
futureBitmap.get()
} catch (e: Exception) {
val placeholder = Glide.with(App.context)
.asBitmap()
.load(R.drawable.ic_album_art_placeholder)
.submit()
placeholder.get()
}
}
Log.i("BitmapMediaItem", "Loaded successfully")
builder.setIconBitmap(albumArtBitmap)
builder.setMediaId("p${item.id}")
ret.add(
MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
return ret
}

fun getAlbumsAsPlaylistsItems(albums: List<Album>): List<MediaBrowserCompat.MediaItem> {
val builder = MediaDescriptionCompat.Builder()
val ret = mutableListOf<MediaBrowserCompat.MediaItem>()
for (item in albums) {
builder.setTitle(item.name)
builder.setSubtitle(String.format("by %s", item.artist))
val albumArtBitmap: Bitmap = if (File(getLocalCoverArtUri(item.id)).exists()) {
Log.i("BitmapMediaItem", "Loading from disk")
BitmapFactory.decodeFile(getLocalCoverArtUri(item.id))
} else {
Log.i("BitmapMediaItem", "Loading from server")
val albumArtUri = Uri.parse(getAlbumArt(item.id))
try {
val futureBitmap = Glide.with(App.context)
.asBitmap()
.load(albumArtUri)
.submit()
futureBitmap.get()
} catch (e: Exception) {
val placeholder = Glide.with(App.context)
.asBitmap()
.load(R.drawable.ic_album_art_placeholder)
.submit()
placeholder.get()
}
}
Log.i("BitmapMediaItem", "Loaded successfully")
builder.setIconBitmap(albumArtBitmap)
builder.setMediaId("a${item.id}")
ret.add(
MediaBrowserCompat.MediaItem(
builder.build(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Playlist(
val songCount: Int,
val duration: Int,
val created: String,
val coverArt: String,
val coverArt: String?,
val entry: List<Song>
) : ICardViewModel {
override fun firstLine(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package tech.logica10.soniclair.services
import android.app.PendingIntent
import android.app.SearchManager
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.media.MediaBrowserServiceCompat
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -39,35 +41,133 @@ class MediaBrowserService : MediaBrowserServiceCompat() {
"android.media.browse.CONTENT_STYLE_PLAYABLE_HINT",
2
)
return BrowserRoot("sonicLairRoot", extras)
return BrowserRoot("HOME", extras)
}

fun getHome(): List<MediaBrowserCompat.MediaItem> {
val builder = MediaDescriptionCompat.Builder()
val ret = mutableListOf<MediaBrowserCompat.MediaItem>()
val future = Glide.with(App.context)
.asBitmap()
.load(R.drawable.ic_album_art_placeholder)
.submit()
val albumArtBitmap: Bitmap;
runBlocking(Dispatchers.IO) {
albumArtBitmap = future.get()
}

// Random songs
builder.setTitle("Random Songs")
builder.setSubtitle("Rediscover your library!")
builder.setIconBitmap(albumArtBitmap)
builder.setMediaId("RANDOMSONGS")
ret.add(
MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
// Top Albums
builder.setTitle("Most Played Albums")
builder.setSubtitle("Jump back to your favourites")

builder.setIconBitmap(albumArtBitmap)
builder.setMediaId("MOSTPLAYED")
ret.add(
MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
// Playlists
builder.setTitle("Playlists")
builder.setSubtitle("Listen to your curated playlists")
builder.setIconBitmap(albumArtBitmap)
builder.setMediaId("PLAYLISTS")
ret.add(
MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
return ret
}

override fun onLoadChildren(
parentMediaId: String,
result: Result<List<MediaBrowserCompat.MediaItem>>
) {
// We're not logged in on a server, we bail
if (getActiveAccount().username == null) {
result.sendResult(listOf())
return
}
if (parentMediaId == "HOME") {
result.sendResult(getHome())
} else if (parentMediaId == "RANDOMSONGS") {
// We're not logged in on a server, we bail
if (getActiveAccount().username == null) {
result.sendResult(listOf())
return
}

// We try to return cached media items if we have any
val songs = KeyValueStorage.getCachedSongs()
// We try to return cached media items if we have any
val songs = KeyValueStorage.getCachedSongs()

// Fetch new songs for the next time
CoroutineScope(Dispatchers.IO).launch {
load(subsonicClient)
}
// We do have some songs!
if (songs.isNotEmpty()) {
runBlocking(Dispatchers.IO) {
result.sendResult(subsonicClient.getAsMediaItems(songs))
// Fetch new songs for the next time
CoroutineScope(Dispatchers.IO).launch {
load(subsonicClient)
}
// We do have some songs!
if (songs.isNotEmpty()) {
runBlocking(Dispatchers.IO) {
result.sendResult(subsonicClient.getSongsAsMediaItems(songs))
}
} else {
// We have nothing for the user at this time, returning an empty list so as to not block the UI.
result.sendResult(listOf())
}
} else if (parentMediaId == "PLAYLISTS") {
// We're not logged in on a server, we bail
if (getActiveAccount().username == null) {
result.sendResult(listOf())
return
}

// We try to return cached media items if we have any
val playlists = KeyValueStorage.getCachedPlaylists()

// Fetch new songs for the next time
CoroutineScope(Dispatchers.IO).launch {
load(subsonicClient)
}
// We do have some songs!
if (playlists.isNotEmpty()) {
runBlocking(Dispatchers.IO) {
result.sendResult(subsonicClient.getPlaylistsAsMediaItems(playlists))
}
} else {
// We have nothing for the user at this time, returning an empty list so as to not block the UI.
result.sendResult(listOf())
}
}else if (parentMediaId == "MOSTPLAYED") {
// We're not logged in on a server, we bail
if (getActiveAccount().username == null) {
result.sendResult(listOf())
return
}

// We try to return cached media items if we have any
val albums = KeyValueStorage.getCachedAlbums()

// Fetch new songs for the next time
CoroutineScope(Dispatchers.IO).launch {
load(subsonicClient)
}
// We do have some songs!
if (albums.isNotEmpty()) {
runBlocking(Dispatchers.IO) {
result.sendResult(subsonicClient.getAlbumsAsPlaylistsItems(albums))
}
} else {
// We have nothing for the user at this time, returning an empty list so as to not block the UI.
result.sendResult(listOf())
}
}
else{
// We have nothing for the user at this time, returning an empty list so as to not block the UI.
result.sendResult(listOf())
}
}

Expand All @@ -92,6 +192,10 @@ class MediaBrowserService : MediaBrowserServiceCompat() {
// Repopulate the media items cache
val songs = subsonicClient.getRandomSongs()
KeyValueStorage.setCachedSongs(songs)
val albums = subsonicClient.getTopAlbums()
KeyValueStorage.setCachedAlbums(albums)
val playlists = subsonicClient.getPlaylists()
KeyValueStorage.setCachedPlaylists(playlists.subList(0, if(playlists.size > 9) 9 else playlists.size))
} catch (e: Exception) {
// Something _awful_ happened. The user doesn't need to know about it
Log.e("MediaBrowser", e.message!!)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.getcapacitor.JSObject
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import org.json.JSONException
Expand Down Expand Up @@ -450,10 +451,32 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
}
"SLPREV" -> prev()
"SLNEXT" -> next()
"SLPLAYID" -> playRadio(value!!)
"SLPLAYSEARCH" -> playSearch(value!!, SearchType.SONG)
"SLPLAYSEARCHARTIST" -> playSearch(value!!, SearchType.ARTIST)
"SLPLAYSEARCHALBUM" -> playSearch(value!!, SearchType.ALBUM)
"SLPLAYID" -> {
CoroutineScope(Dispatchers.IO).launch {
val id = value!!.subSequence(1, value.length).toString()
val type = value.subSequence(0, 1)
when (type) {
"s" -> playRadio(id)
"a" -> playAlbum(id, 0)
"p" -> playPlaylist(id, 0)
}
}
}
"SLPLAYSEARCH" -> {
CoroutineScope(IO).launch {
playSearch(value!!, SearchType.SONG)
}
}
"SLPLAYSEARCHARTIST" -> {
CoroutineScope(IO).launch {
playSearch(value!!, SearchType.ARTIST)
}
}
"SLPLAYSEARCHALBUM" -> {
CoroutineScope(IO).launch {
playSearch(value!!, SearchType.ALBUM)
}
}
"SLCANCEL" -> {
Log.i("MusicService", "Stopping signal received. Stopping.")
stopForeground(true)
Expand Down Expand Up @@ -633,8 +656,8 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {

@Suppress("BlockingMethodInNonBlockingContext")
private fun play() {
if(currentTrack == null){
return;
if (currentTrack == null) {
return
}
wasPlaying = false
if (mMediaPlayer!!.media != null) {
Expand Down

1 comment on commit c1a8d6e

@vercel
Copy link

@vercel vercel bot commented on c1a8d6e Jul 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

soniclair – ./

soniclair-thelinkin3000.vercel.app
soniclair.vercel.app
soniclair-git-main-thelinkin3000.vercel.app

Please sign in to comment.