fix(android): preserve location opt-in during onboarding (#129961)

This commit is contained in:
Peter Steinberger
2026-08-26 01:05:36 -07:00
committed by GitHub
parent ccac43e3cd
commit d9506b2000
2 changed files with 69 additions and 30 deletions
@@ -3104,25 +3104,25 @@ internal fun canFinishOnboarding(
private val requiredContactPermissions = listOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)
private val requiredCalendarPermissions = listOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)
internal fun initialCameraCapabilityEnabled(
internal fun initialDeviceCapabilityEnabled(
savedCapabilityEnabled: Boolean,
androidCameraPermissionGranted: Boolean,
): Boolean = savedCapabilityEnabled && androidCameraPermissionGranted
androidPermissionGranted: Boolean,
): Boolean = savedCapabilityEnabled && androidPermissionGranted
internal fun cameraPermissionRowStatusText(
internal fun deviceCapabilityRowStatusText(
capabilityEnabled: Boolean,
androidCameraPermissionGranted: Boolean,
androidPermissionGranted: Boolean,
): NativeText =
when {
capabilityEnabled -> nativeText("Enabled")
androidCameraPermissionGranted -> nativeText("Off")
androidPermissionGranted -> nativeText("Off")
else -> nativeText("Not allowed")
}
internal fun cameraCapabilityAfterRowTap(
internal fun deviceCapabilityAfterRowTap(
currentCapabilityEnabled: Boolean,
androidCameraPermissionGranted: Boolean,
): Boolean? = if (androidCameraPermissionGranted) !currentCapabilityEnabled else null
androidPermissionGranted: Boolean,
): Boolean? = if (androidPermissionGranted) !currentCapabilityEnabled else null
private fun permissionRowStatusText(granted: Boolean): NativeText = if (granted) nativeText("Granted") else nativeText("Not granted")
@@ -3148,9 +3148,15 @@ private fun rememberPermissionState(
val currentLocationMode by viewModel.locationMode.collectAsState()
var microphoneGranted by rememberSaveable { mutableStateOf(hasPermission(context, Manifest.permission.RECORD_AUDIO)) }
val cameraPermissionGranted = hasPermission(context, Manifest.permission.CAMERA)
var cameraGranted by rememberSaveable { mutableStateOf(initialCameraCapabilityEnabled(currentCameraEnabled, cameraPermissionGranted)) }
var cameraGranted by rememberSaveable { mutableStateOf(initialDeviceCapabilityEnabled(currentCameraEnabled, cameraPermissionGranted)) }
fun hasLocationPermission(): Boolean =
hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ||
hasPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
val locationPermissionGranted = hasLocationPermission()
var locationGranted by rememberSaveable {
mutableStateOf(hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) || hasPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION))
mutableStateOf(initialDeviceCapabilityEnabled(currentLocationMode != LocationMode.Off, locationPermissionGranted))
}
val photosPermissions = photoReadPermissionsForRequest()
var photosGranted by rememberSaveable { mutableStateOf(hasPhotoReadPermission(context)) }
@@ -3238,9 +3244,9 @@ private fun rememberPermissionState(
fun requestCameraCapability() {
val nextCapabilityEnabled =
cameraCapabilityAfterRowTap(
deviceCapabilityAfterRowTap(
currentCapabilityEnabled = cameraGranted,
androidCameraPermissionGranted = hasPermission(context, Manifest.permission.CAMERA),
androidPermissionGranted = hasPermission(context, Manifest.permission.CAMERA),
)
if (nextCapabilityEnabled != null) {
cameraGranted = nextCapabilityEnabled
@@ -3260,14 +3266,26 @@ private fun rememberPermissionState(
nativeText("Capture photos and clips from this phone"),
Icons.Default.CameraAlt,
cameraGranted,
cameraPermissionRowStatusText(
deviceCapabilityRowStatusText(
capabilityEnabled = cameraGranted,
androidCameraPermissionGranted = hasPermission(context, Manifest.permission.CAMERA),
androidPermissionGranted = cameraPermissionGranted,
),
::requestCameraCapability,
),
PermissionRowModel(PermissionRowId.Location, nativeText("Location"), nativeText("Read this phone's location"), Icons.Default.LocationOn, locationGranted) {
request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
PermissionRowModel(
PermissionRowId.Location,
nativeText("Location"),
nativeText("Read this phone's location"),
Icons.Default.LocationOn,
locationGranted,
deviceCapabilityRowStatusText(locationGranted, locationPermissionGranted),
) {
val nextCapabilityEnabled = deviceCapabilityAfterRowTap(locationGranted, hasLocationPermission())
if (nextCapabilityEnabled != null) {
locationGranted = nextCapabilityEnabled
} else {
request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
}
},
if (photosAvailable) {
PermissionRowModel(PermissionRowId.Photos, nativeText("Photos"), nativeText("Read recent photos and media"), Icons.Default.Image, photosGranted) {
@@ -180,21 +180,42 @@ class OnboardingFlowLogicTest {
}
@Test
fun cameraCapabilityStartsOffEvenWhenScannerPermissionWasGranted() {
fun deviceCapabilityStartsOffEvenWhenAndroidPermissionWasGranted() {
assertBooleanCases(
false to initialCameraCapabilityEnabled(savedCapabilityEnabled = false, androidCameraPermissionGranted = false),
false to initialCameraCapabilityEnabled(savedCapabilityEnabled = false, androidCameraPermissionGranted = true),
false to initialCameraCapabilityEnabled(savedCapabilityEnabled = true, androidCameraPermissionGranted = false),
true to initialCameraCapabilityEnabled(savedCapabilityEnabled = true, androidCameraPermissionGranted = true),
false to initialDeviceCapabilityEnabled(savedCapabilityEnabled = false, androidPermissionGranted = false),
false to initialDeviceCapabilityEnabled(savedCapabilityEnabled = false, androidPermissionGranted = true),
false to initialDeviceCapabilityEnabled(savedCapabilityEnabled = true, androidPermissionGranted = false),
true to initialDeviceCapabilityEnabled(savedCapabilityEnabled = true, androidPermissionGranted = true),
)
}
@Test
fun cameraPermissionRowDistinguishesAndroidPermissionFromCapabilityOptIn() {
fun locationCapabilityRequiresBothTheSavedModeAndAndroidPermission() {
listOf(
Triple(LocationMode.Off, false, false),
Triple(LocationMode.Off, true, false),
Triple(LocationMode.WhileUsing, false, false),
Triple(LocationMode.WhileUsing, true, true),
Triple(LocationMode.Always, false, false),
Triple(LocationMode.Always, true, true),
).forEach { (savedMode, androidPermissionGranted, expected) ->
assertEquals(
"savedMode=$savedMode androidPermissionGranted=$androidPermissionGranted",
expected,
initialDeviceCapabilityEnabled(
savedCapabilityEnabled = savedMode != LocationMode.Off,
androidPermissionGranted = androidPermissionGranted,
),
)
}
}
@Test
fun deviceCapabilityRowDistinguishesAndroidPermissionFromCapabilityOptIn() {
assertEqualsCases(
"Not allowed" to cameraPermissionRowStatusText(capabilityEnabled = false, androidCameraPermissionGranted = false).resolveNativeText(),
"Off" to cameraPermissionRowStatusText(capabilityEnabled = false, androidCameraPermissionGranted = true).resolveNativeText(),
"Enabled" to cameraPermissionRowStatusText(capabilityEnabled = true, androidCameraPermissionGranted = true).resolveNativeText(),
"Not allowed" to deviceCapabilityRowStatusText(capabilityEnabled = false, androidPermissionGranted = false).resolveNativeText(),
"Off" to deviceCapabilityRowStatusText(capabilityEnabled = false, androidPermissionGranted = true).resolveNativeText(),
"Enabled" to deviceCapabilityRowStatusText(capabilityEnabled = true, androidPermissionGranted = true).resolveNativeText(),
)
}
@@ -207,10 +228,10 @@ class OnboardingFlowLogicTest {
}
@Test
fun cameraPermissionRowTogglesCapabilityWhenAndroidPermissionAlreadyGranted() {
assertNull(cameraCapabilityAfterRowTap(currentCapabilityEnabled = false, androidCameraPermissionGranted = false))
assertTrue(cameraCapabilityAfterRowTap(currentCapabilityEnabled = false, androidCameraPermissionGranted = true)!!)
assertFalse(cameraCapabilityAfterRowTap(currentCapabilityEnabled = true, androidCameraPermissionGranted = true)!!)
fun deviceCapabilityRowTogglesOnlyWhenAndroidPermissionAlreadyGranted() {
assertNull(deviceCapabilityAfterRowTap(currentCapabilityEnabled = false, androidPermissionGranted = false))
assertTrue(deviceCapabilityAfterRowTap(currentCapabilityEnabled = false, androidPermissionGranted = true)!!)
assertFalse(deviceCapabilityAfterRowTap(currentCapabilityEnabled = true, androidPermissionGranted = true)!!)
}
@Test