Created
June 26, 2025 10:01
-
-
Save joel-teratis/261cdc745672b6c0833df1437b951327 to your computer and use it in GitHub Desktop.
Android Exact Notification Scheduling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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