phone: ask for call permissions on the phone from the car's Phone screen

This commit is contained in:
Seb3thehacker committed 2026-10-24 14:24:28 -07:00
1 parent e2e0d3f8bb
commit b57bfb5324
4 files changed
+76 -9

No files matched your search

+6
View File
@@ -162,6 +162,12 @@
activity so Settings can switch off just this (AppSettings.setGearslipEnabled) and leave activity so Settings can switch off just this (AppSettings.setGearslipEnabled) and leave
the head unit to Android Auto, without disabling the screen the switch is on. the head unit to Android Auto, without disabling the screen the switch is on.
--> -->
<activity
android:name=".PermissionActivity"
android:exported="false"
android:excludeFromRecents="true"
android:theme="@style/Theme.Gearslip.Invisible" />
<activity-alias <activity-alias
android:name=".GearslipUsbAccessory" android:name=".GearslipUsbAccessory"
android:targetActivity=".GearslipActivity" android:targetActivity=".GearslipActivity"
@@ -0,0 +1,49 @@
package app.seb3thehacker.gearslip
import android.app.Activity
import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Display
/**
* Asks for runtime permissions on the phone's own screen, for a car screen that can't show
* Android's dialog itself. It has no look of its own: the dialog appears, the answer is given,
* and it finishes.
*/
class PermissionActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val wanted = intent.getStringArrayExtra(EXTRA_PERMISSIONS)
?.filter { checkSelfPermission(it) != android.content.pm.PackageManager.PERMISSION_GRANTED }
.orEmpty()
if (wanted.isEmpty() || savedInstanceState != null) {
if (wanted.isEmpty()) finish()
return
}
requestPermissions(wanted.toTypedArray(), REQUEST)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
GearslipLog.i("permissions: ${permissions.zip(grantResults.toList()).joinToString { (p, r) -> "${p.substringAfterLast('.')}=${r == 0}" }}")
finish()
}
companion object {
private const val EXTRA_PERMISSIONS = "permissions"
private const val REQUEST = 1
/** Shows the permission dialog on the phone, whichever screen [context] belongs to. */
fun ask(context: Context, permissions: Array<String>) {
val intent = Intent(context, PermissionActivity::class.java)
.putExtra(EXTRA_PERMISSIONS, permissions)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val options = ActivityOptions.makeBasic().setLaunchDisplayId(Display.DEFAULT_DISPLAY)
runCatching { context.startActivity(intent, options.toBundle()) }
.onFailure { GearslipLog.w("permissions: could not ask on the phone: ${it.message}") }
}
}
}
@@ -133,7 +133,7 @@ private const val TAB_KEYPAD = 1
@Composable @Composable
fun PhoneDialerScreen() { fun PhoneDialerScreen() {
val context = LocalContext.current val context = LocalContext.current
val hasPermissions = hasCallPermissions(context) var hasPermissions by remember { mutableStateOf(hasCallPermissions(context)) }
var tab by remember { mutableStateOf(TAB_CONTACTS) } var tab by remember { mutableStateOf(TAB_CONTACTS) }
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -148,7 +148,7 @@ fun PhoneDialerScreen() {
} }
if (!hasPermissions) { if (!hasPermissions) {
MissingPermission(context) MissingPermission(context) { hasPermissions = true }
return@Column return@Column
} }
@@ -159,19 +159,23 @@ fun PhoneDialerScreen() {
} }
@Composable @Composable
private fun MissingPermission(context: Context) { private fun MissingPermission(context: Context, onGranted: () -> Unit) {
// Android's dialog can't show on the car screen, so it opens on the phone; checking again
// every second picks the answer up without the driver having to come back in.
androidx.compose.runtime.LaunchedEffect(Unit) {
while (!hasCallPermissions(context)) kotlinx.coroutines.delay(1_000)
onGranted()
}
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text( Text(
"Gearslip needs the Phone and Contacts permissions to dial from here. Grant them " + "Gearslip needs the Phone and Contacts permissions to dial from here. Allow them on " +
"from the app's settings.", "the phone when you're parked.",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
GsButton(tone = GsTone.Tonal, onClick = { GsButton(tone = GsTone.Tonal, onClick = {
context.startActivity( app.seb3thehacker.gearslip.PermissionActivity.ask(context, CALL_PERMISSIONS)
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null)), }) { Text("Ask on the phone") }
)
}) { Text("Open app settings") }
} }
} }
+8
View File
@@ -9,4 +9,12 @@
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="windowActionBar">false</item> <item name="windowActionBar">false</item>
</style> </style>
<!-- For PermissionActivity: only Android's own permission dialog shows, over whatever was there. -->
<style name="Theme.Gearslip.Invisible" parent="Theme.Gearslip">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources> </resources>