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.media.projection.MediaProjection
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.Settings
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.WindowManager
|
||||
@@ -74,7 +76,13 @@ class GearslipActivity : ComponentActivity(), Projection {
|
||||
private val audioPermission = registerForActivityResult(
|
||||
androidx.activity.result.contract.ActivityResultContracts.RequestPermission(),
|
||||
) { 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")
|
||||
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]. */
|
||||
private val offerBluetooth = kotlinx.coroutines.flow.MutableStateFlow(false)
|
||||
|
||||
@@ -148,6 +159,18 @@ class GearslipActivity : ComponentActivity(), Projection {
|
||||
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)
|
||||
UsageStats.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) {
|
||||
|
||||
@@ -100,6 +100,26 @@ object CarAudio {
|
||||
_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() {
|
||||
_capture.value = Capture.ON
|
||||
link?.start()
|
||||
|
||||
@@ -449,10 +449,13 @@ private sealed interface LyricsState {
|
||||
|
||||
@Composable
|
||||
private fun AudioBadge(capture: CarAudio.Capture) {
|
||||
val micBlocked by CarAudio.micBlocked.collectAsStateWithLifecycle()
|
||||
val text = when (capture) {
|
||||
CarAudio.Capture.ON -> "Audio to car"
|
||||
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
|
||||
}
|
||||
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") } },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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