Skip to content

Add location permission UX to onboarding and settings - #586

Open
temcguir wants to merge 1 commit into
mainfrom
temcguir/location-permissions-and-settings-ui
Open

temcguir wants to merge 1 commit into
mainfrom
temcguir/location-permissions-and-settings-ui

Conversation

@temcguir

Copy link
Copy Markdown
Collaborator

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 LocationProvider contract introduced in #575.

Key Changes

  • :feature:permissions:
    • Adds PermissionEnum.LOCATION as an optional permission card.
    • Implements PermissionsRepository to track dismissed and requested permissions across sessions.
    • Adds PermissionsScreenComponents for location permission rationale alert dialog and app-settings deep-linking.
    • Automatically updates SettingsRepository.updateLocationEnabled(true) when granted during onboarding.
  • :feature:settings:
    • Adds LocationSettingComponent ("Save Location" toggle) to the Settings screen.
    • Adds LocationUiState (hidden when LocationProvider is absent, 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.
  • :app:
    • Binds PermissionsRepositoryImpl via PermissionsModule.
    • Passes onOpenAppSettings from MainActivity / JcaApp to SettingsScreen.
  • Testing:
    • Adds unit and Compose UI tests for onboarding and settings screens and ViewModels (PermissionsScreenTest, PermissionsViewModelTest, SettingsScreenTest, CameraAppSettingsViewModelTest).

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. 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)

Comment on lines +85 to +93
@OptIn(ExperimentalPermissionsApi::class)
@Composable
internal fun PermissionTemplate(
permissionEnum: PermissionEnum,
permissionStates: MultiplePermissionsState,
onDismissPermission: () -> Unit,
onOpenAppSettings: () -> Unit,
modifier: Modifier = Modifier
) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. Document all non-private members: All non-private classes, functions, and composables must have KDoc documentation. (link)

Comment on lines +788 to +793
internal fun setLocationEnabled(enabled: Boolean) {
viewModelScope.launch {
settingsRepository.updateLocationEnabled(enabled)
Log.d(TAG, "set save location enabled: $enabled")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. 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)

Comment on lines +134 to +157
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant