diff --git a/README.md b/README.md
index 7ba7b35..d0dd773 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
A fully-featured Jellyfin music and audiobook player for Android Automotive OS (AAOS) with offline download capabilities.
+[
](https://play.google.com/store/apps/details?id=com.chamika.dashtune)
+
## Overview
DashTune brings the complete Jellyfin music library experience to your car's infotainment system. Stream your music collection, browse audiobooks, download tracks for offline playback, and enjoy seamless integration with Android Automotive OS.
diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts
index db8b046..51f793b 100644
--- a/automotive/build.gradle.kts
+++ b/automotive/build.gradle.kts
@@ -14,8 +14,8 @@ android {
applicationId = "com.chamika.dashtune"
minSdk = 28
targetSdk = 36
- versionCode = 16
- versionName = "1.2.0"
+ versionCode = 17
+ versionName = "1.2.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt
index 3c40ec1..99db671 100644
--- a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt
+++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt
@@ -423,8 +423,11 @@ class DashTuneSessionCallback(
}
(session as MediaLibraryService.MediaLibrarySession).notifyChildrenChanged(ROOT_ID, 4, null)
android.widget.Toast.makeText(service, R.string.library_synced, android.widget.Toast.LENGTH_SHORT).show()
+ SessionResult(SessionResult.RESULT_SUCCESS)
+ } else {
+ android.widget.Toast.makeText(service, R.string.sync_failed, android.widget.Toast.LENGTH_SHORT).show()
+ SessionResult(SessionError.ERROR_UNKNOWN)
}
- SessionResult(SessionResult.RESULT_SUCCESS)
}
}
}
diff --git a/automotive/src/main/java/com/chamika/dashtune/settings/SettingsFragment.kt b/automotive/src/main/java/com/chamika/dashtune/settings/SettingsFragment.kt
index 97b047b..f20a986 100644
--- a/automotive/src/main/java/com/chamika/dashtune/settings/SettingsFragment.kt
+++ b/automotive/src/main/java/com/chamika/dashtune/settings/SettingsFragment.kt
@@ -1,23 +1,35 @@
package com.chamika.dashtune.settings
import android.app.AlertDialog
+import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
+import androidx.media3.session.MediaController
+import androidx.media3.session.SessionCommand
+import androidx.media3.session.SessionResult
+import androidx.media3.session.SessionToken
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import com.chamika.dashtune.DashTuneMusicService
+import com.chamika.dashtune.DashTuneSessionCallback.Companion.SYNC_COMMAND
import com.chamika.dashtune.R
import com.chamika.dashtune.signin.SignInActivity
+import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
+import java.text.DateFormat
+import java.util.Date
@AndroidEntryPoint
class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var viewModel: SettingsViewModel
+ private lateinit var controllerFuture: ListenableFuture
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
@@ -41,6 +53,28 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
+ val syncPref = findPreference("sync_library")
+ syncPref?.summary = lastSyncSummary()
+ syncPref?.setOnPreferenceClickListener {
+ syncPref.isEnabled = false
+ val controller = if (controllerFuture.isDone) controllerFuture.get() else null
+ if (controller == null) {
+ syncPref.isEnabled = true
+ return@setOnPreferenceClickListener true
+ }
+ val resultFuture = controller.sendCustomCommand(
+ SessionCommand(SYNC_COMMAND, Bundle.EMPTY), Bundle.EMPTY
+ )
+ resultFuture.addListener({
+ val result = resultFuture.get()
+ if (result.resultCode == SessionResult.RESULT_SUCCESS) {
+ syncPref.summary = lastSyncSummary()
+ }
+ syncPref.isEnabled = true
+ }, requireActivity().mainExecutor)
+ true
+ }
+
findPreference("sign_out")?.setOnPreferenceClickListener {
AlertDialog.Builder(requireContext())
.setMessage(R.string.sign_out_confirmation)
@@ -57,4 +91,30 @@ class SettingsFragment : PreferenceFragmentCompat() {
true
}
}
+
+ override fun onStart() {
+ super.onStart()
+ val token = SessionToken(
+ requireContext(),
+ ComponentName(requireContext(), DashTuneMusicService::class.java)
+ )
+ controllerFuture = MediaController.Builder(requireContext(), token).buildAsync()
+ }
+
+ override fun onStop() {
+ MediaController.releaseFuture(controllerFuture)
+ super.onStop()
+ }
+
+ private fun lastSyncSummary(): String {
+ val lastSync = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .getLong("last_sync_timestamp", 0L)
+ return if (lastSync > 0) {
+ val formatted = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT)
+ .format(Date(lastSync))
+ getString(R.string.sync_library_last_synced, formatted)
+ } else {
+ getString(R.string.sync_library_never)
+ }
+ }
}
diff --git a/automotive/src/main/res/values/strings.xml b/automotive/src/main/res/values/strings.xml
index 26ed54f..7fbe5fb 100644
--- a/automotive/src/main/res/values/strings.xml
+++ b/automotive/src/main/res/values/strings.xml
@@ -36,4 +36,8 @@
Select at most 4 categories
Cache favourites
Pre-download all favourite songs for offline playback
+ Sync library
+ Never synced
+ Last synced: %s
+ Sync failed
diff --git a/automotive/src/main/res/xml/preferences.xml b/automotive/src/main/res/xml/preferences.xml
index 21400ca..8f650a2 100644
--- a/automotive/src/main/res/xml/preferences.xml
+++ b/automotive/src/main/res/xml/preferences.xml
@@ -39,6 +39,10 @@
android:summary="@string/browse_categories_summary"
android:title="@string/browse_categories" />
+
+