How I Fixed Expo Android 15 BOOT_COMPLETED App Rejections
Google Play’s static analyzer has become much stricter regarding foreground service permissions, especially with Android 15 (API 35). If your Expo app utilizes both audio and notification features, your submission might get abruptly blocked with errors citing BOOT_COMPLETED broadcast receivers.
This post walks through the exact issue, why Google Play flags it even when your app runs perfectly, and the exact Expo Config Plugin workaround that gets your app successfully submitted.
The Problem: Google Play Blocks Submission
Everything works locally. npx expo-doctor reports zero issues. Your app builds flawlessly on EAS or Android Studio. But moments after uploading your production release to the Google Play Console, you get slapped with a rejection error regarding broadcast receivers.
The warning often points to functions like postOrStartForegroundNotification and startForegroundWithNotification.
The scenario:
- You are using expo-audio (e.g., v1.1.0).
- You are using expo-notifications.
- Google Play flags your app as a violation of foreground service policies.
Why This Happens
This issue comes down to Android 15's strict rules and an overzealous static analyzer in the Play Store:
- The Restriction: In Android 15, BOOT_COMPLETED broadcast receivers are strictly prohibited from launching foreground services tied to microphone use or media playback.
- The Culprits: expo-notifications adds a BOOT_COMPLETED receiver to restore scheduled local notifications. expo-audio declares restricted foreground service types for media playback.
- The False Positive: Even if the receiver never triggers the audio service, Google Play sees both declarations in your merged
AndroidManifest.xmland blocks the upload.
The Fix That Worked
To bypass Google's static analysis block, strip the BOOT_COMPLETED actions from the NotificationsService receiver using a custom Expo Config Plugin that overrides the receiver definition during the manifest merge.
Step-by-Step Config Plugin Fix
1. Create the Custom Plugin
Create a new file at plugins/withDisableNotificationsBootActions.js and add:
const { withAndroidManifest } = require("@expo/config-plugins")
/**
* Config plugin to remove BOOT_COMPLETED and related boot actions from
* expo-notifications NotificationsService receiver.
*
* This is required for Android 15 (API 35) compatibility as BOOT_COMPLETED
* broadcast receivers cannot start microphone or media playback foreground services.
*/
const withDisableNotificationsBootActions = (config) => {
return withAndroidManifest(config, (modConfig) => {
const mainApplication = modConfig.modResults.manifest.application?.[0]
if (!mainApplication) {
return modConfig
}
// Ensure tools namespace is declared
if (!modConfig.modResults.manifest.$["xmlns:tools"]) {
modConfig.modResults.manifest.$["xmlns:tools"] =
"http://schemas.android.com/tools"
}
// Initialize receivers array if not exists
if (!mainApplication.receiver) {
mainApplication.receiver = []
}
// Remove any existing NotificationsService receiver
mainApplication.receiver = mainApplication.receiver.filter(
(r) =>
r.$["android:name"] !==
"expo.modules.notifications.service.NotificationsService",
)
// Add replacement receiver with tools:node="replace" to override library's version
mainApplication.receiver.push({
$: {
"android:name":
"expo.modules.notifications.service.NotificationsService",
"android:enabled": "true",
"android:exported": "false",
"tools:node": "replace",
},
"intent-filter": [
{
$: {
"android:priority": "-1",
},
action: [
{
$: {
"android:name": "expo.modules.notifications.NOTIFICATION_EVENT",
},
},
{
$: {
"android:name": "android.intent.action.MY_PACKAGE_REPLACED",
},
},
],
},
],
})
return modConfig
})
}
module.exports = withDisableNotificationsBootActions2. Register the Plugin
Add the plugin in your app.json or app.config.js:
{
"expo": {
"plugins": [
"./plugins/withDisableNotificationsBootActions"
]
}
}3. Rebuild Your App
Before building your final release, clear out your old native directories:
npx expo prebuild --cleanAfter prebuilding, your generated AndroidManifest.xml will be stripped of the offending BOOT_COMPLETED actions, allowing your app to pass Google Play's checks.
Alternative: The "Proceed Anyway" Bypass
If you are in a rush, Google Play offers a bypass. In the Play Console, go to the release issue, click Proceed Anyway, and link the Expo GitHub issue for context. Many developers report this works.
Key Takeaways for React Native Developers
- Static analysis is blind to context: Google Play flags manifest declarations, not runtime behavior.
- Config Plugins are a superpower: tools:node="replace" cleanly patches the manifest without ejecting.
- Android 15 is unforgiving: Audio and media foreground services are highly restricted.
Final Word
If your Android app is getting rejected for BOOT_COMPLETED and foreground service violations, don't waste hours digging through JS/TS. It's a static manifest collision. The config plugin fix gets your build compliant and back into the Play Store.