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.
This commit is contained in:
Seb3thehacker committed 2026-11-06 15:38:11 -08:00
1 parent 9e1295d0de
commit 5ea62b4bd6
23 files changed
+429 -4

No files matched your search

@@ -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<List<App>?>(null)
/** Null until [all] has read the settings once. */
val state: StateFlow<List<App>?> = flow
fun all(context: Context): List<App> = flow.value ?: read(context).also { flow.value = it }
fun of(context: Context, kind: Kind): List<App> = 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<App>) {
flow.value = apps
AppSettings.putString(context, KEY, apps.joinToString(",") { "${it.packageName}:${it.kind.id}" })
}
private fun read(context: Context): List<App> =
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)
}
}
@@ -4,6 +4,7 @@ import app.seb3thehacker.gearslip.R
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.KeyEvent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.media.MediaMetadata import android.media.MediaMetadata
import android.media.Rating import android.media.Rating
@@ -281,6 +282,14 @@ class CarMedia(private val context: Context) {
attachToSession(app) attachToSession(app)
return 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() { val mb = MediaBrowser(context, app.component, object : MediaBrowser.ConnectionCallback() {
override fun onConnected() = this@CarMedia.onConnected(app) override fun onConnected() = this@CarMedia.onConnected(app)
override fun onConnectionFailed() { override fun onConnectionFailed() {
@@ -344,7 +353,7 @@ class CarMedia(private val context: Context) {
GearslipLog.w("media: ${app.label} has no active session to control") 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, // A session-only app (Spotify) just isn't running - say so and offer to open it,
// rather than calling it unsupported. // 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 Rejection.NEEDS_APP_RUNNING
} else { } else {
Rejection.REFUSED Rejection.REFUSED
@@ -834,6 +843,33 @@ class CarMedia(private val context: Context) {
main.postDelayed({ attachToSession(app, launched = true) }, LAUNCH_WAIT_MS) 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. */ /** Brings [app] to the foreground on the phone so it publishes its session. */
private fun openApp(app: MediaApp): Boolean { private fun openApp(app: MediaApp): Boolean {
val intent = context.packageManager.getLaunchIntentForPackage(app.component.packageName) ?: return false 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 const val SESSION_RETRY_MS = 500L
/** After opening a cold app, how long to wait before the first look for its session. */ /** After opening a cold app, how long to wait before the first look for its session. */
const val LAUNCH_WAIT_MS = 1_500L 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. */ /** Retries once the app has been opened: a cold start can take a few seconds to publish. */
const val SESSION_RETRIES_LAUNCHED = 16 const val SESSION_RETRIES_LAUNCHED = 16
/** How long an app gets to start playing a search before Gearslip says it didn't. */ /** How long an app gets to start playing a search before Gearslip says it didn't. */
@@ -1,5 +1,7 @@
package app.seb3thehacker.gearslip.media package app.seb3thehacker.gearslip.media
import app.seb3thehacker.gearslip.host.CustomCarApps
import app.seb3thehacker.gearslip.host.AndroidAuto import app.seb3thehacker.gearslip.host.AndroidAuto
import app.seb3thehacker.gearslip.host.CarDescriptor import app.seb3thehacker.gearslip.host.CarDescriptor
import app.seb3thehacker.gearslip.host.KnownApps import app.seb3thehacker.gearslip.host.KnownApps
@@ -15,6 +17,11 @@ class MediaApp(
val label: String, val label: String,
/** Whose audio to capture: playback capture is scoped to one uid so nothing else leaks in. */ /** Whose audio to capture: playback capture is scoped to one uid so nothing else leaks in. */
val uid: Int, 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 } .distinctBy { it.component.packageName }
.sortedBy { it.label.lowercase() }
.toList() .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<String>): List<MediaApp> {
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. */ /** 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 { private fun declaresMedia(manager: PackageManager, service: android.content.pm.ServiceInfo): Boolean {
if (CarDescriptor.meta(manager, service.packageName, service) == null) return false if (CarDescriptor.meta(manager, service.packageName, service) == null) return false
@@ -1,5 +1,7 @@
package app.seb3thehacker.gearslip.notify package app.seb3thehacker.gearslip.notify
import app.seb3thehacker.gearslip.host.CustomCarApps
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import app.seb3thehacker.gearslip.host.AndroidAuto import app.seb3thehacker.gearslip.host.AndroidAuto
@@ -33,7 +35,17 @@ object MessagingCatalog {
}.getOrDefault(pkg) }.getOrDefault(pkg)
MessagingApp(pkg, label) MessagingApp(pkg, label)
} }
.sortedBy { it.label.lowercase() }
.toList() .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<String>): List<MessagingApp> {
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())
}
} }
} }
@@ -21,6 +21,12 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Checkbox 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.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -399,16 +405,131 @@ private fun AppRows() {
LaunchedEffect(Unit) { withContext(Dispatchers.IO) { LauncherCache.load(context, force = true) } } LaunchedEffect(Unit) { withContext(Dispatchers.IO) { LauncherCache.load(context, force = true) } }
val rows = apps.orEmpty().mapNotNull { entry -> entry.componentId?.let { it to entry } } 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)) 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) -> rows.forEachIndexed { i, (id, entry) ->
val shown = id !in hidden 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() if (i > 0) SettingsDivider()
SettingsRow( SettingsRow(
entry.label, entry.label,
Modifier.toggleable(shown, role = Role.Checkbox) { CarSettings.setAppHidden(id, !it) }, 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)) } }, 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<String>,
onDismiss: () -> Unit,
onAdd: (String, app.seb3thehacker.gearslip.host.CustomCarApps.Kind) -> Unit,
) {
val context = androidx.compose.ui.platform.LocalContext.current
var picked by remember { mutableStateOf<Pair<String, String>?>(null) }
val candidates by androidx.compose.runtime.produceState<List<Triple<String, String, androidx.compose.ui.graphics.ImageBitmap?>>?>(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 <T> List<Pair<T, String>>.label(value: T): String = firstOrNull { it.first == value }?.second.orEmpty() private fun <T> List<Pair<T, String>>.label(value: T): String = firstOrNull { it.first == value }?.second.orEmpty()
@@ -982,4 +982,14 @@
<string name="summary_adblock_off">الإعلانات غير محظورة</string> <string name="summary_adblock_off">الإعلانات غير محظورة</string>
<string name="car_nav_mic">زر الميكروفون</string> <string name="car_nav_mic">زر الميكروفون</string>
<string name="car_nav_mic_note">زر المساعد في شريط التنقل. يعمل زر الصوت في السيارة في كل الأحوال.</string> <string name="car_nav_mic_note">زر المساعد في شريط التنقل. يعمل زر الصوت في السيارة في كل الأحوال.</string>
<string name="car_apps_add">إضافة تطبيق</string>
<string name="car_apps_add_note">لتطبيق لا تعرضه السيارة: تُشغَّل موسيقاه عبر مشغّل Gearslip، أو تظهر رسائله في شاشة الرسائل.</string>
<string name="car_apps_add_pick">اختر تطبيقًا</string>
<string name="car_apps_add_as">عرض %1$s بوصفه</string>
<string name="car_apps_as_media">الموسيقى والصوت</string>
<string name="car_apps_as_media_note">ما يُشغَّل الآن، مع التشغيل والإيقاف المؤقت والتخطي. لا توجد مكتبة للتصفح.</string>
<string name="car_apps_as_messaging">الرسائل</string>
<string name="car_apps_as_messaging_note">إشعاراته، تُقرأ بصوت عالٍ، مع الردود.</string>
<string name="car_apps_added_by_you">أضفته أنت</string>
<string name="car_apps_remove">إزالة %1$s</string>
</resources> </resources>
@@ -962,4 +962,14 @@
<string name="summary_adblock_off">Reklamy neblokovány</string> <string name="summary_adblock_off">Reklamy neblokovány</string>
<string name="car_nav_mic">Tlačítko mikrofonu</string> <string name="car_nav_mic">Tlačítko mikrofonu</string>
<string name="car_nav_mic_note">Tlačítko asistenta na navigační liště. Hlasové tlačítko auta funguje tak jako tak.</string> <string name="car_nav_mic_note">Tlačítko asistenta na navigační liště. Hlasové tlačítko auta funguje tak jako tak.</string>
<string name="car_apps_add">Přidat aplikaci</string>
<string name="car_apps_add_note">Pro aplikaci, kterou auto nezobrazuje: její hudba hraje přes přehrávač Gearslip, nebo se její zprávy zobrazí ve Zprávách.</string>
<string name="car_apps_add_pick">Vyberte aplikaci</string>
<string name="car_apps_add_as">Zobrazit %1$s jako</string>
<string name="car_apps_as_media">Hudba a zvuk</string>
<string name="car_apps_as_media_note">Právě hraje, s přehráním, pozastavením a přeskočením. Bez knihovny k procházení.</string>
<string name="car_apps_as_messaging">Zprávy</string>
<string name="car_apps_as_messaging_note">Její oznámení, předčítaná nahlas, s odpověďmi.</string>
<string name="car_apps_added_by_you">Přidáno vámi</string>
<string name="car_apps_remove">Odebrat %1$s</string>
</resources> </resources>
@@ -824,4 +824,14 @@
<string name="summary_adblock_off">Werbung nicht blockiert</string> <string name="summary_adblock_off">Werbung nicht blockiert</string>
<string name="car_nav_mic">Mikrofontaste</string> <string name="car_nav_mic">Mikrofontaste</string>
<string name="car_nav_mic_note">Die Assistent-Taste in der Navigationsleiste. Die Sprachtaste des Autos funktioniert so oder so.</string> <string name="car_nav_mic_note">Die Assistent-Taste in der Navigationsleiste. Die Sprachtaste des Autos funktioniert so oder so.</string>
<string name="car_apps_add">App hinzufügen</string>
<string name="car_apps_add_note">Für eine App, die das Auto nicht anzeigt: Ihre Musik läuft über den Player von Gearslip, oder ihre Nachrichten erscheinen in Nachrichten.</string>
<string name="car_apps_add_pick">App auswählen</string>
<string name="car_apps_add_as">%1$s anzeigen als</string>
<string name="car_apps_as_media">Musik und Audio</string>
<string name="car_apps_as_media_note">Aktueller Titel mit Wiedergabe, Pause und Überspringen. Keine Mediathek zum Durchsuchen.</string>
<string name="car_apps_as_messaging">Nachrichten</string>
<string name="car_apps_as_messaging_note">Ihre Benachrichtigungen, vorgelesen, mit Antworten.</string>
<string name="car_apps_added_by_you">Von dir hinzugefügt</string>
<string name="car_apps_remove">%1$s entfernen</string>
</resources> </resources>
@@ -826,4 +826,14 @@
<string name="summary_adblock_off">Anuncios sin bloquear</string> <string name="summary_adblock_off">Anuncios sin bloquear</string>
<string name="car_nav_mic">Botón del micrófono</string> <string name="car_nav_mic">Botón del micrófono</string>
<string name="car_nav_mic_note">El botón del asistente en la barra de navegación. El botón de voz del coche funciona igualmente.</string> <string name="car_nav_mic_note">El botón del asistente en la barra de navegación. El botón de voz del coche funciona igualmente.</string>
<string name="car_apps_add">Añadir una app</string>
<string name="car_apps_add_note">Para una app que el coche no muestra: su música suena con el reproductor de Gearslip, o sus mensajes aparecen en Mensajes.</string>
<string name="car_apps_add_pick">Elige una app</string>
<string name="car_apps_add_as">Mostrar %1$s como</string>
<string name="car_apps_as_media">Música y audio</string>
<string name="car_apps_as_media_note">Lo que suena, con reproducir, pausar y saltar. No hay biblioteca para explorar.</string>
<string name="car_apps_as_messaging">Mensajes</string>
<string name="car_apps_as_messaging_note">Sus notificaciones, leídas en voz alta, con respuestas.</string>
<string name="car_apps_added_by_you">Añadida por ti</string>
<string name="car_apps_remove">Quitar %1$s</string>
</resources> </resources>
@@ -952,4 +952,14 @@
<string name="summary_adblock_off">Pubs non bloquées</string> <string name="summary_adblock_off">Pubs non bloquées</string>
<string name="car_nav_mic">Bouton micro</string> <string name="car_nav_mic">Bouton micro</string>
<string name="car_nav_mic_note">Le bouton de l’assistant dans la barre de navigation. Le bouton vocal de la voiture fonctionne dans tous les cas.</string> <string name="car_nav_mic_note">Le bouton de l’assistant dans la barre de navigation. Le bouton vocal de la voiture fonctionne dans tous les cas.</string>
<string name="car_apps_add">Ajouter une appli</string>
<string name="car_apps_add_note">Pour une appli que la voiture n’affiche pas : sa musique passe par le lecteur de Gearslip, ou ses messages s’affichent dans Messages.</string>
<string name="car_apps_add_pick">Choisir une appli</string>
<string name="car_apps_add_as">Afficher %1$s comme</string>
<string name="car_apps_as_media">Musique et audio</string>
<string name="car_apps_as_media_note">Lecture en cours, avec lecture, pause et piste suivante. Pas de bibliothèque à parcourir.</string>
<string name="car_apps_as_messaging">Messages</string>
<string name="car_apps_as_messaging_note">Ses notifications, lues à voix haute, avec réponses.</string>
<string name="car_apps_added_by_you">Ajoutée par vous</string>
<string name="car_apps_remove">Retirer %1$s</string>
</resources> </resources>
@@ -952,4 +952,14 @@
<string name="summary_adblock_off">Pubblicità non bloccata</string> <string name="summary_adblock_off">Pubblicità non bloccata</string>
<string name="car_nav_mic">Pulsante microfono</string> <string name="car_nav_mic">Pulsante microfono</string>
<string name="car_nav_mic_note">Il pulsante dell’assistente nella barra di navigazione. Il tasto vocale dell’auto funziona comunque.</string> <string name="car_nav_mic_note">Il pulsante dell’assistente nella barra di navigazione. Il tasto vocale dell’auto funziona comunque.</string>
<string name="car_apps_add">Aggiungi un’app</string>
<string name="car_apps_add_note">Per un’app che l’auto non mostra: la sua musica passa dal lettore di Gearslip, oppure i suoi messaggi compaiono in Messaggi.</string>
<string name="car_apps_add_pick">Scegli un’app</string>
<string name="car_apps_add_as">Mostra %1$s come</string>
<string name="car_apps_as_media">Musica e audio</string>
<string name="car_apps_as_media_note">In riproduzione, con play, pausa e salto. Nessuna libreria da sfogliare.</string>
<string name="car_apps_as_messaging">Messaggi</string>
<string name="car_apps_as_messaging_note">Le sue notifiche, lette ad alta voce, con risposte.</string>
<string name="car_apps_added_by_you">Aggiunta da te</string>
<string name="car_apps_remove">Rimuovi %1$s</string>
</resources> </resources>
@@ -932,4 +932,14 @@
<string name="summary_adblock_off">広告をブロックしない</string> <string name="summary_adblock_off">広告をブロックしない</string>
<string name="car_nav_mic">マイクボタン</string> <string name="car_nav_mic">マイクボタン</string>
<string name="car_nav_mic_note">ナビゲーションバーのアシスタントボタンです。車の音声ボタンはどちらでも使えます。</string> <string name="car_nav_mic_note">ナビゲーションバーのアシスタントボタンです。車の音声ボタンはどちらでも使えます。</string>
<string name="car_apps_add">アプリを追加</string>
<string name="car_apps_add_note">車に表示されないアプリ向けです。音楽は Gearslip のプレーヤーで再生され、メッセージはメッセージ画面に表示されます。</string>
<string name="car_apps_add_pick">アプリを選択</string>
<string name="car_apps_add_as">%1$s の表示方法</string>
<string name="car_apps_as_media">音楽とオーディオ</string>
<string name="car_apps_as_media_note">再生中の曲と、再生・一時停止・スキップ。ライブラリの閲覧はできません。</string>
<string name="car_apps_as_messaging">メッセージ</string>
<string name="car_apps_as_messaging_note">通知を読み上げ、返信できます。</string>
<string name="car_apps_added_by_you">自分で追加</string>
<string name="car_apps_remove">%1$s を削除</string>
</resources> </resources>
@@ -932,4 +932,14 @@
<string name="summary_adblock_off">광고 차단 안 함</string> <string name="summary_adblock_off">광고 차단 안 함</string>
<string name="car_nav_mic">마이크 버튼</string> <string name="car_nav_mic">마이크 버튼</string>
<string name="car_nav_mic_note">내비게이션 바의 어시스턴트 버튼입니다. 차량의 음성 버튼은 어느 쪽이든 작동합니다.</string> <string name="car_nav_mic_note">내비게이션 바의 어시스턴트 버튼입니다. 차량의 음성 버튼은 어느 쪽이든 작동합니다.</string>
<string name="car_apps_add">앱 추가</string>
<string name="car_apps_add_note">차량에 표시되지 않는 앱용입니다. 음악은 Gearslip 플레이어로 재생되거나 메시지가 메시지 화면에 표시됩니다.</string>
<string name="car_apps_add_pick">앱 선택</string>
<string name="car_apps_add_as">%1$s 표시 방식</string>
<string name="car_apps_as_media">음악 및 오디오</string>
<string name="car_apps_as_media_note">재생 중인 항목과 재생, 일시중지, 건너뛰기. 둘러볼 라이브러리는 없습니다.</string>
<string name="car_apps_as_messaging">메시지</string>
<string name="car_apps_as_messaging_note">알림을 소리 내어 읽고 답장할 수 있습니다.</string>
<string name="car_apps_added_by_you">직접 추가함</string>
<string name="car_apps_remove">%1$s 삭제</string>
</resources> </resources>
@@ -942,4 +942,14 @@
<string name="summary_adblock_off">Advertenties niet geblokkeerd</string> <string name="summary_adblock_off">Advertenties niet geblokkeerd</string>
<string name="car_nav_mic">Microfoonknop</string> <string name="car_nav_mic">Microfoonknop</string>
<string name="car_nav_mic_note">De assistentknop in de navigatiebalk. De spraakknop van de auto werkt hoe dan ook.</string> <string name="car_nav_mic_note">De assistentknop in de navigatiebalk. De spraakknop van de auto werkt hoe dan ook.</string>
<string name="car_apps_add">App toevoegen</string>
<string name="car_apps_add_note">Voor een app die de auto niet toont: de muziek speelt via de speler van Gearslip, of de berichten verschijnen in Berichten.</string>
<string name="car_apps_add_pick">Kies een app</string>
<string name="car_apps_add_as">%1$s tonen als</string>
<string name="car_apps_as_media">Muziek en audio</string>
<string name="car_apps_as_media_note">Wat er speelt, met afspelen, pauzeren en overslaan. Geen bibliotheek om door te bladeren.</string>
<string name="car_apps_as_messaging">Berichten</string>
<string name="car_apps_as_messaging_note">De meldingen, voorgelezen, met antwoorden.</string>
<string name="car_apps_added_by_you">Door jou toegevoegd</string>
<string name="car_apps_remove">%1$s verwijderen</string>
</resources> </resources>
@@ -962,4 +962,14 @@
<string name="summary_adblock_off">Reklamy nieblokowane</string> <string name="summary_adblock_off">Reklamy nieblokowane</string>
<string name="car_nav_mic">Przycisk mikrofonu</string> <string name="car_nav_mic">Przycisk mikrofonu</string>
<string name="car_nav_mic_note">Przycisk asystenta na pasku nawigacji. Przycisk głosowy w samochodzie działa niezależnie od tego.</string> <string name="car_nav_mic_note">Przycisk asystenta na pasku nawigacji. Przycisk głosowy w samochodzie działa niezależnie od tego.</string>
<string name="car_apps_add">Dodaj aplikację</string>
<string name="car_apps_add_note">Dla aplikacji, której samochód nie pokazuje: jej muzyka gra przez odtwarzacz Gearslip albo jej wiadomości pojawiają się w Wiadomościach.</string>
<string name="car_apps_add_pick">Wybierz aplikację</string>
<string name="car_apps_add_as">Pokaż %1$s jako</string>
<string name="car_apps_as_media">Muzyka i audio</string>
<string name="car_apps_as_media_note">Teraz odtwarzane, z odtwarzaniem, pauzą i przewijaniem. Bez biblioteki do przeglądania.</string>
<string name="car_apps_as_messaging">Wiadomości</string>
<string name="car_apps_as_messaging_note">Jej powiadomienia, czytane na głos, z odpowiedziami.</string>
<string name="car_apps_added_by_you">Dodana przez Ciebie</string>
<string name="car_apps_remove">Usuń %1$s</string>
</resources> </resources>
@@ -952,4 +952,14 @@
<string name="summary_adblock_off">Anúncios não bloqueados</string> <string name="summary_adblock_off">Anúncios não bloqueados</string>
<string name="car_nav_mic">Botão do microfone</string> <string name="car_nav_mic">Botão do microfone</string>
<string name="car_nav_mic_note">O botão do assistente na barra de navegação. O botão de voz do carro funciona de qualquer jeito.</string> <string name="car_nav_mic_note">O botão do assistente na barra de navegação. O botão de voz do carro funciona de qualquer jeito.</string>
<string name="car_apps_add">Adicionar um app</string>
<string name="car_apps_add_note">Para um app que o carro não lista: a música toca pelo player do Gearslip, ou as mensagens aparecem em Mensagens.</string>
<string name="car_apps_add_pick">Escolha um app</string>
<string name="car_apps_add_as">Mostrar %1$s como</string>
<string name="car_apps_as_media">Música e áudio</string>
<string name="car_apps_as_media_note">O que está tocando, com tocar, pausar e pular. Não há biblioteca para navegar.</string>
<string name="car_apps_as_messaging">Mensagens</string>
<string name="car_apps_as_messaging_note">As notificações dele, lidas em voz alta, com respostas.</string>
<string name="car_apps_added_by_you">Adicionado por você</string>
<string name="car_apps_remove">Remover %1$s</string>
</resources> </resources>
@@ -834,4 +834,14 @@
<string name="summary_adblock_off">Reclame neblocate</string> <string name="summary_adblock_off">Reclame neblocate</string>
<string name="car_nav_mic">Butonul microfonului</string> <string name="car_nav_mic">Butonul microfonului</string>
<string name="car_nav_mic_note">Butonul asistentului din bara de navigare. Butonul vocal al mașinii funcționează oricum.</string> <string name="car_nav_mic_note">Butonul asistentului din bara de navigare. Butonul vocal al mașinii funcționează oricum.</string>
<string name="car_apps_add">Adaugă o aplicație</string>
<string name="car_apps_add_note">Pentru o aplicație pe care mașina n-o afișează: muzica ei merge prin playerul Gearslip, sau mesajele ei apar în Mesaje.</string>
<string name="car_apps_add_pick">Alege o aplicație</string>
<string name="car_apps_add_as">Afișează %1$s ca</string>
<string name="car_apps_as_media">Muzică și audio</string>
<string name="car_apps_as_media_note">Ce se redă, cu redare, pauză și salt. Nu există bibliotecă de răsfoit.</string>
<string name="car_apps_as_messaging">Mesaje</string>
<string name="car_apps_as_messaging_note">Notificările ei, citite cu voce tare, cu răspunsuri.</string>
<string name="car_apps_added_by_you">Adăugată de tine</string>
<string name="car_apps_remove">Elimină %1$s</string>
</resources> </resources>
@@ -962,4 +962,14 @@
<string name="summary_adblock_off">Реклама не блокируется</string> <string name="summary_adblock_off">Реклама не блокируется</string>
<string name="car_nav_mic">Кнопка микрофона</string> <string name="car_nav_mic">Кнопка микрофона</string>
<string name="car_nav_mic_note">Кнопка ассистента на панели навигации. Голосовая кнопка машины работает в любом случае.</string> <string name="car_nav_mic_note">Кнопка ассистента на панели навигации. Голосовая кнопка машины работает в любом случае.</string>
<string name="car_apps_add">Добавить приложение</string>
<string name="car_apps_add_note">Для приложения, которого нет в машине: его музыка играет через плеер Gearslip, или его сообщения показываются в «Сообщениях».</string>
<string name="car_apps_add_pick">Выберите приложение</string>
<string name="car_apps_add_as">Показывать %1$s как</string>
<string name="car_apps_as_media">Музыка и аудио</string>
<string name="car_apps_as_media_note">Сейчас играет: воспроизведение, пауза и переключение. Библиотеки для просмотра нет.</string>
<string name="car_apps_as_messaging">Сообщения</string>
<string name="car_apps_as_messaging_note">Его уведомления, прочитанные вслух, с ответами.</string>
<string name="car_apps_added_by_you">Добавлено вами</string>
<string name="car_apps_remove">Удалить %1$s</string>
</resources> </resources>
@@ -942,4 +942,14 @@
<string name="summary_adblock_off">Annonser blockeras inte</string> <string name="summary_adblock_off">Annonser blockeras inte</string>
<string name="car_nav_mic">Mikrofonknapp</string> <string name="car_nav_mic">Mikrofonknapp</string>
<string name="car_nav_mic_note">Assistentens knapp i navigeringsfältet. Bilens egen röstknapp fungerar oavsett.</string> <string name="car_nav_mic_note">Assistentens knapp i navigeringsfältet. Bilens egen röstknapp fungerar oavsett.</string>
<string name="car_apps_add">Lägg till en app</string>
<string name="car_apps_add_note">För en app som bilen inte visar: musiken spelas i Gearslips spelare, eller så visas meddelandena under Meddelanden.</string>
<string name="car_apps_add_pick">Välj en app</string>
<string name="car_apps_add_as">Visa %1$s som</string>
<string name="car_apps_as_media">Musik och ljud</string>
<string name="car_apps_as_media_note">Det som spelas, med spela, pausa och hoppa. Inget bibliotek att bläddra i.</string>
<string name="car_apps_as_messaging">Meddelanden</string>
<string name="car_apps_as_messaging_note">Dess aviseringar, upplästa, med svar.</string>
<string name="car_apps_added_by_you">Tillagd av dig</string>
<string name="car_apps_remove">Ta bort %1$s</string>
</resources> </resources>
@@ -942,4 +942,14 @@
<string name="summary_adblock_off">Reklamlar engellenmiyor</string> <string name="summary_adblock_off">Reklamlar engellenmiyor</string>
<string name="car_nav_mic">Mikrofon düğmesi</string> <string name="car_nav_mic">Mikrofon düğmesi</string>
<string name="car_nav_mic_note">Gezinme çubuğundaki asistan düğmesi. Aracın kendi ses düğmesi her durumda çalışır.</string> <string name="car_nav_mic_note">Gezinme çubuğundaki asistan düğmesi. Aracın kendi ses düğmesi her durumda çalışır.</string>
<string name="car_apps_add">Uygulama ekle</string>
<string name="car_apps_add_note">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.</string>
<string name="car_apps_add_pick">Bir uygulama seçin</string>
<string name="car_apps_add_as">%1$s şu şekilde gösterilsin</string>
<string name="car_apps_as_media">Müzik ve ses</string>
<string name="car_apps_as_media_note">Çalan içerik; oynat, duraklat ve atla. Göz atılacak bir kitaplık yok.</string>
<string name="car_apps_as_messaging">Mesajlar</string>
<string name="car_apps_as_messaging_note">Bildirimleri, sesli okunur ve yanıtlanabilir.</string>
<string name="car_apps_added_by_you">Sizin eklediğiniz</string>
<string name="car_apps_remove">%1$s kaldır</string>
</resources> </resources>
@@ -962,4 +962,14 @@
<string name="summary_adblock_off">Реклама не блокується</string> <string name="summary_adblock_off">Реклама не блокується</string>
<string name="car_nav_mic">Кнопка мікрофона</string> <string name="car_nav_mic">Кнопка мікрофона</string>
<string name="car_nav_mic_note">Кнопка асистента на панелі навігації. Голосова кнопка авто працює в будь-якому разі.</string> <string name="car_nav_mic_note">Кнопка асистента на панелі навігації. Голосова кнопка авто працює в будь-якому разі.</string>
<string name="car_apps_add">Додати застосунок</string>
<string name="car_apps_add_note">Для застосунку, якого немає в авто: його музика грає через плеєр Gearslip, або його повідомлення показуються в «Повідомленнях».</string>
<string name="car_apps_add_pick">Виберіть застосунок</string>
<string name="car_apps_add_as">Показувати %1$s як</string>
<string name="car_apps_as_media">Музика й аудіо</string>
<string name="car_apps_as_media_note">Зараз грає: відтворення, пауза й перемикання. Бібліотеки для перегляду немає.</string>
<string name="car_apps_as_messaging">Повідомлення</string>
<string name="car_apps_as_messaging_note">Його сповіщення, прочитані вголос, із відповідями.</string>
<string name="car_apps_added_by_you">Додано вами</string>
<string name="car_apps_remove">Видалити %1$s</string>
</resources> </resources>
@@ -932,4 +932,14 @@
<string name="summary_adblock_off">不拦截广告</string> <string name="summary_adblock_off">不拦截广告</string>
<string name="car_nav_mic">麦克风按钮</string> <string name="car_nav_mic">麦克风按钮</string>
<string name="car_nav_mic_note">导航栏中的助理按钮。车辆自带的语音按钮始终可用。</string> <string name="car_nav_mic_note">导航栏中的助理按钮。车辆自带的语音按钮始终可用。</string>
<string name="car_apps_add">添加应用</string>
<string name="car_apps_add_note">适用于车上没有列出的应用:其音乐通过 Gearslip 的播放器播放,或其消息显示在“消息”中。</string>
<string name="car_apps_add_pick">选择应用</string>
<string name="car_apps_add_as">将 %1$s 显示为</string>
<string name="car_apps_as_media">音乐和音频</string>
<string name="car_apps_as_media_note">正在播放的内容,可播放、暂停和跳过。没有可浏览的媒体库。</string>
<string name="car_apps_as_messaging">消息</string>
<string name="car_apps_as_messaging_note">朗读其通知,并可回复。</string>
<string name="car_apps_added_by_you">由你添加</string>
<string name="car_apps_remove">移除 %1$s</string>
</resources> </resources>
+10
View File
@@ -991,4 +991,14 @@
<string name="summary_adblock_off">Ads not blocked</string> <string name="summary_adblock_off">Ads not blocked</string>
<string name="car_nav_mic">Mic button</string> <string name="car_nav_mic">Mic button</string>
<string name="car_nav_mic_note">The assistant\'s button in the nav bar. The car\'s own voice button works either way.</string> <string name="car_nav_mic_note">The assistant\'s button in the nav bar. The car\'s own voice button works either way.</string>
<string name="car_apps_add">Add an app</string>
<string name="car_apps_add_note">For an app the car doesn\'t list: its music plays through Gearslip\'s player, or its messages show on the Messages screen.</string>
<string name="car_apps_add_pick">Choose an app</string>
<string name="car_apps_add_as">Show %1$s as</string>
<string name="car_apps_as_media">Music and audio</string>
<string name="car_apps_as_media_note">Now playing, with play, pause and skip. There\'s no library to browse.</string>
<string name="car_apps_as_messaging">Messages</string>
<string name="car_apps_as_messaging_note">Its notifications, read aloud, with replies.</string>
<string name="car_apps_added_by_you">Added by you</string>
<string name="car_apps_remove">Remove %1$s</string>
</resources> </resources>