mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
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:
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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user