From 5ea62b4bd64cb961e9a41ba52c0bc4a69cc8356d Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Fri, 6 Nov 2026 15:37:59 -0800 Subject: [PATCH] Any installed app can be added to the car as a music app or a messenger Music plays through Gearslip's player from the app's playback session, pressing its Play key when it isn't running; messages show from its notifications. Added apps can be removed from the Apps card. --- .../gearslip/host/CustomCarApps.kt | 48 +++++++ .../seb3thehacker/gearslip/media/CarMedia.kt | 40 +++++- .../gearslip/media/MediaCatalog.kt | 28 +++- .../gearslip/notify/MessagingCatalog.kt | 14 +- .../gearslip/ui/HomeCarSettings.kt | 123 +++++++++++++++++- gearslip/src/main/res/values-ar/strings.xml | 10 ++ gearslip/src/main/res/values-cs/strings.xml | 10 ++ gearslip/src/main/res/values-de/strings.xml | 10 ++ gearslip/src/main/res/values-es/strings.xml | 10 ++ gearslip/src/main/res/values-fr/strings.xml | 10 ++ gearslip/src/main/res/values-it/strings.xml | 10 ++ gearslip/src/main/res/values-ja/strings.xml | 10 ++ gearslip/src/main/res/values-ko/strings.xml | 10 ++ gearslip/src/main/res/values-nl/strings.xml | 10 ++ gearslip/src/main/res/values-pl/strings.xml | 10 ++ .../src/main/res/values-pt-rBR/strings.xml | 10 ++ gearslip/src/main/res/values-ro/strings.xml | 10 ++ gearslip/src/main/res/values-ru/strings.xml | 10 ++ gearslip/src/main/res/values-sv/strings.xml | 10 ++ gearslip/src/main/res/values-tr/strings.xml | 10 ++ gearslip/src/main/res/values-uk/strings.xml | 10 ++ .../src/main/res/values-zh-rCN/strings.xml | 10 ++ gearslip/src/main/res/values/strings.xml | 10 ++ 23 files changed, 429 insertions(+), 4 deletions(-) create mode 100644 gearslip/src/main/java/app/seb3thehacker/gearslip/host/CustomCarApps.kt diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/host/CustomCarApps.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/host/CustomCarApps.kt new file mode 100644 index 0000000..abe586b --- /dev/null +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/host/CustomCarApps.kt @@ -0,0 +1,48 @@ +package app.seb3thehacker.gearslip.host + +import android.content.Context +import app.seb3thehacker.gearslip.AppSettings +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Apps the driver added to the car by hand, for apps that never declared Android Auto support + * (issue #84). A music app is driven through its playback session, the way Spotify's player is; + * a messenger's notifications show on Gearslip's Messages screen, as any car messenger's do. + * Neither needs anything from the app's developer. + */ +object CustomCarApps { + + enum class Kind(val id: String) { MEDIA("media"), MESSAGING("messaging") } + + data class App(val packageName: String, val kind: Kind) + + private const val KEY = "custom_car_apps" + + private val flow = MutableStateFlow?>(null) + + /** Null until [all] has read the settings once. */ + val state: StateFlow?> = flow + + fun all(context: Context): List = flow.value ?: read(context).also { flow.value = it } + + fun of(context: Context, kind: Kind): List = all(context).filter { it.kind == kind } + + fun isCustom(context: Context, packageName: String): Boolean = all(context).any { it.packageName == packageName } + + fun add(context: Context, packageName: String, kind: Kind) = save(context, all(context).filterNot { it.packageName == packageName } + App(packageName, kind)) + + fun remove(context: Context, packageName: String) = save(context, all(context).filterNot { it.packageName == packageName }) + + private fun save(context: Context, apps: List) { + flow.value = apps + AppSettings.putString(context, KEY, apps.joinToString(",") { "${it.packageName}:${it.kind.id}" }) + } + + private fun read(context: Context): List = + AppSettings.getString(context, KEY, "").split(",").mapNotNull { item -> + val pkg = item.substringBefore(':').takeIf { it.isNotBlank() } ?: return@mapNotNull null + val kind = Kind.entries.firstOrNull { it.id == item.substringAfter(':') } ?: return@mapNotNull null + App(pkg, kind) + } +} diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/media/CarMedia.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/media/CarMedia.kt index 68fdf48..9adf53a 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/media/CarMedia.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/media/CarMedia.kt @@ -4,6 +4,7 @@ import app.seb3thehacker.gearslip.R import android.content.ComponentName import android.content.Context import android.content.Intent +import android.view.KeyEvent import android.graphics.Bitmap import android.media.MediaMetadata import android.media.Rating @@ -281,6 +282,14 @@ class CarMedia(private val context: Context) { attachToSession(app) return } + // Added by hand: no library at all. Wake the app with its Play key if it isn't running, + // then wait for its session as after opening it. + if (app.sessionOnly) { + val running = hasSession(app) + if (!running) pressPlay(app) + if (running) attachToSession(app) else main.postDelayed({ attachToSession(app, launched = true) }, PLAY_WAKE_MS) + return + } val mb = MediaBrowser(context, app.component, object : MediaBrowser.ConnectionCallback() { override fun onConnected() = this@CarMedia.onConnected(app) override fun onConnectionFailed() { @@ -344,7 +353,7 @@ class CarMedia(private val context: Context) { GearslipLog.w("media: ${app.label} has no active session to control") // A session-only app (Spotify) just isn't running - say so and offer to open it, // rather than calling it unsupported. - _rejection.value = if (KnownApps.playerWorks(app.component.packageName)) { + _rejection.value = if (app.sessionOnly || KnownApps.playerWorks(app.component.packageName)) { Rejection.NEEDS_APP_RUNNING } else { Rejection.REFUSED @@ -834,6 +843,33 @@ class CarMedia(private val context: Context) { main.postDelayed({ attachToSession(app, launched = true) }, LAUNCH_WAIT_MS) } + private fun hasSession(app: MediaApp): Boolean = runCatching { + context.getSystemService(MediaSessionManager::class.java) + .getActiveSessions(ComponentName(context, GearslipNotificationListener::class.java)) + .any { it.packageName == app.component.packageName } + }.getOrDefault(false) + + /** + * Sends [app] a Play key press, as a headset button would. Most players start their service + * for it, even when closed, and pick up where they left off; one that doesn't is offered to + * open on the phone once the wait runs out. + */ + private fun pressPlay(app: MediaApp) { + val pkg = app.component.packageName + val receiver = context.packageManager.queryBroadcastReceivers(Intent(Intent.ACTION_MEDIA_BUTTON).setPackage(pkg), 0) + .firstOrNull()?.activityInfo ?: run { + GearslipLog.i("media: ${app.label} takes no media keys, so it has to be opened on the phone") + return + } + GearslipLog.i("media: pressing Play for ${app.label}, which isn't running") + for (action in listOf(KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP)) { + val press = Intent(Intent.ACTION_MEDIA_BUTTON) + .setClassName(pkg, receiver.name) + .putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(action, KeyEvent.KEYCODE_MEDIA_PLAY)) + runCatching { context.sendBroadcast(press) }.onFailure { GearslipLog.w("media: Play for ${app.label} failed: ${it.message}") } + } + } + /** Brings [app] to the foreground on the phone so it publishes its session. */ private fun openApp(app: MediaApp): Boolean { val intent = context.packageManager.getLaunchIntentForPackage(app.component.packageName) ?: return false @@ -853,6 +889,8 @@ class CarMedia(private val context: Context) { const val SESSION_RETRY_MS = 500L /** After opening a cold app, how long to wait before the first look for its session. */ const val LAUNCH_WAIT_MS = 1_500L + /** How long a player woken by its Play key gets before its session is looked for. */ + const val PLAY_WAKE_MS = 1_000L /** Retries once the app has been opened: a cold start can take a few seconds to publish. */ const val SESSION_RETRIES_LAUNCHED = 16 /** How long an app gets to start playing a search before Gearslip says it didn't. */ diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/media/MediaCatalog.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/media/MediaCatalog.kt index 9599583..3087e5f 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/media/MediaCatalog.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/media/MediaCatalog.kt @@ -1,5 +1,7 @@ package app.seb3thehacker.gearslip.media +import app.seb3thehacker.gearslip.host.CustomCarApps + import app.seb3thehacker.gearslip.host.AndroidAuto import app.seb3thehacker.gearslip.host.CarDescriptor import app.seb3thehacker.gearslip.host.KnownApps @@ -15,6 +17,11 @@ class MediaApp( val label: String, /** Whose audio to capture: playback capture is scoped to one uid so nothing else leaks in. */ val uid: Int, + /** + * Added by the driver rather than found (issue #84): there's no library to connect to, so + * the player goes straight to the app's playback session, as it does for Spotify. + */ + val sessionOnly: Boolean = false, ) /** @@ -43,10 +50,29 @@ object MediaCatalog { ) } .distinctBy { it.component.packageName } - .sortedBy { it.label.lowercase() } .toList() + .let { found -> found + added(context, found.map { it.component.packageName }.toSet()) } + .sortedBy { it.label.lowercase() } } + /** The music apps the driver added by hand, unless the app has since become a car app itself. */ + private fun added(context: Context, found: Set): List { + val manager = context.packageManager + return CustomCarApps.of(context, CustomCarApps.Kind.MEDIA).filter { it.packageName !in found }.mapNotNull { custom -> + val info = runCatching { manager.getApplicationInfo(custom.packageName, 0) }.getOrNull() ?: return@mapNotNull null + MediaApp( + // No service to bind: the class name only keeps the component unique and readable. + component = ComponentName(custom.packageName, SESSION_ONLY_CLASS), + label = info.loadLabel(manager).toString(), + uid = info.uid, + sessionOnly = true, + ) + } + } + + /** What an added app's component names, since it has no browser service. */ + const val SESSION_ONLY_CLASS = "gearslip.session-only" + /** A descriptor that can't be read keeps the app, rather than dropping a real player over an odd manifest. */ private fun declaresMedia(manager: PackageManager, service: android.content.pm.ServiceInfo): Boolean { if (CarDescriptor.meta(manager, service.packageName, service) == null) return false diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/notify/MessagingCatalog.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/notify/MessagingCatalog.kt index 85c5d01..330bbcc 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/notify/MessagingCatalog.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/notify/MessagingCatalog.kt @@ -1,5 +1,7 @@ package app.seb3thehacker.gearslip.notify +import app.seb3thehacker.gearslip.host.CustomCarApps + import android.content.Context import android.content.Intent import app.seb3thehacker.gearslip.host.AndroidAuto @@ -33,7 +35,17 @@ object MessagingCatalog { }.getOrDefault(pkg) MessagingApp(pkg, label) } - .sortedBy { it.label.lowercase() } .toList() + .let { found -> found + added(context, found.map { it.packageName }.toSet()) } + .sortedBy { it.label.lowercase() } + } + + /** The messengers the driver added by hand (issue #84); their notifications are all it takes. */ + private fun added(context: Context, found: Set): List { + val manager = context.packageManager + return CustomCarApps.of(context, CustomCarApps.Kind.MESSAGING).filter { it.packageName !in found }.mapNotNull { custom -> + val info = runCatching { manager.getApplicationInfo(custom.packageName, 0) }.getOrNull() ?: return@mapNotNull null + MessagingApp(custom.packageName, info.loadLabel(manager).toString()) + } } } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt index 6e8a5b0..bbd479e 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt @@ -21,6 +21,12 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Checkbox +import androidx.core.graphics.drawable.toBitmap +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.ui.graphics.asImageBitmap +import kotlinx.coroutines.launch +import androidx.compose.foundation.layout.heightIn import androidx.compose.ui.text.input.KeyboardType import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.TextButton @@ -399,16 +405,131 @@ private fun AppRows() { LaunchedEffect(Unit) { withContext(Dispatchers.IO) { LauncherCache.load(context, force = true) } } val rows = apps.orEmpty().mapNotNull { entry -> entry.componentId?.let { it to entry } } if (rows.isEmpty()) SettingsRow(stringResource(if (apps == null) R.string.car_apps_finding else R.string.car_apps_none)) + val custom by app.seb3thehacker.gearslip.host.CustomCarApps.state.collectAsState() + val scope = androidx.compose.runtime.rememberCoroutineScope() + var adding by remember { mutableStateOf(false) } + fun rescan() = scope.launch { withContext(Dispatchers.IO) { LauncherCache.load(context, force = true) } } rows.forEachIndexed { i, (id, entry) -> val shown = id !in hidden + val pkg = entry.media?.component?.packageName ?: entry.messaging?.packageName + val added = pkg != null && custom.orEmpty().any { it.packageName == pkg } if (i > 0) SettingsDivider() SettingsRow( entry.label, Modifier.toggleable(shown, role = Role.Checkbox) { CarSettings.setAppHidden(id, !it) }, + subtitle = if (added) stringResource(R.string.car_apps_added_by_you) else null, leading = { entry.icon?.let { Image(it, contentDescription = null, Modifier.size(40.dp)) } }, - trailing = { Checkbox(checked = shown, onCheckedChange = null) }, + trailing = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (added && pkg != null) { + androidx.compose.material3.IconButton({ + app.seb3thehacker.gearslip.host.CustomCarApps.remove(context, pkg) + rescan() + }) { Icon(androidx.compose.material.icons.Icons.Filled.Delete, stringResource(R.string.car_apps_remove, entry.label)) } + } + Checkbox(checked = shown, onCheckedChange = null) + } + }, ) } + if (apps != null) { + SettingsDivider() + SettingsRow( + stringResource(R.string.car_apps_add), + Modifier.clickable { adding = true }, + subtitle = stringResource(R.string.car_apps_add_note), + icon = androidx.compose.material.icons.Icons.Filled.Add, + ) + } + if (adding) { + AddCarAppDialog( + listed = rows.mapNotNull { (_, e) -> e.media?.component?.packageName ?: e.messaging?.packageName ?: e.template?.component?.packageName }.toSet(), + onDismiss = { adding = false }, + ) { pkg, kind -> + app.seb3thehacker.gearslip.host.CustomCarApps.add(context, pkg, kind) + adding = false + rescan() + } + } +} + +/** + * Picks an installed app the car doesn't list, then whether it's music or messages (issue #84). + * Music plays through Gearslip's player from the app's playback session; messages show from its + * notifications. Neither needs the app to support Android Auto. + */ +@Composable +private fun AddCarAppDialog( + listed: Set, + onDismiss: () -> Unit, + onAdd: (String, app.seb3thehacker.gearslip.host.CustomCarApps.Kind) -> Unit, +) { + val context = androidx.compose.ui.platform.LocalContext.current + var picked by remember { mutableStateOf?>(null) } + val candidates by androidx.compose.runtime.produceState>?>(null) { + value = withContext(Dispatchers.IO) { + val manager = context.packageManager + manager.queryIntentActivities(android.content.Intent(android.content.Intent.ACTION_MAIN).addCategory(android.content.Intent.CATEGORY_LAUNCHER), 0) + .map { it.activityInfo.packageName to it.loadLabel(manager).toString() } + .distinctBy { it.first } + .filter { (pkg, _) -> pkg != context.packageName && pkg !in listed } + .sortedBy { it.second.lowercase() } + .map { (pkg, label) -> + val icon = runCatching { manager.getApplicationIcon(pkg).toBitmap(96, 96).asImageBitmap() }.getOrNull() + Triple(pkg, label, icon) + } + } + } + androidx.compose.material3.AlertDialog( + onDismissRequest = onDismiss, + title = { Text(picked?.let { stringResource(R.string.car_apps_add_as, it.second) } ?: stringResource(R.string.car_apps_add_pick)) }, + text = { + val choice = picked + if (choice == null) { + val list = candidates + if (list == null) { + Text(stringResource(R.string.car_apps_finding)) + } else { + androidx.compose.foundation.lazy.LazyColumn(Modifier.heightIn(max = 420.dp)) { + items(list.size) { i -> + val (pkg, label, icon) = list[i] + Row( + Modifier.fillMaxWidth().clickable { picked = pkg to label }.padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + icon?.let { Image(it, contentDescription = null, Modifier.size(36.dp)) } + Text(label, style = MaterialTheme.typography.bodyLarge) + } + } + } + } + } else { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + KindRow(stringResource(R.string.car_apps_as_media), stringResource(R.string.car_apps_as_media_note)) { + onAdd(choice.first, app.seb3thehacker.gearslip.host.CustomCarApps.Kind.MEDIA) + } + KindRow(stringResource(R.string.car_apps_as_messaging), stringResource(R.string.car_apps_as_messaging_note)) { + onAdd(choice.first, app.seb3thehacker.gearslip.host.CustomCarApps.Kind.MESSAGING) + } + } + } + }, + confirmButton = {}, + dismissButton = { + androidx.compose.material3.TextButton({ if (picked != null) picked = null else onDismiss() }) { + Text(stringResource(if (picked != null) R.string.back else R.string.cancel)) + } + }, + ) +} + +@Composable +private fun KindRow(title: String, note: String, onClick: () -> Unit) { + Column(Modifier.fillMaxWidth().clickable(onClick = onClick).padding(vertical = 12.dp)) { + Text(title, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.primary) + Text(note, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } } private fun List>.label(value: T): String = firstOrNull { it.first == value }?.second.orEmpty() diff --git a/gearslip/src/main/res/values-ar/strings.xml b/gearslip/src/main/res/values-ar/strings.xml index 51a3b4d..b2edf18 100644 --- a/gearslip/src/main/res/values-ar/strings.xml +++ b/gearslip/src/main/res/values-ar/strings.xml @@ -982,4 +982,14 @@ الإعلانات غير محظورة زر الميكروفون زر المساعد في شريط التنقل. يعمل زر الصوت في السيارة في كل الأحوال. + إضافة تطبيق + لتطبيق لا تعرضه السيارة: تُشغَّل موسيقاه عبر مشغّل Gearslip، أو تظهر رسائله في شاشة الرسائل. + اختر تطبيقًا + عرض %1$s بوصفه + الموسيقى والصوت + ما يُشغَّل الآن، مع التشغيل والإيقاف المؤقت والتخطي. لا توجد مكتبة للتصفح. + الرسائل + إشعاراته، تُقرأ بصوت عالٍ، مع الردود. + أضفته أنت + إزالة %1$s diff --git a/gearslip/src/main/res/values-cs/strings.xml b/gearslip/src/main/res/values-cs/strings.xml index 30417d0..1f0e8a7 100644 --- a/gearslip/src/main/res/values-cs/strings.xml +++ b/gearslip/src/main/res/values-cs/strings.xml @@ -962,4 +962,14 @@ Reklamy neblokovány Tlačítko mikrofonu Tlačítko asistenta na navigační liště. Hlasové tlačítko auta funguje tak jako tak. + Přidat aplikaci + Pro aplikaci, kterou auto nezobrazuje: její hudba hraje přes přehrávač Gearslip, nebo se její zprávy zobrazí ve Zprávách. + Vyberte aplikaci + Zobrazit %1$s jako + Hudba a zvuk + Právě hraje, s přehráním, pozastavením a přeskočením. Bez knihovny k procházení. + Zprávy + Její oznámení, předčítaná nahlas, s odpověďmi. + Přidáno vámi + Odebrat %1$s diff --git a/gearslip/src/main/res/values-de/strings.xml b/gearslip/src/main/res/values-de/strings.xml index 506845a..06df565 100644 --- a/gearslip/src/main/res/values-de/strings.xml +++ b/gearslip/src/main/res/values-de/strings.xml @@ -824,4 +824,14 @@ Werbung nicht blockiert Mikrofontaste Die Assistent-Taste in der Navigationsleiste. Die Sprachtaste des Autos funktioniert so oder so. + App hinzufügen + Für eine App, die das Auto nicht anzeigt: Ihre Musik läuft über den Player von Gearslip, oder ihre Nachrichten erscheinen in Nachrichten. + App auswählen + %1$s anzeigen als + Musik und Audio + Aktueller Titel mit Wiedergabe, Pause und Überspringen. Keine Mediathek zum Durchsuchen. + Nachrichten + Ihre Benachrichtigungen, vorgelesen, mit Antworten. + Von dir hinzugefügt + %1$s entfernen diff --git a/gearslip/src/main/res/values-es/strings.xml b/gearslip/src/main/res/values-es/strings.xml index aab9688..5c468c9 100644 --- a/gearslip/src/main/res/values-es/strings.xml +++ b/gearslip/src/main/res/values-es/strings.xml @@ -826,4 +826,14 @@ Anuncios sin bloquear Botón del micrófono El botón del asistente en la barra de navegación. El botón de voz del coche funciona igualmente. + Añadir una app + Para una app que el coche no muestra: su música suena con el reproductor de Gearslip, o sus mensajes aparecen en Mensajes. + Elige una app + Mostrar %1$s como + Música y audio + Lo que suena, con reproducir, pausar y saltar. No hay biblioteca para explorar. + Mensajes + Sus notificaciones, leídas en voz alta, con respuestas. + Añadida por ti + Quitar %1$s diff --git a/gearslip/src/main/res/values-fr/strings.xml b/gearslip/src/main/res/values-fr/strings.xml index 45b5ec7..df7e48b 100644 --- a/gearslip/src/main/res/values-fr/strings.xml +++ b/gearslip/src/main/res/values-fr/strings.xml @@ -952,4 +952,14 @@ Pubs non bloquées Bouton micro Le bouton de l’assistant dans la barre de navigation. Le bouton vocal de la voiture fonctionne dans tous les cas. + Ajouter une appli + Pour une appli que la voiture n’affiche pas : sa musique passe par le lecteur de Gearslip, ou ses messages s’affichent dans Messages. + Choisir une appli + Afficher %1$s comme + Musique et audio + Lecture en cours, avec lecture, pause et piste suivante. Pas de bibliothèque à parcourir. + Messages + Ses notifications, lues à voix haute, avec réponses. + Ajoutée par vous + Retirer %1$s diff --git a/gearslip/src/main/res/values-it/strings.xml b/gearslip/src/main/res/values-it/strings.xml index 1772eed..2f85ccc 100644 --- a/gearslip/src/main/res/values-it/strings.xml +++ b/gearslip/src/main/res/values-it/strings.xml @@ -952,4 +952,14 @@ Pubblicità non bloccata Pulsante microfono Il pulsante dell’assistente nella barra di navigazione. Il tasto vocale dell’auto funziona comunque. + Aggiungi un’app + Per un’app che l’auto non mostra: la sua musica passa dal lettore di Gearslip, oppure i suoi messaggi compaiono in Messaggi. + Scegli un’app + Mostra %1$s come + Musica e audio + In riproduzione, con play, pausa e salto. Nessuna libreria da sfogliare. + Messaggi + Le sue notifiche, lette ad alta voce, con risposte. + Aggiunta da te + Rimuovi %1$s diff --git a/gearslip/src/main/res/values-ja/strings.xml b/gearslip/src/main/res/values-ja/strings.xml index cd1fa35..0e305e7 100644 --- a/gearslip/src/main/res/values-ja/strings.xml +++ b/gearslip/src/main/res/values-ja/strings.xml @@ -932,4 +932,14 @@ 広告をブロックしない マイクボタン ナビゲーションバーのアシスタントボタンです。車の音声ボタンはどちらでも使えます。 + アプリを追加 + 車に表示されないアプリ向けです。音楽は Gearslip のプレーヤーで再生され、メッセージはメッセージ画面に表示されます。 + アプリを選択 + %1$s の表示方法 + 音楽とオーディオ + 再生中の曲と、再生・一時停止・スキップ。ライブラリの閲覧はできません。 + メッセージ + 通知を読み上げ、返信できます。 + 自分で追加 + %1$s を削除 diff --git a/gearslip/src/main/res/values-ko/strings.xml b/gearslip/src/main/res/values-ko/strings.xml index 535a5ae..8ba877c 100644 --- a/gearslip/src/main/res/values-ko/strings.xml +++ b/gearslip/src/main/res/values-ko/strings.xml @@ -932,4 +932,14 @@ 광고 차단 안 함 마이크 버튼 내비게이션 바의 어시스턴트 버튼입니다. 차량의 음성 버튼은 어느 쪽이든 작동합니다. + 앱 추가 + 차량에 표시되지 않는 앱용입니다. 음악은 Gearslip 플레이어로 재생되거나 메시지가 메시지 화면에 표시됩니다. + 앱 선택 + %1$s 표시 방식 + 음악 및 오디오 + 재생 중인 항목과 재생, 일시중지, 건너뛰기. 둘러볼 라이브러리는 없습니다. + 메시지 + 알림을 소리 내어 읽고 답장할 수 있습니다. + 직접 추가함 + %1$s 삭제 diff --git a/gearslip/src/main/res/values-nl/strings.xml b/gearslip/src/main/res/values-nl/strings.xml index e3b163a..8fa839a 100644 --- a/gearslip/src/main/res/values-nl/strings.xml +++ b/gearslip/src/main/res/values-nl/strings.xml @@ -942,4 +942,14 @@ Advertenties niet geblokkeerd Microfoonknop De assistentknop in de navigatiebalk. De spraakknop van de auto werkt hoe dan ook. + App toevoegen + Voor een app die de auto niet toont: de muziek speelt via de speler van Gearslip, of de berichten verschijnen in Berichten. + Kies een app + %1$s tonen als + Muziek en audio + Wat er speelt, met afspelen, pauzeren en overslaan. Geen bibliotheek om door te bladeren. + Berichten + De meldingen, voorgelezen, met antwoorden. + Door jou toegevoegd + %1$s verwijderen diff --git a/gearslip/src/main/res/values-pl/strings.xml b/gearslip/src/main/res/values-pl/strings.xml index 3e0f57d..aa90e82 100644 --- a/gearslip/src/main/res/values-pl/strings.xml +++ b/gearslip/src/main/res/values-pl/strings.xml @@ -962,4 +962,14 @@ Reklamy nieblokowane Przycisk mikrofonu Przycisk asystenta na pasku nawigacji. Przycisk głosowy w samochodzie działa niezależnie od tego. + Dodaj aplikację + Dla aplikacji, której samochód nie pokazuje: jej muzyka gra przez odtwarzacz Gearslip albo jej wiadomości pojawiają się w Wiadomościach. + Wybierz aplikację + Pokaż %1$s jako + Muzyka i audio + Teraz odtwarzane, z odtwarzaniem, pauzą i przewijaniem. Bez biblioteki do przeglądania. + Wiadomości + Jej powiadomienia, czytane na głos, z odpowiedziami. + Dodana przez Ciebie + Usuń %1$s diff --git a/gearslip/src/main/res/values-pt-rBR/strings.xml b/gearslip/src/main/res/values-pt-rBR/strings.xml index 4366b16..6dfccd4 100644 --- a/gearslip/src/main/res/values-pt-rBR/strings.xml +++ b/gearslip/src/main/res/values-pt-rBR/strings.xml @@ -952,4 +952,14 @@ Anúncios não bloqueados Botão do microfone O botão do assistente na barra de navegação. O botão de voz do carro funciona de qualquer jeito. + Adicionar um app + Para um app que o carro não lista: a música toca pelo player do Gearslip, ou as mensagens aparecem em Mensagens. + Escolha um app + Mostrar %1$s como + Música e áudio + O que está tocando, com tocar, pausar e pular. Não há biblioteca para navegar. + Mensagens + As notificações dele, lidas em voz alta, com respostas. + Adicionado por você + Remover %1$s diff --git a/gearslip/src/main/res/values-ro/strings.xml b/gearslip/src/main/res/values-ro/strings.xml index 23ee49a..94c0d8c 100644 --- a/gearslip/src/main/res/values-ro/strings.xml +++ b/gearslip/src/main/res/values-ro/strings.xml @@ -834,4 +834,14 @@ Reclame neblocate Butonul microfonului Butonul asistentului din bara de navigare. Butonul vocal al mașinii funcționează oricum. + Adaugă o aplicație + Pentru o aplicație pe care mașina n-o afișează: muzica ei merge prin playerul Gearslip, sau mesajele ei apar în Mesaje. + Alege o aplicație + Afișează %1$s ca + Muzică și audio + Ce se redă, cu redare, pauză și salt. Nu există bibliotecă de răsfoit. + Mesaje + Notificările ei, citite cu voce tare, cu răspunsuri. + Adăugată de tine + Elimină %1$s diff --git a/gearslip/src/main/res/values-ru/strings.xml b/gearslip/src/main/res/values-ru/strings.xml index e88661f..9a3ec2f 100644 --- a/gearslip/src/main/res/values-ru/strings.xml +++ b/gearslip/src/main/res/values-ru/strings.xml @@ -962,4 +962,14 @@ Реклама не блокируется Кнопка микрофона Кнопка ассистента на панели навигации. Голосовая кнопка машины работает в любом случае. + Добавить приложение + Для приложения, которого нет в машине: его музыка играет через плеер Gearslip, или его сообщения показываются в «Сообщениях». + Выберите приложение + Показывать %1$s как + Музыка и аудио + Сейчас играет: воспроизведение, пауза и переключение. Библиотеки для просмотра нет. + Сообщения + Его уведомления, прочитанные вслух, с ответами. + Добавлено вами + Удалить %1$s diff --git a/gearslip/src/main/res/values-sv/strings.xml b/gearslip/src/main/res/values-sv/strings.xml index 5977320..8682d6b 100644 --- a/gearslip/src/main/res/values-sv/strings.xml +++ b/gearslip/src/main/res/values-sv/strings.xml @@ -942,4 +942,14 @@ Annonser blockeras inte Mikrofonknapp Assistentens knapp i navigeringsfältet. Bilens egen röstknapp fungerar oavsett. + Lägg till en app + För en app som bilen inte visar: musiken spelas i Gearslips spelare, eller så visas meddelandena under Meddelanden. + Välj en app + Visa %1$s som + Musik och ljud + Det som spelas, med spela, pausa och hoppa. Inget bibliotek att bläddra i. + Meddelanden + Dess aviseringar, upplästa, med svar. + Tillagd av dig + Ta bort %1$s diff --git a/gearslip/src/main/res/values-tr/strings.xml b/gearslip/src/main/res/values-tr/strings.xml index 21a20fc..c897e67 100644 --- a/gearslip/src/main/res/values-tr/strings.xml +++ b/gearslip/src/main/res/values-tr/strings.xml @@ -942,4 +942,14 @@ Reklamlar engellenmiyor Mikrofon düğmesi Gezinme çubuğundaki asistan düğmesi. Aracın kendi ses düğmesi her durumda çalışır. + Uygulama ekle + Aracın listelemediği bir uygulama için: müziği Gearslip oynatıcısında çalar veya mesajları Mesajlar ekranında görünür. + Bir uygulama seçin + %1$s şu şekilde gösterilsin + Müzik ve ses + Çalan içerik; oynat, duraklat ve atla. Göz atılacak bir kitaplık yok. + Mesajlar + Bildirimleri, sesli okunur ve yanıtlanabilir. + Sizin eklediğiniz + %1$s kaldır diff --git a/gearslip/src/main/res/values-uk/strings.xml b/gearslip/src/main/res/values-uk/strings.xml index 3ec17de..9ad3ae2 100644 --- a/gearslip/src/main/res/values-uk/strings.xml +++ b/gearslip/src/main/res/values-uk/strings.xml @@ -962,4 +962,14 @@ Реклама не блокується Кнопка мікрофона Кнопка асистента на панелі навігації. Голосова кнопка авто працює в будь-якому разі. + Додати застосунок + Для застосунку, якого немає в авто: його музика грає через плеєр Gearslip, або його повідомлення показуються в «Повідомленнях». + Виберіть застосунок + Показувати %1$s як + Музика й аудіо + Зараз грає: відтворення, пауза й перемикання. Бібліотеки для перегляду немає. + Повідомлення + Його сповіщення, прочитані вголос, із відповідями. + Додано вами + Видалити %1$s diff --git a/gearslip/src/main/res/values-zh-rCN/strings.xml b/gearslip/src/main/res/values-zh-rCN/strings.xml index 102c6a6..7029074 100644 --- a/gearslip/src/main/res/values-zh-rCN/strings.xml +++ b/gearslip/src/main/res/values-zh-rCN/strings.xml @@ -932,4 +932,14 @@ 不拦截广告 麦克风按钮 导航栏中的助理按钮。车辆自带的语音按钮始终可用。 + 添加应用 + 适用于车上没有列出的应用:其音乐通过 Gearslip 的播放器播放,或其消息显示在“消息”中。 + 选择应用 + 将 %1$s 显示为 + 音乐和音频 + 正在播放的内容,可播放、暂停和跳过。没有可浏览的媒体库。 + 消息 + 朗读其通知,并可回复。 + 由你添加 + 移除 %1$s diff --git a/gearslip/src/main/res/values/strings.xml b/gearslip/src/main/res/values/strings.xml index e1a8d7f..13d2573 100644 --- a/gearslip/src/main/res/values/strings.xml +++ b/gearslip/src/main/res/values/strings.xml @@ -991,4 +991,14 @@ Ads not blocked Mic button The assistant\'s button in the nav bar. The car\'s own voice button works either way. + Add an app + For an app the car doesn\'t list: its music plays through Gearslip\'s player, or its messages show on the Messages screen. + Choose an app + Show %1$s as + Music and audio + Now playing, with play, pause and skip. There\'s no library to browse. + Messages + Its notifications, read aloud, with replies. + Added by you + Remove %1$s