Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,19 @@

<receiver
android:name="com.onesignal.notifications.receivers.NotificationDismissReceiver"
android:exported="true"/>
android:exported="false"/>

<receiver
android:name="com.onesignal.notifications.receivers.BootUpReceiver"
android:exported="true">
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>

Check warning on line 90 in OneSignalSDK/onesignal/notifications/src/main/AndroidManifest.xml

View check run for this annotation

Claude / Claude Code Review

BootUpReceiver loses QUICKBOOT_POWERON deliveries with exported=false

🟡 Setting `BootUpReceiver` to `android:exported="false"` silently disables the `android.intent.action.QUICKBOOT_POWERON` action that remains in its intent-filter. Unlike `BOOT_COMPLETED` (a protected broadcast sent by `system_server` under SYSTEM_UID, which still reaches non-exported receivers), `QUICKBOOT_POWERON` is not in AOSP's protected broadcast list — it is emitted by OEM launchers (HTC Sense, older Lenovo / older Samsung quick-power-on flows) under their own UIDs, so the platform will bl
Comment on lines 84 to 90
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 🟡 Setting BootUpReceiver to android:exported="false" silently disables the android.intent.action.QUICKBOOT_POWERON action that remains in its intent-filter. Unlike BOOT_COMPLETED (a protected broadcast sent by system_server under SYSTEM_UID, which still reaches non-exported receivers), QUICKBOOT_POWERON is not in AOSP's protected broadcast list — it is emitted by OEM launchers (HTC Sense, older Lenovo / older Samsung quick-power-on flows) under their own UIDs, so the platform will block cross-UID delivery to a non-exported receiver. Full-reboot restore via BOOT_COMPLETED is unaffected, only the quickboot fast-resume path on those legacy OEM devices regresses; fix by dropping the now-unreachable QUICKBOOT_POWERON action from the filter, splitting it into its own exported receiver, or keeping the receiver exported="true" with android:permission="android.permission.RECEIVE_BOOT_COMPLETED" as a guard. Nit — affected device population is very small in 2026.

Extended reasoning...

What the bug is. BootUpReceiver declares two actions in its intent-filter: android.intent.action.BOOT_COMPLETED and android.intent.action.QUICKBOOT_POWERON (manifest lines 84-90). The PR flips the receiver from exported="true" to exported="false". With exported="false", Android only delivers broadcasts from the same UID or from system_server for protected broadcasts. BOOT_COMPLETED IS in AOSP's protected broadcast list (declared in frameworks/base/core/res/AndroidManifest.xml) and is dispatched by system_server, so it continues to reach the receiver. QUICKBOOT_POWERON is NOT in that list — it is an OEM-specific broadcast originally introduced by HTC Sense's quickboot flow and adopted by some older Lenovo / older Samsung quick-power-on launchers, dispatched by an OEM launcher process under its own UID.

The code path that triggers it. BootUpReceiver.kt runs the same logic (NotificationRestoreWorkManager.beginEnqueueingWork) regardless of which action arrived — i.e., the SDK explicitly opted into supporting the quickboot fast-resume path when it added QUICKBOOT_POWERON to the filter. After this PR, an OEM launcher's QUICKBOOT_POWERON broadcast targeted at the OneSignal app's process is cross-UID, the receiver is non-exported, and Android's broadcast access control blocks the delivery. The action remains in the filter but is silently unreachable.

Why existing code doesn't prevent it. The other components that were also flipped to exported="false" in this PR are safe for different reasons: UpgradeReceiver receives MY_PACKAGE_REPLACED from system_server (protected), NotificationDismissReceiver and the two NotificationOpenedActivity variants are triggered via PendingIntent that runs under the host app's own UID. BootUpReceiver for BOOT_COMPLETED is similarly safe (protected broadcast from system_server). QUICKBOOT_POWERON is the one action with no such rescue: no protected-broadcast exemption, no same-UID sender, no PendingIntent wrapping.

Impact. On the affected legacy OEM devices (mostly older HTC Sense, some older Lenovo, some older Samsung quick-power-on), users who quickboot the device will no longer have OneSignal notifications restored via NotificationRestoreWorkManager. Full reboots still work because BOOT_COMPLETED is unaffected. This is exactly the scenario the PR description's 'Recommended follow-up coverage' lists as untested ('boot restore'). Impact in 2026 is narrow — HTC stopped selling Android phones around 2017, quickboot is largely deprecated, and Android 8+ also tightens manifest receivers for implicit broadcasts not on the exempt list — so the quickboot regression only meaningfully affects a small legacy population on API 21-25 devices that still emit QUICKBOOT_POWERON from a non-system-privileged sender.

How to fix. Pick one: (a) drop <action android:name="android.intent.action.QUICKBOOT_POWERON" /> from the intent-filter as a deliberate, documented behavior change since it is now unreachable anyway; (b) split QUICKBOOT_POWERON into its own <receiver android:exported="true"> so the cross-UID OEM-launcher broadcast can reach it; or (c) revert BootUpReceiver to android:exported="true" and add android:permission="android.permission.RECEIVE_BOOT_COMPLETED" as a guard (though some QUICKBOOT_POWERON senders may not hold that permission). The other five components in this PR can stay non-exported.

Step-by-step proof.

  1. App with OneSignal SDK is installed on an older HTC Sense device (API 21-25), and OneSignal has previously displayed notifications that are pending restore.
  2. User triggers quickboot (fast-resume) rather than a full reboot.
  3. HTC Sense's quickboot launcher process (its own UID, not SYSTEM_UID) calls sendBroadcast(new Intent("android.intent.action.QUICKBOOT_POWERON")).
  4. ActivityManagerService walks registered receivers for that action and finds com.onesignal.notifications.receivers.BootUpReceiver declared with android:exported="false".
  5. QUICKBOOT_POWERON is not in AOSP's protected broadcast list, so AMS does NOT apply the SYSTEM_UID-bypass that lets protected broadcasts reach non-exported receivers.
  6. AMS checks the sender UID (HTC launcher) against the receiver's component visibility: cross-UID broadcast to non-exported receiver → denied.
  7. BootUpReceiver.onReceive is never invoked for QUICKBOOT_POWERON.
  8. NotificationRestoreWorkManager.beginEnqueueingWork is not enqueued on the quickboot path. Pending OneSignal notifications are not restored.
  9. Confirm BOOT_COMPLETED still works: on a full reboot, system_server (SYSTEM_UID) dispatches the protected BOOT_COMPLETED broadcast; AMS allows it to reach non-exported receivers; BootUpReceiver.onReceive fires; restore proceeds normally. Only the quickboot fast-resume path is regressed.

<receiver
android:name="com.onesignal.notifications.receivers.UpgradeReceiver"
android:exported="true">
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
Expand All @@ -102,13 +102,13 @@
android:excludeFromRecents="true"
android:taskAffinity=""
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="true" />
android:exported="false" />

<activity
android:name="com.onesignal.notifications.activities.NotificationOpenedActivityAndroid22AndOlder"
android:noHistory="true"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="true" />
android:exported="false" />
</application>
</manifest>
Loading