Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions automotive/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ android {
applicationId = "com.chamika.dashtune"
minSdk = 28
targetSdk = 36
versionCode = 20
versionName = "1.2.4"
versionCode = 21
versionName = "1.2.5"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import okio.buffer
import okio.sink
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class AlbumArtContentProvider : ContentProvider() {

private val client = OkHttpClient()
private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()

companion object {
private val uriMap = mutableMapOf<Uri, Uri>()
Expand Down Expand Up @@ -86,28 +90,38 @@ class AlbumArtContentProvider : ContentProvider() {
.build()

Log.d(LOG_TAG, "Downloading $remoteUri ...")
client.newCall(request).execute().use {
if (it.code == 200) {
Log.d(LOG_TAG, "Downloaded $remoteUri")
val source = it.body.source()
source.request(Long.MAX_VALUE)

val sink = tmpFile.sink().buffer()
sink.writeAll(source)
sink.flush()
sink.close()

tmpFile.renameTo(file)
} else {
Log.w(LOG_TAG, "Failed to download $remoteUri: \n ${it.code} - ${it.body}")
FirebaseUtils.safeSetCustomKey("album_art_path", remoteUri.path ?: "unknown")
FirebaseUtils.safeRecordException(Exception("Album art download failed: HTTP ${it.code}"))
try {
client.newCall(request).execute().use {
if (it.code == 200) {
Log.d(LOG_TAG, "Downloaded $remoteUri")
val source = it.body.source()
source.request(Long.MAX_VALUE)

val sink = tmpFile.sink().buffer()
sink.writeAll(source)
sink.flush()
sink.close()

tmpFile.renameTo(file)
} else {
Log.w(LOG_TAG, "Failed to download $remoteUri: \n ${it.code} - ${it.body}")
FirebaseUtils.safeSetCustomKey("album_art_path", remoteUri.path ?: "unknown")
FirebaseUtils.safeRecordException(Exception("Album art download failed: HTTP ${it.code}"))
}
}
} catch (e: IOException) {
Log.w(LOG_TAG, "Network error downloading $remoteUri", e)
} finally {
tmpFile.delete()
synchronized(inProgress) {
inProgress[remoteUri]?.countDown()
inProgress.remove(remoteUri)
}

inProgress.get(remoteUri)?.countDown()
inProgress.remove(remoteUri)
}

if (!file.exists()) {
throw FileNotFoundException(uri.path)
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,23 @@ class DashTuneMusicService : MediaLibraryService() {
}

playerListener = object : Player.Listener {
override fun onPlayerError(error: androidx.media3.common.PlaybackException) {
val p = mediaLibrarySession.player
val failedId = p.currentMediaItem?.mediaId
Log.e(LOG_TAG, "Player error on $failedId: ${error.errorCodeName}", error)
FirebaseUtils.safeSetCustomKey("player_error_code", error.errorCodeName)
FirebaseUtils.safeSetCustomKey("failed_track_id", failedId ?: "")
FirebaseUtils.safeRecordException(error)

if (p.hasNextMediaItem()) {
p.seekToNextMediaItem()
p.prepare()
} else {
p.stop()
p.clearMediaItems()
}
}

override fun onEvents(player: Player, events: Player.Events) {
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
// Save audiobook position for the OLD track before transition
Expand Down Expand Up @@ -419,6 +436,24 @@ class DashTuneMusicService : MediaLibraryService() {
super.onUpdateNotification(session, startInForegroundRequired)
}

fun isTrackCachedOrOnline(mediaId: String): Boolean {
if (hasInternet()) return true
return try {
val state = downloadManager.downloadIndex.getDownload(mediaId)?.state
state == Download.STATE_COMPLETED
} catch (e: Exception) {
Log.w(LOG_TAG, "Failed to check cache for $mediaId", e)
false
}
}

private fun hasInternet(): Boolean {
val net = connectivityManager.activeNetwork ?: return false
val caps = connectivityManager.getNetworkCapabilities(net) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}

fun onLogin() {
FirebaseUtils.safeLog("Login applied to service")
jellyfinApi.update(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,28 +124,25 @@ class DashTuneSessionCallback(
): ListenableFuture<LibraryResult<MediaItem>> {
Log.i(LOG_TAG, "onGetRoot")

if (!accountManager.isAuthenticated) {
return Futures.immediateFuture(
LibraryResult.ofError(
SessionError(
SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
service.getString(R.string.sign_in_to_your_jellyfin_server)
),
MediaLibraryService.LibraryParams.Builder()
.setExtras(authenticationExtras()).build()
)
)
}

val artSize = params?.extras?.getInt(EXTRAS_KEY_MEDIA_ART_SIZE_PIXELS)
ensureTreeInitialized(artSize)

// Always return a valid root MediaItem. Returning LibraryResult.ofError here
// causes the Media3 -> legacy MediaBrowserService bridge to deliver a null root
// to the AAOS Media Center, which then fails with onConnectFailed and shows
// a blank screen instead of the sign-in button. The unauthenticated case is
// surfaced via the auth extras on the root params and the auth check in
// onGetChildren.
return SuspendToFutureAdapter.launchFuture {
try {
LibraryResult.ofItem(
repository.getItem(ROOT_ID),
val rootItem = repository.getItem(ROOT_ID)
val outParams = if (!accountManager.isAuthenticated) {
MediaLibraryService.LibraryParams.Builder()
.setExtras(authenticationExtras()).build()
} else {
params
)
}
LibraryResult.ofItem(rootItem, outParams)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to get library root", e)
FirebaseUtils.safeSetCustomKey("failed_operation", "get_library_root")
Expand Down Expand Up @@ -455,21 +452,35 @@ class DashTuneSessionCallback(
try {
val prefs = PreferenceManager.getDefaultSharedPreferences(service)

val mediaItemsToRestore = prefs
val savedIds = prefs
.getString(PLAYLIST_IDS_PREF, "")
?.split(",")
?.filter { it.isNotEmpty() }
?.map { async { repository.getItem(it) } }
?.awaitAll() ?: listOf()
?: emptyList()

val playableIds = savedIds.filter { service.isTrackCachedOrOnline(it) }
if (playableIds.size < savedIds.size) {
Log.i(LOG_TAG, "Skipping ${savedIds.size - playableIds.size} unplayable tracks (offline + uncached)")
}

val mediaItemsToRestore = playableIds
.map { async { repository.getItem(it) } }
.awaitAll()

Log.d(LOG_TAG, "Resuming playback with $mediaItemsToRestore")
FirebaseUtils.safeLog("Restoring playback: index=${prefs.getInt(PLAYLIST_INDEX_PREF, 0)}, trackCount=${mediaItemsToRestore.size}")

MediaSession.MediaItemsWithStartPosition(
mediaItemsToRestore,
prefs.getInt(PLAYLIST_INDEX_PREF, 0),
prefs.getLong(PLAYLIST_TRACK_POSITON_MS_PREF, 0),
)
if (mediaItemsToRestore.isEmpty()) {
MediaSession.MediaItemsWithStartPosition(emptyList(), 0, 0L)
} else {
val savedIndex = prefs.getInt(PLAYLIST_INDEX_PREF, 0)
.coerceIn(0, mediaItemsToRestore.lastIndex)
MediaSession.MediaItemsWithStartPosition(
mediaItemsToRestore,
savedIndex,
prefs.getLong(PLAYLIST_TRACK_POSITON_MS_PREF, 0),
)
}
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to resume playback", e)
FirebaseUtils.safeRecordException(e)
Expand Down
Loading