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.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. */
@@ -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<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. */
private fun declaresMedia(manager: PackageManager, service: android.content.pm.ServiceInfo): Boolean {
if (CarDescriptor.meta(manager, service.packageName, service) == null) return false
@@ -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<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.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<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()
@@ -982,4 +982,14 @@
<string name="summary_adblock_off">الإعلانات غير محظورة</string>
<string name="car_nav_mic">زر الميكروفون</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>
@@ -962,4 +962,14 @@
<string name="summary_adblock_off">Reklamy neblokovány</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_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>
@@ -824,4 +824,14 @@
<string name="summary_adblock_off">Werbung nicht blockiert</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_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>
@@ -826,4 +826,14 @@
<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_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>
@@ -952,4 +952,14 @@
<string name="summary_adblock_off">Pubs non bloquées</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_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>
@@ -952,4 +952,14 @@
<string name="summary_adblock_off">Pubblicità non bloccata</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_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>
@@ -932,4 +932,14 @@
<string name="summary_adblock_off">広告をブロックしない</string>
<string name="car_nav_mic">マイクボタン</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>
@@ -932,4 +932,14 @@
<string name="summary_adblock_off">광고 차단 안 함</string>
<string name="car_nav_mic">마이크 버튼</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>
@@ -942,4 +942,14 @@
<string name="summary_adblock_off">Advertenties niet geblokkeerd</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_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>
@@ -962,4 +962,14 @@
<string name="summary_adblock_off">Reklamy nieblokowane</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_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>
@@ -952,4 +952,14 @@
<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_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>
@@ -834,4 +834,14 @@
<string name="summary_adblock_off">Reclame neblocate</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_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>
@@ -962,4 +962,14 @@
<string name="summary_adblock_off">Реклама не блокируется</string>
<string name="car_nav_mic">Кнопка микрофона</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>
@@ -942,4 +942,14 @@
<string name="summary_adblock_off">Annonser blockeras inte</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_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>
@@ -942,4 +942,14 @@
<string name="summary_adblock_off">Reklamlar engellenmiyor</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_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>
@@ -962,4 +962,14 @@
<string name="summary_adblock_off">Реклама не блокується</string>
<string name="car_nav_mic">Кнопка мікрофона</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>
@@ -932,4 +932,14 @@
<string name="summary_adblock_off">不拦截广告</string>
<string name="car_nav_mic">麦克风按钮</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>
+10
View File
@@ -991,4 +991,14 @@
<string name="summary_adblock_off">Ads not blocked</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_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>