Skip to content

fix: [SDK-4735] route requestPermission fallbackToSettings to settings when permanently denied#2656

Merged
fadi-george merged 11 commits into
mainfrom
fadi/sdk-4735
Jun 2, 2026
Merged

fix: [SDK-4735] route requestPermission fallbackToSettings to settings when permanently denied#2656
fadi-george merged 11 commits into
mainfrom
fadi/sdk-4735

Conversation

@fadi-george
Copy link
Copy Markdown
Contributor

@fadi-george fadi-george commented Jun 1, 2026

Description

One Line Summary

Fix requestPermission(fallbackToSettings = true) so the "open settings" prompt actually appears when a permission is permanently denied.

Before, SDK wasn't showing settings prompt:
Screenshot 2026-05-29 at 7 36 51 PM

Details

Motivation

When a notification (or location) permission is permanently denied, requestPermission(true) is supposed to route the user to the app's settings. Two separate bugs prevented this:

  1. Settings dialog created then immediately dismissed (regression). The fallback dialog was parented to the transparent PermissionsActivity, which finishes immediately after the reject callback. Its onDestroy() then called AlertDialogPrepromptForAndroidSettings.dismissCurrentDialog(), tearing down the dialog the instant it was shown. Symptom: showSettings == true in the debugger, but nothing visible on screen. This regressed in the Oct 2025 ViewModel refactor (6fa661dfb), which moved the activity-finish to after the callback and added the onDestroy dismiss; the pre-refactor flow finished the activity first and showed the dialog on the resumed host activity.

  2. Stuck permanently-denied state. shouldShowSettings only persisted the "resolved denial" preference on the exact shouldShowRequestPermissionRationale true -> false transition. If a permission was already permanently blocked when OneSignal first observed it (denied via system Settings, in a prior session/version, or process death mid-prompt), that transition is never seen, the pref stays false, and the fallback is permanently stuck.

Scope

  • Affects the notification and location permission settings-fallback flow only.
  • NotificationPermissionController / LocationPermissionController: the fallback dialog is now deferred until a non-PermissionsActivity host activity is in the foreground, then shown there.
  • PermissionsActivity.onDestroy -> dismissCurrentDialog() removed, along with the now-dead currentDialog/dismissCurrentDialog tracking in AlertDialogPrepromptForAndroidSettings.
  • PermissionsViewModel + IPreferencesService: new PROMPTED_PERMISSION_ pref records that OneSignal has requested a permission at least once, used by shouldShowSettings to recover the permanently-blocked state without having witnessed the rationale transition.
  • No public API changes. Normal first-prompt and grant flows are unchanged.
  • Also corrects a broken IModule import in LocationModule and reformats its builder chains.

Testing

Unit testing

Added 3 cases to PermissionsViewModelTests covering the recovery path: shows settings when permanently blocked and previously prompted, does not show settings on a first denial when never prompted before, and persists the prompted-before flag after a request. Existing permission tests updated/retained.

Manual testing

Built the demo app against the local SDK in Android Studio. Verified that, on an install whose notification permission was permanently denied, tapping PROMPT PUSH now surfaces the "open settings" dialog (and it stays visible) rather than nothing.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Note: touches notification and location runtime-permission prompting (no dedicated checklist item exists for permissions).

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs (none)

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

Made with Cursor

@fadi-george fadi-george marked this pull request as ready for review June 1, 2026 17:13
@fadi-george fadi-george requested a review from nan-li June 1, 2026 17:17
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

📊 Diff Coverage Report

Diff Coverage Report (Changed Lines Only)

Gate: aggregate coverage on changed executable lines must be ≥ 80% (JaCoCo line data for lines touched in the diff).

Changed Files Coverage

  • PermissionsActivity.kt: 0/68 touched executable lines (0.0%) (all lines — could not resolve diff)
    • 68 uncovered touched lines in this file
  • AlertDialogPrepromptForAndroidSettings.kt: 0/16 touched executable lines (0.0%) (16 touched lines in diff)
    • 16 uncovered touched lines in this file
  • PermissionsViewModel.kt: 18/19 touched executable lines (94.7%) (32 touched lines in diff)
  • LocationModule.kt: 0/10 touched executable lines (0.0%) (20 touched lines in diff)
    • 10 uncovered touched lines in this file
  • LocationPermissionController.kt: 0/31 touched executable lines (0.0%) (56 touched lines in diff)
    • 31 uncovered touched lines in this file
  • NotificationPermissionController.kt: 0/29 touched executable lines (0.0%) (54 touched lines in diff)
    • 29 uncovered touched lines in this file

Overall (aggregate gate)

18/173 touched executable lines covered (10.4% — requires ≥ 80%)

Per-file detail (informational; gate is aggregate above):

  • PermissionsActivity.kt: 0.0% (68 uncovered touched lines)

  • AlertDialogPrepromptForAndroidSettings.kt: 0.0% (16 uncovered touched lines)

  • LocationModule.kt: 0.0% (10 uncovered touched lines)

  • LocationPermissionController.kt: 0.0% (31 uncovered touched lines)

  • NotificationPermissionController.kt: 0.0% (29 uncovered touched lines)

❌ Coverage Check Failed

Aggregate coverage on touched lines is 10.4% (minimum 80%).

📥 View workflow run

@fadi-george
Copy link
Copy Markdown
Contributor Author

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Permission-flow logic change with new persisted state and deferred lifecycle handler registration — worth a human look despite no bugs surfaced by automated review.

Extended reasoning...

Overview

The PR fixes two bugs in requestPermission(fallbackToSettings = true) for notification and location permissions: (1) the settings fallback dialog was being torn down by PermissionsActivity.onDestroy immediately after creation, and (2) the permanently-denied recovery path could get permanently stuck when the shouldShowRequestPermissionRationale true → false transition was never witnessed. Touches PermissionsActivity, AlertDialogPrepromptForAndroidSettings, PermissionsViewModel, both NotificationPermissionController / LocationPermissionController, a new PROMPTED_PERMISSION_ preference key, plus an unrelated LocationModule import fix and formatting.

Security risks

No direct security risks — no auth, crypto, or permission-grant logic changes. The change only affects how the SDK routes the user to the system Settings UI when a permission is permanently denied; the OS still owns the actual grant decision. The new preference key is a boolean local flag with no PII.

Level of scrutiny

Medium-to-high. This is critical user-facing runtime-permission flow code, with logic changes (lifecycle handler registration to defer dialog presentation, new persisted state with subtle ordering relative to shouldShowSettings) rather than a mechanical refactor. The deferred-dialog pattern relies on addActivityLifecycleHandler firing onActivityAvailable synchronously when there is a current activity, which is correctly handled by the is PermissionsActivity guard. The recovery-path ordering (PROMPTED flag written after shouldShowSettings reads it) is documented and looks correct.

Other factors

The diff coverage gate is failing (10.4% vs required 80%) because the new Controller paths are exercised by Robolectric tests that JaCoCo doesn't credit — substantive tests were added (3 cases per controller plus 3 ViewModel cases), so this is a tooling artifact rather than a real coverage gap. PR has been manually tested by the author. Author explicitly requested review via @claude review; given the touched surface (permissions) and the non-trivial state machine changes, a human reviewer familiar with the Oct 2025 ViewModel refactor history should sign off.

Copy link
Copy Markdown
Contributor

@nan-li nan-li left a comment

Choose a reason for hiding this comment

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

I repro'd original issue and now it works as expected

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.

2 participants