Skip to content

Instantly share code, notes, and snippets.

@joel-teratis
Created June 26, 2025 10:01
Show Gist options
  • Save joel-teratis/261cdc745672b6c0833df1437b951327 to your computer and use it in GitHub Desktop.
Save joel-teratis/261cdc745672b6c0833df1437b951327 to your computer and use it in GitHub Desktop.
Android Exact Notification Scheduling
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
...
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:hardwareAccelerated="true" android:roundIcon="@mipmap/ic_launcher_round">
...
<receiver android:name=".NotificationReceiver" android:exported="true"/>
...
</application>
</manifest>
package your.package.name
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.net.Uri
import android.provider.Settings
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.facebook.react.modules.core.DeviceEventManagerModule
class ExactNotificationSchedulerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private var exactAlarmReceiver: BroadcastReceiver? = null
override fun getName(): String {
return "ExactNotificationScheduler"
}
/**
* Schedule a notification using the Android AlarmManager.
*
* @param id A unique identifier for the notification.
* @param title The notification title.
* @param body The notification body.
* @param triggerAtMillis Time in milliseconds at which to fire the notification.
*/
@ReactMethod
fun scheduleNotification(id: String, title: String, body: String, triggerAtMillis: Double) {
val context = reactApplicationContext
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (!alarmManager.canScheduleExactAlarms()) {
return
}
val intent = Intent(context, NotificationReceiver::class.java).apply {
putExtra("notificationId", id)
putExtra("title", title)
putExtra("body", body)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
id.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis.toLong(),
pendingIntent
)
}
/**
* Cancels a scheduled notification by its id.
*
* @param id Unique identifier for the notification to cancel.
*/
@ReactMethod
fun cancelNotification(id: String) {
val context = reactApplicationContext
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, NotificationReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
id.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
}
@ReactMethod
fun canScheduleExactAlarms(promise: Promise) {
try {
val context = reactApplicationContext
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
promise.resolve(alarmManager.canScheduleExactAlarms())
} catch (e: Exception) {
promise.reject("ERROR", e)
}
}
@ReactMethod
fun openSettingsForExactAlarms() {
val context = reactApplicationContext
val intent = Intent(android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
intent.data = Uri.parse("package:" + context.packageName)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
@ReactMethod
fun addListener(eventName: String) {
// Required for RN event emitter
}
@ReactMethod
fun removeListeners(count: Int) {
// Required for RN event emitter
}
/**
* Start listening for changes in the exact alarm scheduling permission.
* When the permission state changes, an event "ExactAlarmPermissionChanged"
* will be emitted to JavaScript with a boolean value.
*/
@ReactMethod
fun startExactAlarmPermissionListener() {
if (exactAlarmReceiver == null) {
exactAlarmReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// Emit an event to JavaScript with the new permission state.
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("ExactAlarmPermissionChanged", true)
}
}
// Create an IntentFilter for the permission state change action.
val filter = IntentFilter(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED)
reactApplicationContext.registerReceiver(exactAlarmReceiver, filter)
}
}
}
package your.package.name
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class ExactNotificationSchedulerPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(ExactNotificationSchedulerModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
package your.package.name
import android.app.AlarmManager
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import your.package.name.ExactNotificationSchedulerPackage
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
this,
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
packages.add(ExactNotificationSchedulerPackage());
return packages
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
)
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}
package your.package.name
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import your.package.name.R
class NotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val title = intent.getStringExtra("title") ?: "Default Title"
val body = intent.getStringExtra("body") ?: "Default Body"
val notificationId = intent.getStringExtra("notificationId")?.hashCode() ?: System.currentTimeMillis().toInt()
val channelId = "exact_notification_channel"
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"Exact Notifications",
NotificationManager.IMPORTANCE_MAX
)
notificationManager.createNotificationChannel(channel)
}
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
launchIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
val contentIntent = PendingIntent.getActivity(
context,
notificationId,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, channelId)
.setContentTitle(title)
.setContentText(body)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(contentIntent)
.setAutoCancel(true)
.setLocalOnly(false)
.build()
notificationManager.notify(notificationId, notification)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment