diff --git a/fastlane/metadata/android/en-US/changelogs/130.txt b/fastlane/metadata/android/en-US/changelogs/128.txt
similarity index 100%
rename from fastlane/metadata/android/en-US/changelogs/130.txt
rename to fastlane/metadata/android/en-US/changelogs/128.txt
diff --git a/fastlane/metadata/android/en-US/changelogs/129.txt b/fastlane/metadata/android/en-US/changelogs/129.txt
new file mode 100644
index 00000000..99df21de
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/129.txt
@@ -0,0 +1,12 @@
+### Fixes
+- Fix a bug in 4.7.0 that repeat mode was activated by default.
+
+### Features
+- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
+- Properly handling nested directory structures (Android Auto)
+- Add a toast when adding tracks to the playlist
+- Allow pinning when offline
+
+### Dependencies
+- Update koin
+- Update media3 to v1.1.0
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6137c2c9..9b704ce0 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,6 +4,7 @@ gradle                 = "8.1.1"
 
 navigation             = "2.6.0"
 gradlePlugin           = "8.1.0"
+androidxcar            = "1.2.0"
 androidxcore           = "1.10.1"
 ktlint                 = "0.43.2"
 ktlintGradle           = "11.5.0"
@@ -49,6 +50,7 @@ kotlin                  = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin"
 ktlintGradle            = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
 detekt                  = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
 
+car                     = { module = "androidx.car.app:app", version.ref = "androidxcar" }
 core                    = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
 design                  = { module = "com.google.android.material:material", version.ref = "materialDesign" }
 annotations             = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
@@ -103,4 +105,4 @@ apacheCodecs            = { module = "commons-codec:commons-codec", version.ref
 robolectric             = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
 
 [plugins]
-ksp                     = { id = "com.google.devtools.ksp", version.ref = "ksp" }
\ No newline at end of file
+ksp                     = { id = "com.google.devtools.ksp", version.ref = "ksp" }
diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle
index cea9d70f..5f9420ae 100644
--- a/ultrasonic/build.gradle
+++ b/ultrasonic/build.gradle
@@ -12,8 +12,8 @@ android {
 
     defaultConfig {
         applicationId "org.moire.ultrasonic"
-        versionCode 128
-        versionName "4.7.0"
+        versionCode 129
+        versionName "4.7.1"
 
         minSdkVersion versions.minSdk
         targetSdkVersion versions.targetSdk
@@ -81,6 +81,7 @@ android {
         disable 'ObsoleteLintCustomCheck'
         // We manage dependencies on Gitlab with RenovateBot
         disable 'GradleDependency'
+        disable 'AndroidGradlePluginVersion'
         textReport true
         checkDependencies true
     }
@@ -100,6 +101,7 @@ dependencies {
         exclude group: "com.android.support"
     }
 
+    implementation libs.car
     implementation libs.core
     implementation libs.design
     implementation libs.multidex
diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml
index 711f13e2..0d25405d 100644
--- a/ultrasonic/src/main/AndroidManifest.xml
+++ b/ultrasonic/src/main/AndroidManifest.xml
@@ -12,6 +12,8 @@
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 
+    <uses-sdk tools:overrideLibrary="androidx.car.app" />
+
     <supports-screens
         android:anyDensity="true"
         android:largeScreens="true"
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt
index 0154a05a..ee203b41 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt
@@ -7,7 +7,10 @@
 
 package org.moire.ultrasonic.playback
 
+import android.content.Context
+import android.os.Build
 import android.os.Bundle
+import androidx.car.app.connection.CarConnection
 import androidx.media3.common.HeartRating
 import androidx.media3.common.MediaItem
 import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
@@ -97,6 +100,7 @@ const val PLAY_COMMAND = "play "
 @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
 class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
 
+    private val applicationContext: Context by inject()
     private val activeServerProvider: ActiveServerProvider by inject()
 
     private val serviceJob = SupervisorJob()
@@ -115,6 +119,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
     private val placeholderButton = getPlaceholderButton()
 
     private var heartIsCurrentlyOn = false
+    private var customRepeatModeSet = false
 
     // This button is used for an unstarred track, and its action will star the track
     private val heartButtonToggleOn =
@@ -230,7 +235,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
             commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
         }
 
-        session.player.repeatMode = Player.REPEAT_MODE_ALL
+        configureRepeatMode(session.player)
 
         return MediaSession.ConnectionResult.accept(
             availableSessionCommands.build(),
@@ -238,6 +243,42 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
         )
     }
 
+    private fun configureRepeatMode(player: Player) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            Timber.d("Car app library available, observing CarConnection")
+
+            val originalRepeatMode = player.repeatMode
+
+            var lastCarConnectionType = -1
+
+            CarConnection(applicationContext).type.observeForever {
+                if (lastCarConnectionType == it)
+                    return@observeForever
+
+                lastCarConnectionType = it
+
+                Timber.d("CarConnection type changed to %s", it)
+
+                when (it) {
+                    CarConnection.CONNECTION_TYPE_PROJECTION ->
+                        if (!customRepeatModeSet) {
+                            Timber.d("[CarConnection] Setting repeat mode to ALL")
+                            player.repeatMode = Player.REPEAT_MODE_ALL
+                            customRepeatModeSet = true
+                        }
+
+                    CarConnection.CONNECTION_TYPE_NOT_CONNECTED ->
+                        if (customRepeatModeSet) {
+                            Timber.d("[CarConnection] Resetting repeat mode")
+                            player.repeatMode = originalRepeatMode
+                            customRepeatModeSet = false
+                        }
+                }
+            }
+        } else
+            Timber.d("Car app library not available")
+    }
+
     override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
         if (controller.controllerVersion != 0) {
             // Let Media3 controller (for instance the MediaNotificationProvider)
@@ -369,6 +410,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
 
             PlaybackService.CUSTOM_COMMAND_REPEAT_MODE -> {
                 customCommandFuture = Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
+                customRepeatModeSet = true
 
                 session.player.setNextRepeatMode()
                 session.updateCustomCommands()