From 19a2e30d0b3cc9dc2f7a6605001e66817645e6da Mon Sep 17 00:00:00 2001
From: Boof2015 <75185879+Boof2015@users.noreply.github.com>
Date: Mon, 29 Jun 2026 17:29:45 -0400
Subject: [PATCH] android auto more stuff
---
.../astracar/AstraCarCommandService.kt | 4 ++++
.../astracar/AstraCarFavoriteAction.kt | 6 ++++++
.../modules/astracar/AstraCarMediaService.kt | 8 ++++++++
.../expo/modules/astracar/AstraCarModule.kt | 8 ++++++++
.../modules/astracar/AstraCarNowPlaying.kt | 18 ++++++++++++++++++
.../main/res/drawable/ic_astra_favorite.xml | 9 +++++++++
.../res/drawable/ic_astra_favorite_border.xml | 9 +++++++++
modules/astra-car/index.ts | 2 ++
src/audio/carSync.ts | 7 ++++++-
src/car/carPlayback.ts | 19 +++++++++++++++++++
10 files changed, 89 insertions(+), 1 deletion(-)
create mode 100644 modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarFavoriteAction.kt
create mode 100644 modules/astra-car/android/src/main/res/drawable/ic_astra_favorite.xml
create mode 100644 modules/astra-car/android/src/main/res/drawable/ic_astra_favorite_border.xml
diff --git a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarCommandService.kt b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarCommandService.kt
index 7737786..c1a01ae 100644
--- a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarCommandService.kt
+++ b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarCommandService.kt
@@ -116,6 +116,10 @@ class AstraCarCommandService : HeadlessJsTaskService() {
)
}
+ fun startFavoriteAction(context: Context) {
+ start(context, Intent(context, AstraCarCommandService::class.java).putExtra(EXTRA_COMMAND, AstraCarFavoriteAction.COMMAND))
+ }
+
fun startPlayFromMediaId(context: Context, mediaId: String?) {
val media = AstraCarMediaIds.decode(mediaId) ?: return
start(
diff --git a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarFavoriteAction.kt b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarFavoriteAction.kt
new file mode 100644
index 0000000..3500817
--- /dev/null
+++ b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarFavoriteAction.kt
@@ -0,0 +1,6 @@
+package expo.modules.astracar
+
+object AstraCarFavoriteAction {
+ const val TOGGLE = "astra.favorite.toggle"
+ const val COMMAND = "toggleFavorite"
+}
diff --git a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarMediaService.kt b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarMediaService.kt
index 56e7104..4ffd91a 100644
--- a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarMediaService.kt
+++ b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarMediaService.kt
@@ -51,6 +51,14 @@ class AstraCarMediaService : MediaBrowserServiceCompat() {
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
AstraCarCommandService.startPlayFromSearch(this@AstraCarMediaService, query, extras)
}
+
+ override fun onCustomAction(action: String?, extras: Bundle?) {
+ if (action == AstraCarFavoriteAction.TOGGLE) {
+ AstraCarCommandService.startFavoriteAction(this@AstraCarMediaService)
+ } else {
+ super.onCustomAction(action, extras)
+ }
+ }
},
)
applyAstraState(AstraCarNowPlayingStore.load(this@AstraCarMediaService))
diff --git a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarModule.kt b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarModule.kt
index dc7f2d4..a50296d 100644
--- a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarModule.kt
+++ b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarModule.kt
@@ -37,6 +37,12 @@ class AstraCarNowPlayingRecord : Record {
@Field
val position: Double? = null
+
+ @Field
+ val trackPath: String? = null
+
+ @Field
+ val isFavorite: Boolean = false
}
class AstraCarModule : Module() {
@@ -56,6 +62,8 @@ class AstraCarModule : Module() {
hasTrack = state.hasTrack,
durationSeconds = state.duration,
positionSeconds = state.position,
+ trackPath = state.trackPath,
+ isFavorite = state.isFavorite,
),
)
}
diff --git a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarNowPlaying.kt b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarNowPlaying.kt
index 452a1cd..f82c093 100644
--- a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarNowPlaying.kt
+++ b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarNowPlaying.kt
@@ -16,6 +16,8 @@ data class AstraCarNowPlayingState(
val hasTrack: Boolean,
val durationSeconds: Double?,
val positionSeconds: Double?,
+ val trackPath: String?,
+ val isFavorite: Boolean,
)
object AstraCarNowPlayingStore {
@@ -94,9 +96,19 @@ object AstraCarNowPlayingStore {
((state.positionSeconds ?: 0.0) * 1000).toLong(),
if (playbackState == PlaybackStateCompat.STATE_PLAYING) 1f else 0f,
)
+ .apply {
+ favoriteAction(state)?.let(::addCustomAction)
+ }
.build()
}
+ private fun favoriteAction(state: AstraCarNowPlayingState): PlaybackStateCompat.CustomAction? {
+ if (!state.hasTrack || state.trackPath.isNullOrBlank()) return null
+ val label = if (state.isFavorite) "Unfavorite" else "Favorite"
+ val icon = if (state.isFavorite) R.drawable.ic_astra_favorite else R.drawable.ic_astra_favorite_border
+ return PlaybackStateCompat.CustomAction.Builder(AstraCarFavoriteAction.TOGGLE, label, icon).build()
+ }
+
private fun encode(state: AstraCarNowPlayingState): String =
JSONObject()
.put("title", state.title)
@@ -107,6 +119,8 @@ object AstraCarNowPlayingStore {
.put("hasTrack", state.hasTrack)
.put("durationSeconds", state.durationSeconds)
.put("positionSeconds", state.positionSeconds)
+ .put("trackPath", state.trackPath)
+ .put("isFavorite", state.isFavorite)
.toString()
private fun decode(value: String?): AstraCarNowPlayingState {
@@ -122,6 +136,8 @@ object AstraCarNowPlayingStore {
hasTrack = json.optBoolean("hasTrack", false),
durationSeconds = json.optDoubleOrNull("durationSeconds"),
positionSeconds = json.optDoubleOrNull("positionSeconds"),
+ trackPath = json.optNullableString("trackPath"),
+ isFavorite = json.optBoolean("isFavorite", false),
)
}.getOrDefault(emptyState())
}
@@ -136,6 +152,8 @@ object AstraCarNowPlayingStore {
hasTrack = false,
durationSeconds = null,
positionSeconds = null,
+ trackPath = null,
+ isFavorite = false,
)
private fun JSONObject.optNullableString(key: String): String? {
diff --git a/modules/astra-car/android/src/main/res/drawable/ic_astra_favorite.xml b/modules/astra-car/android/src/main/res/drawable/ic_astra_favorite.xml
new file mode 100644
index 0000000..34d39fe
--- /dev/null
+++ b/modules/astra-car/android/src/main/res/drawable/ic_astra_favorite.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/modules/astra-car/android/src/main/res/drawable/ic_astra_favorite_border.xml b/modules/astra-car/android/src/main/res/drawable/ic_astra_favorite_border.xml
new file mode 100644
index 0000000..9bfc293
--- /dev/null
+++ b/modules/astra-car/android/src/main/res/drawable/ic_astra_favorite_border.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/modules/astra-car/index.ts b/modules/astra-car/index.ts
index b501a27..5f21e77 100644
--- a/modules/astra-car/index.ts
+++ b/modules/astra-car/index.ts
@@ -16,6 +16,8 @@ export interface AstraCarNowPlayingState {
hasTrack: boolean;
duration?: number | null;
position?: number | null;
+ trackPath?: string | null;
+ isFavorite?: boolean;
}
declare class AstraCarModuleType extends NativeModule {
diff --git a/src/audio/carSync.ts b/src/audio/carSync.ts
index 38eafeb..235e0cd 100644
--- a/src/audio/carSync.ts
+++ b/src/audio/carSync.ts
@@ -2,6 +2,7 @@ import TrackPlayer, { State, type Track as RntpTrack } from 'react-native-track-
import type { PlaybackState, Track } from '@/types/audio';
import { AstraCar } from '../../modules/astra-car';
+import { usePlaylistStore } from '@/stores/playlistStore';
function mapRntpState(state?: State): PlaybackState {
switch (state) {
@@ -20,7 +21,7 @@ function mapRntpState(state?: State): PlaybackState {
type CarNowPlayingTrack = Pick<
Track,
- 'title' | 'artist' | 'album' | 'artworkData' | 'duration' | 'sourceType' | 'sourceId' | 'artworkSourceId'
+ 'path' | 'title' | 'artist' | 'album' | 'artworkData' | 'duration' | 'sourceType' | 'sourceId' | 'artworkSourceId'
>;
/** True when the track should use its remote server cover (no local cache to serve). */
@@ -51,6 +52,7 @@ export function setCarNowPlaying(
// Android Auto loads art only from content:// URIs, so we pass structured identity
// (local hash or remote source+id) and let the native module build the content URI.
const remote = isRemoteArt(track);
+ const trackPath = track?.path ?? null;
AstraCar.setNowPlaying({
title: track?.title ?? null,
artist: track?.artist ?? null,
@@ -62,6 +64,8 @@ export function setCarNowPlaying(
hasTrack: Boolean(track),
duration: duration ?? track?.duration ?? null,
position: position ?? null,
+ trackPath,
+ isFavorite: trackPath ? usePlaylistStore.getState().favoritePaths.has(trackPath) : false,
});
}
@@ -74,6 +78,7 @@ export function setCarNowPlayingFromRntpTrack(
setCarNowPlaying(
track
? {
+ path: typeof track.astraPath === 'string' ? track.astraPath : String(track.url ?? track.id),
title: track.title ?? 'Unknown title',
artist: track.artist ?? 'Unknown artist',
album: track.album ?? '',
diff --git a/src/car/carPlayback.ts b/src/car/carPlayback.ts
index 8da1ef3..6d4bae8 100644
--- a/src/car/carPlayback.ts
+++ b/src/car/carPlayback.ts
@@ -15,9 +15,11 @@ import { buildArtistList, filterTracksByArtist } from '@/library/artistGrouping'
import { dbTrackToTrack } from '@/library/trackAdapter';
import { playForCar, playTracksForCar, pause, seekTo, skipToNext, skipToPrevious } from '@/audio/playbackController';
import { syncCarNowPlayingFromTrackPlayer } from '@/audio/carSync';
+import TrackPlayer, { type Track as RntpTrack } from 'react-native-track-player';
import { useAudioSettingsStore } from '@/stores/audioSettingsStore';
import { useEQStore } from '@/stores/eqStore';
import { useLibraryStore } from '@/stores/libraryStore';
+import { usePlaylistStore } from '@/stores/playlistStore';
import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore';
import { useSettingsStore } from '@/stores/settingsStore';
import type { DbTrack } from '@/types/library';
@@ -53,6 +55,7 @@ async function initializeForCar(): Promise {
initPromise = (async () => {
await useSettingsStore.getState().load();
await useLibraryStore.getState().initialize();
+ await usePlaylistStore.getState().refresh();
await useRemoteSourcesStore.getState().init();
await Promise.all([
useEQStore.getState().load(),
@@ -92,6 +95,9 @@ export async function handleAstraCarCommand(payload: CarCommandPayload): Promise
case 'seek':
if (typeof payload.position === 'number') await seekTo(payload.position);
break;
+ case 'toggleFavorite':
+ await handleFavoriteCommand();
+ break;
default:
break;
}
@@ -102,6 +108,19 @@ export async function handleAstraCarCommand(payload: CarCommandPayload): Promise
}
}
+async function handleFavoriteCommand(): Promise {
+ const activeTrack = await TrackPlayer.getActiveTrack();
+ const path = rntpTrackPath(activeTrack);
+ if (!path) return;
+ await usePlaylistStore.getState().toggleFavorite({ path });
+}
+
+function rntpTrackPath(track: RntpTrack | null | undefined): string | null {
+ if (!track) return null;
+ if (typeof track.astraPath === 'string' && track.astraPath.length > 0) return track.astraPath;
+ return typeof track.url === 'string' && track.url.length > 0 ? track.url : null;
+}
+
async function playMedia(media: CarMediaPayload): Promise {
const db = await openLibraryDb();
const resolved = await resolveMediaTracks(db, media);