diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt index 644f488d43e2..edfc1a669d5e 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt @@ -1370,6 +1370,9 @@ internal fun canFinishOnboarding( -> true } +private val requiredContactPermissions = listOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS) +private val requiredCalendarPermissions = listOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR) + /** Builds permission rows and applies granted feature toggles after onboarding. */ @Composable private fun rememberPermissionState( @@ -1383,8 +1386,12 @@ private fun rememberPermissionState( } val photosPermissions = photoReadPermissionsForRequest() var photosGranted by rememberSaveable { mutableStateOf(hasPhotoReadPermission(context)) } - var contactsGranted by rememberSaveable { mutableStateOf(hasPermission(context, Manifest.permission.READ_CONTACTS)) } - var calendarGranted by rememberSaveable { mutableStateOf(hasPermission(context, Manifest.permission.READ_CALENDAR)) } + var contactsGranted by rememberSaveable { + mutableStateOf(requiredContactPermissions.all { permission -> hasPermission(context, permission) }) + } + var calendarGranted by rememberSaveable { + mutableStateOf(requiredCalendarPermissions.all { permission -> hasPermission(context, permission) }) + } var notificationsGranted by rememberSaveable { mutableStateOf(Build.VERSION.SDK_INT < 33 || hasPermission(context, Manifest.permission.POST_NOTIFICATIONS)) } @@ -1430,8 +1437,18 @@ private fun rememberPermissionState( permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true || locationGranted photosGranted = hasPhotoReadPermission(context) || photosPermissions.any { permissions[it] == true } - contactsGranted = permissions[Manifest.permission.READ_CONTACTS] ?: contactsGranted - calendarGranted = permissions[Manifest.permission.READ_CALENDAR] ?: calendarGranted + contactsGranted = + mergedRequiredPermissionGrantState( + permissions = permissions, + requiredPermissions = requiredContactPermissions, + currentlyGranted = { permission -> hasPermission(context, permission) }, + ) + calendarGranted = + mergedRequiredPermissionGrantState( + permissions = permissions, + requiredPermissions = requiredCalendarPermissions, + currentlyGranted = { permission -> hasPermission(context, permission) }, + ) notificationsGranted = if (Build.VERSION.SDK_INT >= 33) { permissions[Manifest.permission.POST_NOTIFICATIONS] ?: notificationsGranted @@ -1471,10 +1488,10 @@ private fun rememberPermissionState( null }, PermissionRowModel("Contacts", "Read contacts securely", Icons.Default.Person, contactsGranted) { - request(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS) + request(*requiredContactPermissions.toTypedArray()) }, PermissionRowModel("Calendar", "Read events and schedules", Icons.Default.CalendarMonth, calendarGranted) { - request(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR) + request(*requiredCalendarPermissions.toTypedArray()) }, PermissionRowModel("Notifications", "Send important alerts", Icons.Default.Notifications, notificationsGranted) { if (Build.VERSION.SDK_INT >= 33) request(Manifest.permission.POST_NOTIFICATIONS) diff --git a/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt index a321ec6dfc38..be3aa1681ea8 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt @@ -88,6 +88,34 @@ class OnboardingFlowLogicTest { assertFalse(deniedRead) } + @Test + fun contactAndCalendarPermissionGroupsRequireBothGrants() { + val permissionGroups = + listOf( + listOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), + listOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR), + ) + + for (requiredPermissions in permissionGroups) { + val readPermission = requiredPermissions.first() + val writePermission = requiredPermissions.last() + assertFalse( + mergedRequiredPermissionGrantState( + permissions = mapOf(readPermission to true), + requiredPermissions = requiredPermissions, + currentlyGranted = { false }, + ), + ) + assertTrue( + mergedRequiredPermissionGrantState( + permissions = mapOf(writePermission to true), + requiredPermissions = requiredPermissions, + currentlyGranted = { permission -> permission == readPermission }, + ), + ) + } + } + @Test fun nearbyGatewayFoundStateIsConnectable() { assertEquals(