CarPlay und Android Auto: Die App fährt mit
Vom Template-Layout bis zum nativen Kotlin-Player: warum die Auto-Integration erst funktionierte, als sie aufhörte, eine Fernbedienung für die App zu sein.
Teil der Serie "Behind the App" — vorgezogen, weil das Update dazu gerade ausgeliefert wird
Ein Satz, der so nicht bleiben sollte
Am 30. Juni 2026 habe ich einen Satz in die App eingebaut, der von Anfang an als Übergangslösung gedacht war:
Bitte Handy entsperren und Sender erneut antippen
Der steht seitdem in AutoBrowseService.kt, in zwei Sprachen, und er erscheint im Android-Auto-Display, wenn jemand bei gesperrtem Telefon einen Sender antippt. Technisch war das ein Fortschritt — vorher zeigte Android Auto nur ein generisches „Die App funktioniert nicht". Als Produktaussage taugt der Satz trotzdem nicht: Er bittet den Fahrer, während der Fahrt zum Handy zu greifen. Genau das, was eine Auto-Integration überflüssig machen soll.
Dieser Hinweis ist inzwischen Geschichte. Der Weg dahin ging über vier Ausbaustufen, einen lange unentdeckten Totalausfall und einen Architekturwechsel.
Die erste Version war eine Fernbedienung
Gebaut habe ich CarPlay und Android Auto ab März 2026. Auf iOS über die CarPlay-Templates, auf Android über einen MediaBrowserService — bei mir AutoBrowseService.kt, einen Monat später schon 665 Zeilen Kotlin. Beide Systeme funktionieren nach demselben Prinzip: Die App liefert einen Baum aus Listen, das Auto rendert ihn nach eigenen Regeln, und ein Tap kommt als Kommando zurück.
Das erste, was man dabei lernt, ist Verzicht. Apples CPGridTemplate ist hart auf acht Buttons begrenzt. Alles darüber muss in eine Listenansicht, deshalb gibt es in Favoriten und Verlauf acht Kacheln plus „Alle anzeigen". Wie diese acht Kacheln aussehen, entscheidet nicht mein Code, sondern das Fahrzeug: Das Testauto eines Nutzers, ein Mercedes mit breitem Display, legt sie in eine einzige Reihe. Andere Head-Units machen 4×2, günstige Nachrüstgeräte 2×2. Man baut ein Layout, das man selbst nie zu Gesicht bekommt.
Das zweite Zugeständnis betrifft die Logos. Die Senderdaten kommen aus unserer eigenen Sender-API, einem erweiterten Fork des offenen radio-browser-Projekts, den wir selbst hosten. Bei den Logos hilft das wenig: Die Bilder stammen von den Sendern selbst, in allen denkbaren Seitenverhältnissen, Auflösungen und Formaten, mit hellen und dunklen Hintergründen. Nebeneinander in einem Grid sieht das chaotisch aus. TuneIn löst das mit einer kuratierten Logo-Datenbank, was bei über 40.000 Sendern für uns keine Option ist. Unsere Lösung läuft über das eigene Backend: Jedes Logo wird zu einer 384×384-PNG-Kachel auf dunklem Grund verarbeitet, mit Buchstaben-Fallback für Sender ohne Bild. Die Kacheln haben dadurch einen dunklen Rahmen und sehen vermutlich weniger edel aus als bei TuneIn. Sie sehen dafür alle gleich aus, und das ist im fahrenden Auto mehr wert.
Diese erste Version funktionierte. Solange die App auf dem Handy geöffnet und der Bildschirm an war.
Kein Ton, und trotzdem grüne Builds
In den Rezensionen tauchte immer wieder derselbe Satz auf: Man müsse erst übers Handy einen Sender starten, bis das Autoradio die App überhaupt bedient. Die naheliegende Erklärung war Startlatenz, und dort habe ich zuerst angesetzt: Cold-Start, Hintergrund-Drosselung, Headless-JS.
Am 25. Juni 2026 habe ich dann in einem Tester-Log gesehen, was wirklich passierte: Jeder Sender-Tap im Auto erzeugte acht Fehlversuche und dann die Zeile „giving up after 8 retries". Kein Ton, nie.
Die Ursache lag zwei Monate zurück und hatte nichts mit dem Auto zu tun. Die Wiedergabe im Auto läuft über ein eigenes Session-Kommando namens PLAY_STATION, das der Track-Player nicht von Haus aus kennt — es kommt über einen Patch in die Bibliothek, verwaltet von patch-package. Dieser Patch war seit dem 19. April kaputt: abgeschnitten, syntaktisch nicht mehr lesbar. patch-package meldet so etwas standardmäßig nur als Warnung und beendet sich mit Exit-Code 0. Im postinstall-Skript stand zusätzlich ein || true. Jeder lokale Build und jeder EAS-Build lief also grün durch, ohne den Patch.
Damit war PLAY_STATION kein registriertes Kommando mehr. Das Auto schickte es trotzdem, bekam sofort einen Fehler zurück, versuchte es acht Mal und gab auf. Das Browsen funktionierte weiter, weil das im nativen Service liegt und keinen Patch braucht — die App sah im Auto völlig intakt aus, nur eben stumm.
Seitdem lautet postinstall bei mir patch-package --error-on-fail. Ein nicht anwendbarer Patch bricht die Installation jetzt hart ab, lokal wie im CI. Daraus wurde eine Diagnose-Regel, die inzwischen in der Projektdokumentation steht: Wenn natives Verhalten unerklärlich kaputt ist, prüfe zuerst, ob die Patches überhaupt angewandt wurden. Die Suche hatte bis dahin eine Ebene über dem eigentlichen Fehler stattgefunden.
Das eigentliche Problem war die Architektur
Mit reparierten Patches ging die Wiedergabe wieder — bei entsperrtem Gerät. Bei gesperrtem Bildschirm blieb es dabei, dass nichts passierte. Das lag nicht an einem Fehler im Code, sondern an der Architektur.
Die gesamte Abspiellogik lag in React Native, im PlayerContext. Der native Auto-Dienst war nur der Bote: Tap entgegennehmen, Kommando an die JS-Seite schicken, dort Sender laden und abspielen. Das setzt voraus, dass die JavaScript-Seite läuft. Auf einem kalten, gesperrten Gerät läuft sie nicht. Der Headless-Boot startet auf echten Geräten nicht zuverlässig, und eine Activity aus dem Hintergrund zu starten, verhindert Android bei aktivem Sperrbildschirm gezielt.
Einen Umweg habe ich probiert und wieder verworfen: den Dienst per startForegroundService hochziehen. Der muss dann binnen etwa fünf Sekunden eine Vordergrund-Benachrichtigung zeigen, was er ohne laufende Wiedergabe nicht kann — Android beendet ihn dann als hängend. Im Log sah man den Dienst im Sekundentakt entstehen und sterben, was die ganze Auto-Sitzung destabilisierte.
An diesem Punkt entstand der erwähnte Entsperren-Hinweis: die beste Antwort, die diese Architektur noch hergab. Die eigentliche Konsequenz war, die Logik umzubauen.
Der Umbau: der Player zieht ins Auto
Im Juli habe ich die Wiedergabe für Android Auto komplett nativ neu gebaut. AutoNativePlayer.kt enthält seitdem einen eigenen ExoPlayer, der direkt im Auto-Dienst lebt: Stream öffnen, abspielen, ICY-Metadaten für den Songtitel selbst auslesen, bei Bedarf auf den Backend-Proxy ausweichen, vor- und zurückblättern in der Senderliste. Dazu ein Schiedsrichter für den Audio-Fokus, damit sich nativer Player und App-Player nie gleichzeitig auf denselben Ausgang legen. Die zuletzt gespielte Liste und der letzte Sender liegen in einer nativen Einstellungsdatei, damit beim nächsten Einsteigen etwas da ist, auch wenn die App vorher gar nicht lief.
Das Ergebnis: Ein Tap im Auto braucht React Native nicht mehr. Kaltes Gerät, gesperrter Bildschirm, App seit Tagen nicht geöffnet — der Ton kommt trotzdem. In den Einstellungen gibt es dafür den Schalter „Nativer Auto-Modus", standardmäßig an.
Rückblickend ist das die Lektion, die den ganzen Aufwand rechtfertigt: Eine Auto-Integration ist keine zweite Oberfläche für die App. Sie ist eine zweite Anwendung, die unter fremder Prozesskontrolle läuft und alles selbst können muss, was sie verspricht.
Was Android Auto einem nicht verzeiht
Beim nativen Umbau kamen Eigenheiten ans Licht, die in keiner Dokumentation stehen.
Android Auto merkt sich Antworten dauerhaft, auch falsche. Wenn der Google-Assistent den Sender-Baum abfragt — was er etwa eine Sekunde nach dem Start des Dienstes tut, also regelmäßig bevor das Mobilfunknetz nach dem Losfahren bereit ist —, dann bekommt er eine Fehlermeldung, und die klebt. Die Liste heilt sich nicht, wenn das Netz zwei Minuten später da ist. Der Dienst holt Favoriten, Verlauf und Genres deshalb mit wachsendem Abstand nach (3, 6, 12, 24 Sekunden, maximal fünf Versuche) und muss dem System danach ausdrücklich mitteilen, dass die alte Antwort ungültig ist. Erst dann verschwindet der Fehler-Platzhalter.
Beim Wiedereinsteigen ins Auto hat es drei Anläufe gebraucht, weil die Praxis von der Dokumentation abweicht. Naheliegend wäre, dass Android Auto den zuletzt gespielten Sender erneut anfordert. Die Tester-Logs zeigten: Es fragt weder das eine noch das andere dafür vorgesehene Kommando ab. Die „Weiter"-Kachel drückt schlicht Play. Ein frisch gestarteter Dienst hat aber nichts, was er abspielen könnte — deshalb passierte nichts. Die Lösung war unspektakulär: Ein Play auf leerem Dienst stellt jetzt die letzte Wiedergabe aus der Persistenz wieder her.
Der Fehler, der Pro-Nutzer im Auto ausbremste
Der folgenreichste Fund betraf die Pro-Prüfung. Ein Nutzer meldete, dass die App ihm im Auto einen Pro-Hinweis zeigte — obwohl er Pro hatte.
Drei Fehler wirkten zusammen. Der schwerste lag in JavaScript: Die Funktion, die die Nutzerrechte vom Server holt, fing jeden API-Fehler ab und lieferte eine leere Liste zurück. Diese leere Liste überschrieb den lokalen Rechte-Cache. Aufgerufen wird sie bei jedem Wechsel der App in den Vordergrund — also genau in dem Moment, in dem man ins Auto steigt und das Handy einmal kurz anfasst. Ein Netzhänger beim Losfahren reichte, um einen zahlenden Nutzer für die Fahrt zu einem Gratisnutzer zu machen. Dazu kam ein nativer Lesefehler, der bei blockiertem Speicher hart auf „kein Recht" fiel, und die schon beschriebene Cache-Eigenheit von Android Auto, die den einmal gezeigten Pro-Hinweis festhielt.
Die Regel, die daraus wurde, gilt jetzt an beiden Stellen: Nur eine erfolgreiche Antwort darf den Rechte-Cache verändern. Ein Fehler bedeutet „unbekannt", nicht „nein".
Testen ohne Auto
Google liefert für Android Auto einen Simulator, die Desktop Head Unit. Der letzte Build stammt aus dem Jahr 2022, ein Update gab es seitdem nicht — und mittlerweile scheitert die Verbindung zur heutigen Android-Auto-App an einem Zertifikatsproblem. Der Transport kommt zustande, die Projektion startet nie, eine brauchbare Fehlermeldung gibt es nicht. Damit war lokales Testen der echten Auto-Oberfläche unmöglich, und ein Fahrzeug zum Entwickeln habe ich nicht.
Übrig blieben zwei Werkzeuge. Ein selbst gebauter Media Controller Test, mit dem sich alles prüfen lässt, was zwischen Auto-Oberfläche und Dienst passiert: Browsen, Sender starten, Play/Pause, Zustand der Sitzung. Und Logs von Testern aus echten Fahrzeugen. Deshalb ist ein Teil der Arbeit dieser Runde reine Instrumentierung: Jedes Transport-Kommando schreibt jetzt eine Zeile ins Diagnoseprotokoll, das Nutzer als Datei teilen können. Ohne diese Zeilen hätte ich die drei Fehlversuche beim Wiedereinsteigen nie aufgeklärt.
Kleine Randnotiz für alle, die das nachbauen: Das Motorola-Testgerät unterdrückt Debug-Logs, bis man sie pro Tag-Name freischaltet. Ein stummes Log sieht dabei exakt aus wie ein totes Feature.
CarPlay bekam dieselbe Kur
Nachdem Android Auto lief, habe ich die Erkenntnisse auf iOS gegengeprüft. Die Mechanik ist dort anders — CarPlay startet die App für seine Szene selbst, JavaScript läuft also. Die Fehlermuster waren trotzdem dieselben drei: Die Pro-Prüfung lief über denselben verwundbaren Cache und wurde nach dem Eintreffen der Rechte nie neu bewertet. Favoriten und Verlauf zeigten bei einem Netzfehler „leer" statt es erneut zu versuchen. Und ein zu früher Tap lief ins Nichts, weil der Player noch keine Warteschlange hatte — kein Fehler, kein Ton, nichts.
Genau das war das Tester-Zitat, mit dem diese Runde begonnen hatte: „Die App musste zwischendurch geöffnet werden, um eine CarPlay-Aktion auszulösen." Frühe Taps werden jetzt gepuffert und nachgeholt, sobald der Player bereit ist.
Warum das Pro ist
Die Frage kommt regelmäßig, und sie ist berechtigt: Warum kostet ausgerechnet die Auto-Nutzung Geld?
Die ehrliche Antwort steht in diesem Text. Zwischen der ersten CarPlay-Zeile im März und der Version, die jetzt ausgeliefert wird, liegen vier Ausbaustufen, ein kompletter nativer Player in Kotlin und eine Reihe von Korrekturen, die sich ohne eigenes Testfahrzeug nur über Logs aus echten Autos aufklären ließen. Das ist der aufwendigste Einzelbereich der App, und er bleibt aufwendig: Jedes Android-Update, jedes Update der Wiedergabe-Bibliothek und jede neue Head-Unit kann die Integration wieder brechen. Ich muss das dauerhaft pflegen, ohne es selbst im Alltag testen zu können.
Dazu kommt der Rahmen: In dieser App gibt es keine Werbung und kein Werbe-Tracking. Die Basis-App ist kostenlos, die Entwicklung finanziert allein die Pro-Version. Wer das Auto-Feature nutzt, bezahlt damit ziemlich genau den Teil, der ihn am meisten Arbeit gekostet hat.
Wo es jetzt steht
Die native Android-Auto-Wiedergabe und die CarPlay-Korrekturen kamen beide mit Version 2.17, im Abstand von einem Tag. Beides ist inzwischen in echten Fahrzeugen bestätigt — Android Auto wie CarPlay, mit kaltem und gesperrtem Gerät.
Auf dem iPhone ist das seit Ende Juli draußen. Android-Nutzer bekommen es erst jetzt, mit Version 2.19, und das hat einen unspektakulären Grund: Die Prüfzeiten in der Google Play Console sind derzeit so lang, dass ich 2.17 und 2.18 im Rollout zusammengefasst und übersprungen habe. Zwei fertige Releases lagen also wochenlang bereit, während genau die Nutzer, die sich über das Auto-Verhalten beschwert hatten, weiter mit der alten Version fuhren. Das ist der Teil dieser Geschichte, an dem der beste Fix nichts nützt.
Wenn 2.19 durch ist, gilt für Android dasselbe wie für iOS: Handy in die Tasche, Auto an, Sender antippen. Der Satz mit dem Entsperren wird dann nicht mehr gebraucht.
Im nächsten Post geht es um das älteste Protokoll im ganzen Stack: ICY-Metadaten. Songtitel, die mitten im Audio-Strom stecken, ein Standard aus den späten Neunzigern, und die Frage, warum „einfach den Titel anzeigen" bei Radio-Streams ein Minenfeld ist.
Radio Adfree ist eine werbefreie Radio-App für iOS und Android mit über 40.000 Sendern, Song-Erkennung und Podcasts.