mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
car audio: pause the phone's music when the car switches to the radio, send AAC to cars that take nothing else, and open GM's own channel in GM cars
This commit is contained in:
1 parent
2812613bca
commit
73be554eb2
5 files changed
+180
-5
No files matched your search
@@ -84,6 +84,7 @@ class GearslipRunner(
|
|||||||
private var sensorTypes: List<Int> = emptyList()
|
private var sensorTypes: List<Int> = emptyList()
|
||||||
@Volatile private var audioLink: AudioLink? = null
|
@Volatile private var audioLink: AudioLink? = null
|
||||||
@Volatile private var guidanceLink: AudioLink? = null
|
@Volatile private var guidanceLink: AudioLink? = null
|
||||||
|
@Volatile private var vendorChannelId = -1
|
||||||
private var audioMessagesSeen = 0
|
private var audioMessagesSeen = 0
|
||||||
private var touchWidth = 0
|
private var touchWidth = 0
|
||||||
private var touchHeight = 0
|
private var touchHeight = 0
|
||||||
@@ -213,6 +214,8 @@ class GearslipRunner(
|
|||||||
guidanceLink?.onMessage(messageId, body)
|
guidanceLink?.onMessage(messageId, body)
|
||||||
} else if (frame.channel == CarBluetooth.channelId) {
|
} else if (frame.channel == CarBluetooth.channelId) {
|
||||||
CarBluetooth.onMessage(messageId, body)
|
CarBluetooth.onMessage(messageId, body)
|
||||||
|
} else if (frame.channel == vendorChannelId) {
|
||||||
|
onVendorMessage(messageId, body)
|
||||||
} else if (frame.channel == CarMic.current?.channelId) {
|
} else if (frame.channel == CarMic.current?.channelId) {
|
||||||
CarMic.current?.onMessage(messageId, body)
|
CarMic.current?.onMessage(messageId, body)
|
||||||
} else if (frame.channel == audioLink?.channelId) {
|
} else if (frame.channel == audioLink?.channelId) {
|
||||||
@@ -470,6 +473,7 @@ class GearslipRunner(
|
|||||||
startMediaStatusChannel(serviceDiscoveryResponse)
|
startMediaStatusChannel(serviceDiscoveryResponse)
|
||||||
startClusterNavChannel(serviceDiscoveryResponse)
|
startClusterNavChannel(serviceDiscoveryResponse)
|
||||||
startBluetooth(serviceDiscoveryResponse)
|
startBluetooth(serviceDiscoveryResponse)
|
||||||
|
startVendorChannel(serviceDiscoveryResponse)
|
||||||
|
|
||||||
val input = ServiceDiscovery.findInputService(serviceDiscoveryResponse, video.displayId)
|
val input = ServiceDiscovery.findInputService(serviceDiscoveryResponse, video.displayId)
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
@@ -504,7 +508,7 @@ class GearslipRunner(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (media.codecType != ServiceDiscovery.CODEC_PCM) {
|
if (media.codecType != ServiceDiscovery.CODEC_PCM) {
|
||||||
log.w("the car offers no PCM media sink, only ${media.codecName}; it may play our PCM as silence")
|
log.w("the car offers no PCM media sink, only ${media.codecName}; music goes as AAC")
|
||||||
}
|
}
|
||||||
val first = media.configs.firstOrNull()
|
val first = media.configs.firstOrNull()
|
||||||
SessionReport.audio(
|
SessionReport.audio(
|
||||||
@@ -515,6 +519,9 @@ class GearslipRunner(
|
|||||||
media,
|
media,
|
||||||
sendOnChannel = { id, body -> send(id, body, encrypted = true, channel = media.serviceId) },
|
sendOnChannel = { id, body -> send(id, body, encrypted = true, channel = media.serviceId) },
|
||||||
sendControl = { id, body -> send(id, body, encrypted = true) },
|
sendControl = { id, body -> send(id, body, encrypted = true) },
|
||||||
|
// The driver picked the radio or another source on the car: pause the phone's music
|
||||||
|
// as Android Auto does, instead of letting it play on to no one.
|
||||||
|
onTakenBack = { CarServices.pausePhoneMedia("the car switched to another source") },
|
||||||
)
|
)
|
||||||
openChannel(media.serviceId)
|
openChannel(media.serviceId)
|
||||||
}
|
}
|
||||||
@@ -571,6 +578,29 @@ class GearslipRunner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Bluetooth pairing, for calls: the channel only opens if the driver asks to pair. */
|
/** Bluetooth pairing, for calls: the channel only opens if the driver asks to pair. */
|
||||||
|
/**
|
||||||
|
* GM's own channel ("com.gm.vip"), opened in GM cars only. Issue #18: a 2021 Bolt ends every
|
||||||
|
* session about 25 s in, and of the channels it offers, this is the one a Dacia that stays
|
||||||
|
* connected doesn't have. What GM sends on it is private, so Gearslip only opens it and logs
|
||||||
|
* what arrives, by message id and size.
|
||||||
|
*/
|
||||||
|
private fun startVendorChannel(serviceDiscoveryResponse: ByteArray) {
|
||||||
|
val vendors = ServiceDiscovery.findVendorServices(serviceDiscoveryResponse)
|
||||||
|
vendors.forEach { log.i("vendor channel ${it.serviceId}: ${it.name}") }
|
||||||
|
val gm = vendors.firstOrNull { it.name.startsWith("com.gm.") } ?: return
|
||||||
|
vendorChannelId = gm.serviceId
|
||||||
|
log.i("-> ChannelOpenRequest on GM's channel ${gm.serviceId}")
|
||||||
|
openChannel(gm.serviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onVendorMessage(messageId: Int, body: ByteArray) {
|
||||||
|
if (messageId == MSG_CHANNEL_OPEN_RESPONSE) {
|
||||||
|
log.i("<- GM channel open: status=${Protobuf.readInt32Field(body, 1) ?: 0}")
|
||||||
|
} else {
|
||||||
|
log.i("<- GM channel message id=$messageId (${body.size} bytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startBluetooth(serviceDiscoveryResponse: ByteArray) {
|
private fun startBluetooth(serviceDiscoveryResponse: ByteArray) {
|
||||||
val bluetooth = ServiceDiscovery.findBluetoothService(serviceDiscoveryResponse) ?: run {
|
val bluetooth = ServiceDiscovery.findBluetoothService(serviceDiscoveryResponse) ?: run {
|
||||||
log.i("no Bluetooth service - the car can't be paired from here")
|
log.i("no Bluetooth service - the car can't be paired from here")
|
||||||
|
|||||||
@@ -160,6 +160,18 @@ object ServiceDiscovery {
|
|||||||
*/
|
*/
|
||||||
class BluetoothService(val serviceId: Int, val carAddress: String, val pairingMethods: List<Int>)
|
class BluetoothService(val serviceId: Int, val carAddress: String, val pairingMethods: List<Int>)
|
||||||
|
|
||||||
|
/** A carmaker's own channel (service field 12), for its apps on the phone: GM's is "com.gm.vip". */
|
||||||
|
class VendorService(val serviceId: Int, val name: String)
|
||||||
|
|
||||||
|
fun findVendorServices(response: ByteArray): List<VendorService> =
|
||||||
|
Wire.allBytes(Wire.fields(response), 1).mapNotNull { channel ->
|
||||||
|
val service = Wire.fields(channel)
|
||||||
|
val id = Wire.varint(service, 1)?.toInt() ?: return@mapNotNull null
|
||||||
|
val fields = Wire.fields(Wire.bytes(service, 12) ?: return@mapNotNull null)
|
||||||
|
val name = Wire.bytes(fields, 1)?.let { String(it, Charsets.UTF_8) } ?: return@mapNotNull null
|
||||||
|
VendorService(id, name)
|
||||||
|
}
|
||||||
|
|
||||||
fun findBluetoothService(response: ByteArray): BluetoothService? {
|
fun findBluetoothService(response: ByteArray): BluetoothService? {
|
||||||
for (channel in Wire.allBytes(Wire.fields(response), 1)) {
|
for (channel in Wire.allBytes(Wire.fields(response), 1)) {
|
||||||
val service = Wire.fields(channel)
|
val service = Wire.fields(channel)
|
||||||
@@ -178,6 +190,8 @@ object ServiceDiscovery {
|
|||||||
const val STREAM_GUIDANCE = 1
|
const val STREAM_GUIDANCE = 1
|
||||||
const val STREAM_MEDIA = 3
|
const val STREAM_MEDIA = 3
|
||||||
const val CODEC_PCM = 1
|
const val CODEC_PCM = 1
|
||||||
|
const val CODEC_AAC_LC = 2
|
||||||
|
const val CODEC_AAC_LC_ADTS = 4
|
||||||
|
|
||||||
private val STREAMS = mapOf(0 to "NONE", 1 to "GUIDANCE", 2 to "SYSTEM_AUDIO", 3 to "MEDIA", 4 to "TELEPHONY")
|
private val STREAMS = mapOf(0 to "NONE", 1 to "GUIDANCE", 2 to "SYSTEM_AUDIO", 3 to "MEDIA", 4 to "TELEPHONY")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package app.seb3thehacker.gearslip.audio
|
||||||
|
|
||||||
|
import android.media.MediaCodec
|
||||||
|
import android.media.MediaCodecInfo
|
||||||
|
import android.media.MediaFormat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns captured PCM into AAC-LC for a car whose music channel takes nothing else. Cars that
|
||||||
|
* offer PCM never get here: AAC costs a little delay and quality, and every car that works today
|
||||||
|
* is a PCM car.
|
||||||
|
*
|
||||||
|
* Android's encoder hands back raw access units, which is what AUDIO_AAC_LC expects. For
|
||||||
|
* AUDIO_AAC_LC_ADTS each unit gets the 7-byte ADTS header in front, so the car can find the
|
||||||
|
* frame edges itself. The encoder's first output is its configuration, not audio, and is skipped.
|
||||||
|
*/
|
||||||
|
internal class AacEncoder(private val sampleRate: Int, private val channels: Int, private val adts: Boolean) {
|
||||||
|
|
||||||
|
private val codec: MediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC).apply {
|
||||||
|
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, channels).apply {
|
||||||
|
setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
|
||||||
|
setInteger(MediaFormat.KEY_BIT_RATE, if (channels == 1) 96_000 else 192_000)
|
||||||
|
setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16 * 1024)
|
||||||
|
}
|
||||||
|
configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
private val info = MediaCodec.BufferInfo()
|
||||||
|
private var samplesIn = 0L
|
||||||
|
|
||||||
|
/** Encodes [size] bytes of 16-bit PCM and returns the AAC frames that came out, oldest first. */
|
||||||
|
fun encode(pcm: ByteArray, size: Int): List<ByteArray> {
|
||||||
|
var offset = 0
|
||||||
|
val out = ArrayList<ByteArray>()
|
||||||
|
while (offset < size) {
|
||||||
|
val index = codec.dequeueInputBuffer(INPUT_WAIT_US)
|
||||||
|
if (index < 0) {
|
||||||
|
drain(out)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val buffer = codec.getInputBuffer(index) ?: continue
|
||||||
|
val n = minOf(buffer.remaining(), size - offset)
|
||||||
|
buffer.put(pcm, offset, n)
|
||||||
|
val presentationUs = samplesIn * 1_000_000L / sampleRate
|
||||||
|
codec.queueInputBuffer(index, 0, n, presentationUs, 0)
|
||||||
|
samplesIn += n / (2 * channels)
|
||||||
|
offset += n
|
||||||
|
drain(out)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drain(out: MutableList<ByteArray>) {
|
||||||
|
while (true) {
|
||||||
|
val index = codec.dequeueOutputBuffer(info, 0)
|
||||||
|
if (index < 0) return
|
||||||
|
if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG == 0 && info.size > 0) {
|
||||||
|
val buffer = codec.getOutputBuffer(index)
|
||||||
|
if (buffer != null) {
|
||||||
|
val frame = ByteArray(info.size)
|
||||||
|
buffer.position(info.offset)
|
||||||
|
buffer.get(frame)
|
||||||
|
out.add(if (adts) adtsHeader(frame.size) + frame else frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codec.releaseOutputBuffer(index, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ADTS for one AAC-LC frame: sync word, no CRC, and the frame's length including this header. */
|
||||||
|
private fun adtsHeader(payload: Int): ByteArray {
|
||||||
|
val length = payload + 7
|
||||||
|
val freq = FREQUENCIES.indexOf(sampleRate).let { if (it < 0) 3 else it }
|
||||||
|
val profile = 1 // AAC-LC is object type 2, written as type minus one
|
||||||
|
return byteArrayOf(
|
||||||
|
0xFF.toByte(),
|
||||||
|
0xF1.toByte(),
|
||||||
|
((profile shl 6) or (freq shl 2) or (channels shr 2)).toByte(),
|
||||||
|
(((channels and 3) shl 6) or (length shr 11)).toByte(),
|
||||||
|
((length shr 3) and 0xFF).toByte(),
|
||||||
|
(((length and 7) shl 5) or 0x1F).toByte(),
|
||||||
|
0xFC.toByte(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
runCatching { codec.stop() }
|
||||||
|
runCatching { codec.release() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val INPUT_WAIT_US = 10_000L
|
||||||
|
val FREQUENCIES = listOf(96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ class AudioLink(
|
|||||||
private val asksFocus: Boolean = true,
|
private val asksFocus: Boolean = true,
|
||||||
private val onReady: (AudioLink) -> Unit = { CarAudio.attach(it) },
|
private val onReady: (AudioLink) -> Unit = { CarAudio.attach(it) },
|
||||||
private val onClose: (AudioLink) -> Unit = { CarAudio.detach(it) },
|
private val onClose: (AudioLink) -> Unit = { CarAudio.detach(it) },
|
||||||
|
/** The car took the sound back after giving it, as when the driver switches to the radio. */
|
||||||
|
private val onTakenBack: () -> Unit = {},
|
||||||
) : CarAudio.Link {
|
) : CarAudio.Link {
|
||||||
|
|
||||||
private var configIndex = 0
|
private var configIndex = 0
|
||||||
@@ -46,6 +48,9 @@ class AudioLink(
|
|||||||
private var dropped = 0
|
private var dropped = 0
|
||||||
@Volatile private var configTimer: Thread? = null
|
@Volatile private var configTimer: Thread? = null
|
||||||
private val ackLock = Object()
|
private val ackLock = Object()
|
||||||
|
/** Only for a sink that takes AAC and nothing else; PCM sinks get the capture as it is. */
|
||||||
|
@Volatile private var aac: AacEncoder? = null
|
||||||
|
private val aacSink get() = service.codecType == ServiceDiscovery.CODEC_AAC_LC || service.codecType == ServiceDiscovery.CODEC_AAC_LC_ADTS
|
||||||
|
|
||||||
private val config get() = service.configs.getOrNull(configIndex) ?: service.configs.firstOrNull()
|
private val config get() = service.configs.getOrNull(configIndex) ?: service.configs.firstOrNull()
|
||||||
override val sampleRate get() = config?.sampleRate ?: 48_000
|
override val sampleRate get() = config?.sampleRate ?: 48_000
|
||||||
@@ -156,9 +161,12 @@ class AudioLink(
|
|||||||
fun onFocus(state: Int) {
|
fun onFocus(state: Int) {
|
||||||
val name = FOCUS_STATES[state] ?: "unknown($state)"
|
val name = FOCUS_STATES[state] ?: "unknown($state)"
|
||||||
GearslipLog.i("$tag: <- AudioFocusResponse state=$name")
|
GearslipLog.i("$tag: <- AudioFocusResponse state=$name")
|
||||||
|
val had = focusGranted
|
||||||
focusGranted = state == FOCUS_STATE_GAIN || state == FOCUS_STATE_GAIN_TRANSIENT ||
|
focusGranted = state == FOCUS_STATE_GAIN || state == FOCUS_STATE_GAIN_TRANSIENT ||
|
||||||
state == FOCUS_STATE_GAIN_MEDIA_ONLY
|
state == FOCUS_STATE_GAIN_MEDIA_ONLY
|
||||||
if (focusGranted) startStream() else stop()
|
if (focusGranted) startStream() else stop()
|
||||||
|
// A ducking request leaves the music playing; a loss means another source has the speakers.
|
||||||
|
if (had && (state == FOCUS_STATE_LOSS || state == FOCUS_STATE_LOSS_TRANSIENT)) onTakenBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
@@ -181,6 +189,12 @@ class AudioLink(
|
|||||||
)
|
)
|
||||||
inFlight = 0
|
inFlight = 0
|
||||||
lastAckAt = System.currentTimeMillis()
|
lastAckAt = System.currentTimeMillis()
|
||||||
|
if (aacSink && aac == null) {
|
||||||
|
aac = runCatching { AacEncoder(sampleRate, channels, adts = service.codecType == ServiceDiscovery.CODEC_AAC_LC_ADTS) }
|
||||||
|
.onSuccess { GearslipLog.i("$tag: the car takes ${service.codecName} only - encoding to AAC") }
|
||||||
|
.onFailure { GearslipLog.e("$tag: couldn't start the AAC encoder", it) }
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
started = true
|
started = true
|
||||||
GearslipLog.i("$tag: -> Start(session=$sessionId, config=$configIndex) - streaming ${sampleRate}Hz x$channels")
|
GearslipLog.i("$tag: -> Start(session=$sessionId, config=$configIndex) - streaming ${sampleRate}Hz x$channels")
|
||||||
}
|
}
|
||||||
@@ -195,6 +209,8 @@ class AudioLink(
|
|||||||
}
|
}
|
||||||
focusRequested = false
|
focusRequested = false
|
||||||
focusGranted = false
|
focusGranted = false
|
||||||
|
aac?.release()
|
||||||
|
aac = null
|
||||||
GearslipLog.i("$tag: -> Stop, AudioFocusRequest(RELEASE) after $chunks chunks / $bytes bytes")
|
GearslipLog.i("$tag: -> Stop, AudioFocusRequest(RELEASE) after $chunks chunks / $bytes bytes")
|
||||||
chunks = 0
|
chunks = 0
|
||||||
bytes = 0
|
bytes = 0
|
||||||
@@ -202,6 +218,25 @@ class AudioLink(
|
|||||||
|
|
||||||
override fun write(pcm: ByteArray, size: Int) {
|
override fun write(pcm: ByteArray, size: Int) {
|
||||||
if (!started) return
|
if (!started) return
|
||||||
|
val encoder = aac
|
||||||
|
if (encoder == null) {
|
||||||
|
send(pcm.copyOf(size))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val frames = try {
|
||||||
|
encoder.encode(pcm, size)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// A codec that dies mid-drive leaves the car silent, as it was before AAC; log it once.
|
||||||
|
GearslipLog.e("$tag: AAC encoding failed - no more audio this drive", e)
|
||||||
|
aac = null
|
||||||
|
started = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
frames.forEach(::send)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One Data message, once the car has room for it. */
|
||||||
|
private fun send(payload: ByteArray) {
|
||||||
// Wait for the car to catch up rather than throw the chunk away: a dropped chunk is a gap
|
// Wait for the car to catch up rather than throw the chunk away: a dropped chunk is a gap
|
||||||
// the driver hears, while a short wait is soaked up by the capture buffer behind us.
|
// the driver hears, while a short wait is soaked up by the capture buffer behind us.
|
||||||
synchronized(ackLock) {
|
synchronized(ackLock) {
|
||||||
@@ -228,7 +263,7 @@ class AudioLink(
|
|||||||
val micros = System.nanoTime() / 1_000
|
val micros = System.nanoTime() / 1_000
|
||||||
for (i in 0 until 8) timestamp[i] = ((micros shr ((7 - i) * 8)) and 0xFF).toByte()
|
for (i in 0 until 8) timestamp[i] = ((micros shr ((7 - i) * 8)) and 0xFF).toByte()
|
||||||
try {
|
try {
|
||||||
sendOnChannel(MSG_DATA, timestamp + pcm.copyOf(size))
|
sendOnChannel(MSG_DATA, timestamp + payload)
|
||||||
} catch (e: java.io.IOException) {
|
} catch (e: java.io.IOException) {
|
||||||
GearslipLog.e("$tag: the link died mid-stream", e)
|
GearslipLog.e("$tag: the link died mid-stream", e)
|
||||||
started = false
|
started = false
|
||||||
@@ -236,7 +271,7 @@ class AudioLink(
|
|||||||
}
|
}
|
||||||
synchronized(ackLock) { inFlight++ }
|
synchronized(ackLock) { inFlight++ }
|
||||||
chunks++
|
chunks++
|
||||||
bytes += size
|
bytes += payload.size
|
||||||
// The first chunk proves the whole path; after that, an occasional line is enough to show
|
// The first chunk proves the whole path; after that, an occasional line is enough to show
|
||||||
// the car is still taking audio and to catch a window that has quietly closed.
|
// the car is still taking audio and to catch a window that has quietly closed.
|
||||||
if (chunks == 1 || chunks % CHUNK_LOG_EVERY == 0) {
|
if (chunks == 1 || chunks % CHUNK_LOG_EVERY == 0) {
|
||||||
@@ -272,6 +307,8 @@ class AudioLink(
|
|||||||
private const val FOCUS_STATE_GAIN = 1
|
private const val FOCUS_STATE_GAIN = 1
|
||||||
private const val FOCUS_STATE_GAIN_TRANSIENT = 2
|
private const val FOCUS_STATE_GAIN_TRANSIENT = 2
|
||||||
private const val FOCUS_STATE_GAIN_MEDIA_ONLY = 6
|
private const val FOCUS_STATE_GAIN_MEDIA_ONLY = 6
|
||||||
|
private const val FOCUS_STATE_LOSS = 3
|
||||||
|
private const val FOCUS_STATE_LOSS_TRANSIENT = 5
|
||||||
private const val SESSION_ID = 2
|
private const val SESSION_ID = 2
|
||||||
private const val STALL_MS = 2_000L
|
private const val STALL_MS = 2_000L
|
||||||
private const val CHUNK_LOG_EVERY = 200 // about every four seconds
|
private const val CHUNK_LOG_EVERY = 200 // about every four seconds
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ object CarServices {
|
|||||||
* headphones are, so the apps won't pause themselves. Every playing session is paused, not
|
* headphones are, so the apps won't pause themselves. Every playing session is paused, not
|
||||||
* just the one Gearslip opened: anything the car was playing was going through the car.
|
* just the one Gearslip opened: anything the car was playing was going through the car.
|
||||||
*/
|
*/
|
||||||
fun pausePhoneMedia() {
|
fun pausePhoneMedia(why: String = "the car left") {
|
||||||
val ctx = context ?: return
|
val ctx = context ?: return
|
||||||
val manager = ctx.getSystemService(MediaSessionManager::class.java)
|
val manager = ctx.getSystemService(MediaSessionManager::class.java)
|
||||||
val listener = ComponentName(ctx, GearslipNotificationListener::class.java)
|
val listener = ComponentName(ctx, GearslipNotificationListener::class.java)
|
||||||
@@ -237,7 +237,7 @@ object CarServices {
|
|||||||
}
|
}
|
||||||
playing.forEach { runCatching { it.transportControls.pause() } }
|
playing.forEach { runCatching { it.transportControls.pause() } }
|
||||||
if (playing.isNotEmpty()) {
|
if (playing.isNotEmpty()) {
|
||||||
app.seb3thehacker.gearslip.GearslipLog.i("media: paused ${playing.joinToString { it.packageName }} as the car left")
|
app.seb3thehacker.gearslip.GearslipLog.i("media: paused ${playing.joinToString { it.packageName }}: $why")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user