feat(android): consolidate Settings into grouped card sections

Remove header bloat, merge Node info into a single Device card,
group permissions into Media/Notifications/Data Access cards with
internal dividers, and combine Screen+Debug into Preferences.
Sections reduced from 9 to 6.
This commit is contained in:
Ayaan Zaidi
2026-03-13 14:26:02 +05:30
parent 8b0e16a1c8
commit c04544891d

View File

@@ -345,179 +345,90 @@ fun SettingsSheet(viewModel: MainViewModel) {
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
item {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(
"SETTINGS",
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
color = mobileAccent,
)
Text("Device Configuration", style = mobileTitle2, color = mobileText)
Text(
"Manage capabilities, permissions, and diagnostics.",
style = mobileCallout,
color = mobileTextSecondary,
)
}
}
item { HorizontalDivider(color = mobileBorder) }
// Order parity: Node → Voice → Camera → Messaging → Location → Screen.
// ── Node ──
item {
Text(
"NODE",
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
color = mobileAccent,
)
}
item {
OutlinedTextField(
value = displayName,
onValueChange = viewModel::setDisplayName,
label = { Text("Name", style = mobileCaption1, color = mobileTextSecondary) },
modifier = Modifier.fillMaxWidth(),
textStyle = mobileBody.copy(color = mobileText),
colors = settingsTextFieldColors(),
)
}
item { Text("Instance ID: $instanceId", style = mobileCallout.copy(fontFamily = FontFamily.Monospace), color = mobileTextSecondary) }
item { Text("Device: $deviceModel", style = mobileCallout, color = mobileTextSecondary) }
item { Text("Version: $appVersion", style = mobileCallout, color = mobileTextSecondary) }
item { HorizontalDivider(color = mobileBorder) }
// Voice
item {
Text(
"VOICE",
"DEVICE",
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
color = mobileAccent,
)
}
item {
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Microphone permission", style = mobileHeadline) },
supportingContent = {
Column(modifier = Modifier.settingsRowModifier()) {
OutlinedTextField(
value = displayName,
onValueChange = viewModel::setDisplayName,
label = { Text("Name", style = mobileCaption1, color = mobileTextSecondary) },
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp),
textStyle = mobileBody.copy(color = mobileText),
colors = settingsTextFieldColors(),
)
HorizontalDivider(color = mobileBorder)
Column(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text("$deviceModel · $appVersion", style = mobileCallout, color = mobileTextSecondary)
Text(
if (micPermissionGranted) {
"Granted. Use the Voice tab mic button to capture transcript while the app is open."
} else {
"Required for foreground Voice tab transcription."
},
style = mobileCallout,
instanceId.take(8) + "",
style = mobileCaption1.copy(fontFamily = FontFamily.Monospace),
color = mobileTextTertiary,
)
},
trailingContent = {
Button(
onClick = {
if (micPermissionGranted) {
openAppSettings(context)
} else {
audioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (micPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
}
item {
Text(
"Voice wake and talk modes were removed. Voice now uses one mic on/off flow in the Voice tab while the app is open.",
style = mobileCallout,
color = mobileTextSecondary,
)
}
item { HorizontalDivider(color = mobileBorder) }
// Camera
item {
Text(
"CAMERA",
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
color = mobileAccent,
)
}
item {
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Allow Camera", style = mobileHeadline) },
supportingContent = { Text("Allows the gateway to request photos or short video clips (foreground only).", style = mobileCallout) },
trailingContent = { Switch(checked = cameraEnabled, onCheckedChange = ::setCameraEnabledChecked) },
)
}
item {
Text(
"Tip: grant Microphone permission for video clips with audio.",
style = mobileCallout,
color = mobileTextSecondary,
)
}
item { HorizontalDivider(color = mobileBorder) }
// Messaging
item {
Text(
"MESSAGING",
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
color = mobileAccent,
)
}
item {
val buttonLabel =
when {
!smsPermissionAvailable -> "Unavailable"
smsPermissionGranted -> "Manage"
else -> "Grant"
}
}
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("SMS Permission", style = mobileHeadline) },
supportingContent = {
Text(
if (smsPermissionAvailable) {
"Allow the gateway to send SMS from this device."
} else {
"SMS requires a device with telephony hardware."
}
// ── Media ──
item {
Text(
"MEDIA",
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
color = mobileAccent,
)
}
item {
Column(modifier = Modifier.settingsRowModifier()) {
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Microphone", style = mobileHeadline) },
supportingContent = {
Text(
if (micPermissionGranted) "Granted" else "Required for voice transcription.",
style = mobileCallout,
)
},
style = mobileCallout,
)
},
trailingContent = {
Button(
onClick = {
if (!smsPermissionAvailable) return@Button
if (smsPermissionGranted) {
openAppSettings(context)
} else {
smsPermissionLauncher.launch(Manifest.permission.SEND_SMS)
trailingContent = {
Button(
onClick = {
if (micPermissionGranted) {
openAppSettings(context)
} else {
audioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (micPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
enabled = smsPermissionAvailable,
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(buttonLabel, style = mobileCallout.copy(fontWeight = FontWeight.Bold))
}
},
)
}
)
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Camera", style = mobileHeadline) },
supportingContent = { Text("Photos and video clips (foreground only).", style = mobileCallout) },
trailingContent = { Switch(checked = cameraEnabled, onCheckedChange = ::setCameraEnabledChecked) },
)
}
}
item { HorizontalDivider(color = mobileBorder) }
// Notifications
// ── Notifications & Messaging ──
item {
Text(
"NOTIFICATIONS",
@@ -526,67 +437,87 @@ fun SettingsSheet(viewModel: MainViewModel) {
)
}
item {
val buttonLabel =
if (notificationsPermissionGranted) {
"Manage"
} else {
"Grant"
}
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("System Notifications", style = mobileHeadline) },
supportingContent = {
Text(
"Required for `system.notify` and Android foreground service alerts.",
style = mobileCallout,
)
},
trailingContent = {
Button(
onClick = {
if (notificationsPermissionGranted || Build.VERSION.SDK_INT < 33) {
openAppSettings(context)
} else {
notificationsPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
Column(modifier = Modifier.settingsRowModifier()) {
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("System Notifications", style = mobileHeadline) },
supportingContent = {
Text("Alerts and foreground service.", style = mobileCallout)
},
trailingContent = {
Button(
onClick = {
if (notificationsPermissionGranted || Build.VERSION.SDK_INT < 33) {
openAppSettings(context)
} else {
notificationsPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (notificationsPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Notification Listener", style = mobileHeadline) },
supportingContent = {
Text("Read and interact with notifications.", style = mobileCallout)
},
trailingContent = {
Button(
onClick = { openNotificationListenerSettings(context) },
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (notificationListenerEnabled) "Manage" else "Enable",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
if (smsPermissionAvailable) {
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("SMS", style = mobileHeadline) },
supportingContent = {
Text("Send SMS from this device.", style = mobileCallout)
},
trailingContent = {
Button(
onClick = {
if (smsPermissionGranted) {
openAppSettings(context)
} else {
smsPermissionLauncher.launch(Manifest.permission.SEND_SMS)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (smsPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(buttonLabel, style = mobileCallout.copy(fontWeight = FontWeight.Bold))
}
},
)
}
item {
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Notification Listener Access", style = mobileHeadline) },
supportingContent = {
Text(
"Required for `notifications.list` and `notifications.actions`.",
style = mobileCallout,
)
},
trailingContent = {
Button(
onClick = { openNotificationListenerSettings(context) },
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (notificationListenerEnabled) "Manage" else "Enable",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
}
}
}
item { HorizontalDivider(color = mobileBorder) }
// Data access
// ── Data Access ──
item {
Text(
"DATA ACCESS",
@@ -595,142 +526,115 @@ fun SettingsSheet(viewModel: MainViewModel) {
)
}
item {
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Photos Permission", style = mobileHeadline) },
supportingContent = {
Text(
"Required for `photos.latest`.",
style = mobileCallout,
)
},
trailingContent = {
Button(
onClick = {
if (photosPermissionGranted) {
openAppSettings(context)
} else {
photosPermissionLauncher.launch(photosPermission)
Column(modifier = Modifier.settingsRowModifier()) {
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Photos", style = mobileHeadline) },
supportingContent = { Text("Access recent photos.", style = mobileCallout) },
trailingContent = {
Button(
onClick = {
if (photosPermissionGranted) {
openAppSettings(context)
} else {
photosPermissionLauncher.launch(photosPermission)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (photosPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Contacts", style = mobileHeadline) },
supportingContent = { Text("Search and add contacts.", style = mobileCallout) },
trailingContent = {
Button(
onClick = {
if (contactsPermissionGranted) {
openAppSettings(context)
} else {
contactsPermissionLauncher.launch(arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS))
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (contactsPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Calendar", style = mobileHeadline) },
supportingContent = { Text("Read and create events.", style = mobileCallout) },
trailingContent = {
Button(
onClick = {
if (calendarPermissionGranted) {
openAppSettings(context)
} else {
calendarPermissionLauncher.launch(arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR))
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (calendarPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
if (motionAvailable) {
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Motion", style = mobileHeadline) },
supportingContent = { Text("Track steps and activity.", style = mobileCallout) },
trailingContent = {
val motionButtonLabel =
when {
!motionPermissionRequired -> "Manage"
motionPermissionGranted -> "Manage"
else -> "Grant"
}
Button(
onClick = {
if (!motionPermissionRequired || motionPermissionGranted) {
openAppSettings(context)
} else {
motionPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(motionButtonLabel, style = mobileCallout.copy(fontWeight = FontWeight.Bold))
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (photosPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
}
item {
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Contacts Permission", style = mobileHeadline) },
supportingContent = {
Text(
"Required for `contacts.search` and `contacts.add`.",
style = mobileCallout,
)
},
trailingContent = {
Button(
onClick = {
if (contactsPermissionGranted) {
openAppSettings(context)
} else {
contactsPermissionLauncher.launch(arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS))
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (contactsPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
}
item {
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Calendar Permission", style = mobileHeadline) },
supportingContent = {
Text(
"Required for `calendar.events` and `calendar.add`.",
style = mobileCallout,
)
},
trailingContent = {
Button(
onClick = {
if (calendarPermissionGranted) {
openAppSettings(context)
} else {
calendarPermissionLauncher.launch(arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR))
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (calendarPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
}
item {
val motionButtonLabel =
when {
!motionAvailable -> "Unavailable"
!motionPermissionRequired -> "Manage"
motionPermissionGranted -> "Manage"
else -> "Grant"
}
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Motion Permission", style = mobileHeadline) },
supportingContent = {
Text(
if (!motionAvailable) {
"This device does not expose accelerometer or step-counter motion sensors."
} else {
"Required for `motion.activity` and `motion.pedometer`."
},
style = mobileCallout,
)
},
trailingContent = {
Button(
onClick = {
if (!motionAvailable) return@Button
if (!motionPermissionRequired || motionPermissionGranted) {
openAppSettings(context)
} else {
motionPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION)
}
},
enabled = motionAvailable,
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(motionButtonLabel, style = mobileCallout.copy(fontWeight = FontWeight.Bold))
}
},
)
}
}
item { HorizontalDivider(color = mobileBorder) }
// Location
// ── Location ──
item {
Text(
"LOCATION",
@@ -739,7 +643,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
)
}
item {
Column(modifier = Modifier.settingsRowModifier(), verticalArrangement = Arrangement.spacedBy(0.dp)) {
Column(modifier = Modifier.settingsRowModifier()) {
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
@@ -781,50 +685,39 @@ fun SettingsSheet(viewModel: MainViewModel) {
)
}
}
item { HorizontalDivider(color = mobileBorder) }
// Screen
// ── Preferences ──
item {
Text(
"SCREEN",
"PREFERENCES",
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
color = mobileAccent,
)
}
item {
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Prevent Sleep", style = mobileHeadline) },
supportingContent = { Text("Keeps the screen awake while OpenClaw is open.", style = mobileCallout) },
trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) },
)
}
item { HorizontalDivider(color = mobileBorder) }
// Debug
item {
Text(
"DEBUG",
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
color = mobileAccent,
)
}
item {
ListItem(
modifier = Modifier.settingsRowModifier(),
colors = listItemColors,
headlineContent = { Text("Debug Canvas Status", style = mobileHeadline) },
supportingContent = { Text("Show status text in the canvas when debug is enabled.", style = mobileCallout) },
trailingContent = {
Switch(
checked = canvasDebugStatusEnabled,
onCheckedChange = viewModel::setCanvasDebugStatusEnabled,
Column(modifier = Modifier.settingsRowModifier()) {
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Prevent Sleep", style = mobileHeadline) },
supportingContent = { Text("Keep screen awake while open.", style = mobileCallout) },
trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) },
)
},
)
}
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Debug Canvas", style = mobileHeadline) },
supportingContent = { Text("Show status overlay on canvas.", style = mobileCallout) },
trailingContent = {
Switch(
checked = canvasDebugStatusEnabled,
onCheckedChange = viewModel::setCanvasDebugStatusEnabled,
)
},
)
}
}
item { Spacer(modifier = Modifier.height(24.dp)) }
}