mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
car audio: explain a blocked microphone permission instead of failing silently
This commit is contained in:
1 parent
fff2db63b0
commit
4fe9421884
4 files changed
+74
-2
No files matched your search
@@ -8,8 +8,10 @@ import android.hardware.usb.UsbAccessory
|
|||||||
import android.hardware.usb.UsbManager
|
import android.hardware.usb.UsbManager
|
||||||
import android.media.projection.MediaProjection
|
import android.media.projection.MediaProjection
|
||||||
import android.media.projection.MediaProjectionManager
|
import android.media.projection.MediaProjectionManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.provider.Settings
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
@@ -74,7 +76,13 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
private val audioPermission = registerForActivityResult(
|
private val audioPermission = registerForActivityResult(
|
||||||
androidx.activity.result.contract.ActivityResultContracts.RequestPermission(),
|
androidx.activity.result.contract.ActivityResultContracts.RequestPermission(),
|
||||||
) { granted ->
|
) { granted ->
|
||||||
if (granted) askAudioCapture() else {
|
if (granted) askAudioCapture()
|
||||||
|
else if (!shouldShowRequestPermissionRationale(android.Manifest.permission.RECORD_AUDIO)) {
|
||||||
|
// Refused twice: Android now says no without showing anything, so the phone must.
|
||||||
|
GearslipLog.w("audio: record permission blocked - Android no longer asks")
|
||||||
|
CarAudio.micBlocked()
|
||||||
|
micBlockedNotice.value = true
|
||||||
|
} else {
|
||||||
GearslipLog.w("audio: record permission declined")
|
GearslipLog.w("audio: record permission declined")
|
||||||
CarAudio.denied()
|
CarAudio.denied()
|
||||||
}
|
}
|
||||||
@@ -91,6 +99,9 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Raised when the microphone permission is blocked; see [MicBlockedDialog]. */
|
||||||
|
private val micBlockedNotice = kotlinx.coroutines.flow.MutableStateFlow(false)
|
||||||
|
|
||||||
/** Raised once, after the first "no" to the capture dialog; see [AudioChoiceDialog]. */
|
/** Raised once, after the first "no" to the capture dialog; see [AudioChoiceDialog]. */
|
||||||
private val offerBluetooth = kotlinx.coroutines.flow.MutableStateFlow(false)
|
private val offerBluetooth = kotlinx.coroutines.flow.MutableStateFlow(false)
|
||||||
|
|
||||||
@@ -148,6 +159,18 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
onKeepAsking = ::close,
|
onKeepAsking = ::close,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val micNotice by micBlockedNotice.collectAsState()
|
||||||
|
if (micNotice) {
|
||||||
|
app.seb3thehacker.gearslip.ui.MicBlockedDialog(
|
||||||
|
onOpenSettings = {
|
||||||
|
micBlockedNotice.value = false
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", packageName, null)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDismiss = { micBlockedNotice.value = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +196,10 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
Prefetch.warm(this)
|
Prefetch.warm(this)
|
||||||
UsageStats.onAppOpened(this)
|
UsageStats.onAppOpened(this)
|
||||||
app.seb3thehacker.gearslip.update.UpdateChecker.onAppOpened(this)
|
app.seb3thehacker.gearslip.update.UpdateChecker.onAppOpened(this)
|
||||||
|
// Back from the app's settings with the microphone turned on: try car audio again.
|
||||||
|
if (checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||||
|
CarAudio.micAllowed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
|||||||
@@ -100,6 +100,26 @@ object CarAudio {
|
|||||||
_capture.value = Capture.DENIED
|
_capture.value = Capture.DENIED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _micBlocked = MutableStateFlow(false)
|
||||||
|
|
||||||
|
/** The microphone permission is off for good: Android refuses without asking, so say why. */
|
||||||
|
val micBlocked: StateFlow<Boolean> = _micBlocked.asStateFlow()
|
||||||
|
|
||||||
|
fun micBlocked() {
|
||||||
|
_micBlocked.value = true
|
||||||
|
denied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The driver turned the permission back on mid-drive: ask for capture again. */
|
||||||
|
fun micAllowed() {
|
||||||
|
if (!_micBlocked.value) return
|
||||||
|
_micBlocked.value = false
|
||||||
|
if (_capture.value == Capture.DENIED) {
|
||||||
|
_capture.value = Capture.OFF
|
||||||
|
if (wanted) request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun captureStarted() {
|
internal fun captureStarted() {
|
||||||
_capture.value = Capture.ON
|
_capture.value = Capture.ON
|
||||||
link?.start()
|
link?.start()
|
||||||
|
|||||||
@@ -449,10 +449,13 @@ private sealed interface LyricsState {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AudioBadge(capture: CarAudio.Capture) {
|
private fun AudioBadge(capture: CarAudio.Capture) {
|
||||||
|
val micBlocked by CarAudio.micBlocked.collectAsStateWithLifecycle()
|
||||||
val text = when (capture) {
|
val text = when (capture) {
|
||||||
CarAudio.Capture.ON -> "Audio to car"
|
CarAudio.Capture.ON -> "Audio to car"
|
||||||
CarAudio.Capture.ASKING -> "Allow audio capture on the phone"
|
CarAudio.Capture.ASKING -> "Allow audio capture on the phone"
|
||||||
CarAudio.Capture.DENIED -> "Audio stays on the phone"
|
CarAudio.Capture.DENIED ->
|
||||||
|
if (micBlocked) "Audio stays on the phone: turn on Gearslip's microphone permission"
|
||||||
|
else "Audio stays on the phone"
|
||||||
CarAudio.Capture.OFF -> return
|
CarAudio.Capture.OFF -> return
|
||||||
}
|
}
|
||||||
Text(text, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(text, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
|||||||
@@ -47,3 +47,25 @@ fun AudioChoiceDialog(onUseBluetooth: () -> Unit, onKeepAsking: () -> Unit) {
|
|||||||
dismissButton = { TextButton(onClick = onKeepAsking) { Text("Keep asking") } },
|
dismissButton = { TextButton(onClick = onKeepAsking) { Text("Keep asking") } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android lets an app capture other apps' audio only while it holds the microphone permission.
|
||||||
|
* Once the driver has refused that twice, Android stops asking, and music silently stays on the
|
||||||
|
* phone; this says why and where to turn it back on.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MicBlockedDialog(onOpenSettings: () -> Unit, onDismiss: () -> Unit) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("Car audio needs the microphone permission") },
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
"Android lets an app send other apps' sound to the car only if it has the " +
|
||||||
|
"microphone permission. It's turned off for Gearslip, so music plays on the " +
|
||||||
|
"phone. Turn it on under Permissions, then Microphone.",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = { TextButton(onClick = onOpenSettings) { Text("Open app settings") } },
|
||||||
|
dismissButton = { TextButton(onClick = onDismiss) { Text("Not now") } },
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user