Conversation
Adds an optional location permission request step to the onboarding flow and introduces a "Save Location" toggle switch in the in-app Settings screen.
Key changes:
- In `:feature:permissions`:
- Adds `PermissionEnum.LOCATION` as an optional permission card.
- Implements `PermissionsRepository` to track dismissed/requested permissions across sessions.
- Adds `PermissionsScreenComponents` for location permission rationale dialog and settings deep-linking.
- When granted during onboarding, updates `SettingsRepository.updateLocationEnabled(true)` automatically.
- In `:feature:settings`:
- Adds `LocationSettingComponent` ("Save Location" toggle) to the Settings screen.
- Adds `LocationUiState` (Hidden when no LocationProvider is available, Enabled.On/Off when present).
- Handles runtime permission requesting when toggled on, and displays a rationale dialog directing users to system app settings if permanently denied.
- In `:app`:
- Binds `PermissionsRepositoryImpl` via `PermissionsModule`.
- Passes `onOpenAppSettings` from `MainActivity` / `JcaApp` to `SettingsScreen`.
- Adds unit and Compose UI tests for onboarding and settings screens and ViewModels.
There was a problem hiding this comment.
Code Review
This pull request introduces location geotagging support, including a new optional location permission onboarding flow and a "Save Location" toggle in the settings screen. The implementation uses a DataStore-backed repository to persist permission request states and includes comprehensive unit and UI tests. The feedback focuses on adhering to the repository's style guide regarding test tag naming conventions, adding KDoc to internal composables, removing a debug log, and optimizing the permission filtering logic to better handle composite permissions.
| const val RECORD_AUDIO_PERMISSION_BUTTON = "RecordAudioPermissionButton" | ||
|
|
||
| const val WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON = "WriteExternalStoragePermissionButton" | ||
| const val LOCATION_PERMISSION_BUTTON = "LocationPermissionButton" |
There was a problem hiding this comment.
According to the repository style guide (lines 90-96), new test tags must have a string value that is a lower_snake_case string following the schema element_purpose_value. The value "LocationPermissionButton" violates this rule. Please update the string value to follow the lower_snake_case naming convention (e.g., "btn_location_permission").
References
- Test Tag Naming Convention: When creating a new test tag, the constant name must be in UPPER_SNAKE_CASE, and the string value must be a lower_snake_case string following a specific schema: element_purpose_value. (link)
| const val FPS_UNSUPPORTED_TAG = "FpsUnsupportedTag" | ||
| const val VIDEO_QUALITY_UNSUPPORTED_TAG = "VideoQualityUnsupportedTag" | ||
| const val PERMISSION_RECORD_AUDIO_NOT_GRANTED_TAG = "PermissionRecordAudioNotGrantedTag" | ||
| const val PERMISSION_LOCATION_NOT_GRANTED_TAG = "PermissionLocationNotGrantedTag" |
There was a problem hiding this comment.
According to the repository style guide (lines 90-96), new test tags must have a string value that is a lower_snake_case string following the schema element_purpose_value. The value "PermissionLocationNotGrantedTag" violates this rule. Please update the string value to follow the lower_snake_case naming convention (e.g., "text_permission_location_not_granted").
References
- Test Tag Naming Convention: When creating a new test tag, the constant name must be in UPPER_SNAKE_CASE, and the string value must be a lower_snake_case string following a specific schema: element_purpose_value. (link)
| @OptIn(ExperimentalPermissionsApi::class) | ||
| @Composable | ||
| internal fun PermissionTemplate( | ||
| permissionEnum: PermissionEnum, | ||
| permissionStates: MultiplePermissionsState, | ||
| onDismissPermission: () -> Unit, | ||
| onOpenAppSettings: () -> Unit, | ||
| modifier: Modifier = Modifier | ||
| ) { |
There was a problem hiding this comment.
According to the repository style guide (lines 82-86), all non-private classes, functions, and composables must have KDoc documentation. Please add KDoc for the internal PermissionTemplate composable.
/**
* A template for displaying a permission request screen.
*
* @param permissionEnum The permission being requested.
* @param permissionStates The state of the permissions being requested.
* @param onDismissPermission Callback when the permission request is dismissed or skipped.
* @param onOpenAppSettings Callback to open the system app settings.
* @param modifier The modifier to be applied to the layout.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
internal fun PermissionTemplate(
permissionEnum: PermissionEnum,
permissionStates: MultiplePermissionsState,
onDismissPermission: () -> Unit,
onOpenAppSettings: () -> Unit,
modifier: Modifier = Modifier
) {References
- Document all non-private members: All non-private classes, functions, and composables must have KDoc documentation. (link)
| internal fun setLocationEnabled(enabled: Boolean) { | ||
| viewModelScope.launch { | ||
| settingsRepository.updateLocationEnabled(enabled) | ||
| Log.d(TAG, "set save location enabled: $enabled") | ||
| } | ||
| } |
There was a problem hiding this comment.
According to the repository style guide (lines 76-80), debug logs should be scrutinized and removed before merging unless they provide essential, long-term value. The log Log.d(TAG, "set save location enabled: $enabled") appears to be a remnant of debugging and should be removed.
internal fun setLocationEnabled(enabled: Boolean) {
viewModelScope.launch {
settingsRepository.updateLocationEnabled(enabled)
}
}References
- Scrutinize Debug Logs: Question the use of Log.d, Log.v, and especially println(). These are often remnants of debugging and should be removed before merging unless they provide essential, long-term value. (link)
| fun getRequestablePermissions( | ||
| permissionStates: MultiplePermissionsState, | ||
| requestedPermissions: Set<String> = emptySet() | ||
| ): List<PermissionEnum> = buildSet { | ||
| for (permissionState in permissionStates.permissions) { | ||
| val permission = PermissionEnum.fromString(permissionState.permission) | ||
| val componentStates = permission.getPermissions().mapNotNull { permStr -> | ||
| permissionStates.permissions.firstOrNull { it.permission == permStr } | ||
| } | ||
| val isAnyGranted = componentStates.any { it.status.isGranted } | ||
| if (!isAnyGranted) { | ||
| if (!permission.isOptional()) { | ||
| add(permission) | ||
| } else { | ||
| val wasPreviouslyHandled = | ||
| permission.name in requestedPermissions || | ||
| componentStates.any { it.status.shouldShowRationale } | ||
| if (!wasPreviouslyHandled) { | ||
| add(permission) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }.toList() |
There was a problem hiding this comment.
The current implementation of getRequestablePermissions iterates over each individual system permission in permissionStates.permissions. For composite permissions like LOCATION (which contains both fine and coarse location), this results in redundant lookups and processing. We can optimize this and make it more robust by iterating over PermissionEnum.entries instead. This also avoids the need for buildSet and .toList(), as each PermissionEnum is processed exactly once.
fun getRequestablePermissions(
permissionStates: MultiplePermissionsState,
requestedPermissions: Set<String> = emptySet()
): List<PermissionEnum> = buildList {
for (permission in PermissionEnum.entries) {
val componentStates = permission.getPermissions().mapNotNull { permStr ->
permissionStates.permissions.firstOrNull { it.permission == permStr }
}
if (componentStates.isEmpty()) continue
val isAnyGranted = componentStates.any { it.status.isGranted }
if (!isAnyGranted) {
if (!permission.isOptional()) {
add(permission)
} else {
val wasPreviouslyHandled =
permission.name in requestedPermissions ||
componentStates.any { it.status.shouldShowRationale }
if (!wasPreviouslyHandled) {
add(permission)
}
}
}
}
}References
- Simplify Complex Logic: Look for needlessly complex code. If a multi-line block of logic can be condensed into a more concise and readable idiomatic expression, suggest the simplification. (link)
Summary
Adds an optional location permission request step to the onboarding flow and introduces a "Save Location" toggle switch in the in-app Settings screen.
This builds on the location settings persistence and decoupled
LocationProvidercontract introduced in #575.Key Changes
:feature:permissions:PermissionEnum.LOCATIONas an optional permission card.PermissionsRepositoryto track dismissed and requested permissions across sessions.PermissionsScreenComponentsfor location permission rationale alert dialog and app-settings deep-linking.SettingsRepository.updateLocationEnabled(true)when granted during onboarding.:feature:settings:LocationSettingComponent("Save Location" toggle) to the Settings screen.LocationUiState(hidden whenLocationProvideris absent,Enabled.On/Offwhen present).:app:PermissionsRepositoryImplviaPermissionsModule.onOpenAppSettingsfromMainActivity/JcaApptoSettingsScreen.PermissionsScreenTest,PermissionsViewModelTest,SettingsScreenTest,CameraAppSettingsViewModelTest).