react-native-alarms 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.bundle/config +2 -0
  2. package/AlarmService.d.ts +112 -0
  3. package/AlarmService.ts +196 -0
  4. package/README.md +385 -2
  5. package/android/app/build.gradle +119 -0
  6. package/android/app/debug.keystore +0 -0
  7. package/android/app/proguard-rules.pro +10 -0
  8. package/android/app/src/main/AndroidManifest.xml +81 -0
  9. package/android/app/src/main/java/com/reactnativealarm/MainActivity.kt +29 -0
  10. package/android/app/src/main/java/com/reactnativealarm/MainApplication.kt +29 -0
  11. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmData.kt +134 -0
  12. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmModule.kt +263 -0
  13. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmPackage.kt +24 -0
  14. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmReceiver.kt +43 -0
  15. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmRingActivity.kt +86 -0
  16. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmScheduler.kt +74 -0
  17. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmService.kt +285 -0
  18. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmStore.kt +13 -0
  19. package/android/app/src/main/java/com/reactnativealarm/alarm/BootReceiver.kt +29 -0
  20. package/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  21. package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  22. package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  23. package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  24. package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  25. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  26. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  27. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  28. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  29. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  30. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  31. package/android/app/src/main/res/values/strings.xml +3 -0
  32. package/android/app/src/main/res/values/styles.xml +9 -0
  33. package/android/build.gradle +21 -0
  34. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  35. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  36. package/android/gradle.properties +49 -0
  37. package/android/gradlew +248 -0
  38. package/android/gradlew.bat +98 -0
  39. package/android/settings.gradle +6 -0
  40. package/app.json +4 -0
  41. package/index.d.ts +20 -0
  42. package/index.js +12 -0
  43. package/index.ts +21 -0
  44. package/ios/.xcode.env +11 -0
  45. package/ios/Podfile +34 -0
  46. package/ios/Podfile.lock +2179 -0
  47. package/ios/ReactNativeAlarm/AppDelegate.swift +14 -0
  48. package/ios/ReactNativeAlarm/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  49. package/ios/ReactNativeAlarm/Images.xcassets/Contents.json +6 -0
  50. package/ios/ReactNativeAlarm/Info.plist +79 -0
  51. package/ios/ReactNativeAlarm/LaunchScreen.storyboard +47 -0
  52. package/ios/ReactNativeAlarm/PrivacyInfo.xcprivacy +37 -0
  53. package/ios/ReactNativeAlarm/SceneDelegate.swift +56 -0
  54. package/ios/ReactNativeAlarm.xcodeproj/project.pbxproj +530 -0
  55. package/ios/ReactNativeAlarm.xcodeproj/xcshareddata/xcschemes/ReactNativeAlarm.xcscheme +88 -0
  56. package/ios/ReactNativeAlarm.xcworkspace/contents.xcworkspacedata +10 -0
  57. package/ios/alarm/AlarmData.swift +97 -0
  58. package/ios/alarm/AlarmKitBackend.swift +212 -0
  59. package/ios/alarm/AlarmModule.m +53 -0
  60. package/ios/alarm/AlarmModule.swift +284 -0
  61. package/ios/alarm/AlarmStore.swift +51 -0
  62. package/ios/alarm/NotificationBackend.swift +227 -0
  63. package/package.json +91 -3
  64. package/specs/NativeAlarm.ts +95 -0
@@ -0,0 +1,24 @@
1
+ package com.alarm
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ class AlarmPackage : BaseReactPackage() {
10
+ override fun getModule(name: String, ctx: ReactApplicationContext): NativeModule? =
11
+ if (name == AlarmModule.NAME) AlarmModule(ctx) else null
12
+
13
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
14
+ mapOf(
15
+ AlarmModule.NAME to ReactModuleInfo(
16
+ AlarmModule.NAME, AlarmModule.NAME,
17
+ false, // canOverrideExistingModule
18
+ false, // needsEagerInit
19
+ false, // isCxxModule
20
+ false // isTurboModule
21
+ )
22
+ )
23
+ }
24
+ }
@@ -0,0 +1,43 @@
1
+ package com.alarm
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.util.Log
7
+
8
+ /**
9
+ * AlarmManager fires here at the exact trigger time. The receiver only gets ~10s of
10
+ * guaranteed execution, so it updates the store and hands off to [AlarmService]
11
+ * immediately. The service does the actual ringing, screen wake and UI.
12
+ */
13
+ class AlarmReceiver : BroadcastReceiver() {
14
+ companion object {
15
+ private const val TAG = "AlarmReceiver"
16
+ const val ACTION_FIRE = "com.alarm.FIRE"
17
+ }
18
+
19
+ override fun onReceive(ctx: Context, intent: Intent) {
20
+ if (intent.action != ACTION_FIRE) return
21
+ val id = intent.getStringExtra(AlarmScheduler.EXTRA_ID) ?: return
22
+ val alarm = AlarmStore.get(ctx, id)
23
+ if (alarm == null) {
24
+ Log.w(TAG, "Alarm $id fired but is no longer stored")
25
+ return
26
+ }
27
+ Log.i(TAG, "Alarm $id fired (snooze=${intent.getBooleanExtra(AlarmScheduler.EXTRA_SNOOZE, false)})")
28
+
29
+ if (!intent.getBooleanExtra(AlarmScheduler.EXTRA_SNOOZE, false)) {
30
+ if (alarm.repeatType == "none") {
31
+ alarm.enabled = false
32
+ AlarmStore.save(ctx, alarm)
33
+ } else {
34
+ // Repeating: arm the next occurrence right away so it is guaranteed even
35
+ // if the user force-stops the ringing screen.
36
+ runCatching { AlarmScheduler.schedule(ctx, alarm) }
37
+ .onFailure { Log.e(TAG, "Could not re-arm repeating alarm $id", it) }
38
+ }
39
+ }
40
+
41
+ AlarmService.start(ctx, id)
42
+ }
43
+ }
@@ -0,0 +1,86 @@
1
+ package com.alarm
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.os.Build
6
+ import android.os.Bundle
7
+ import android.view.WindowManager
8
+ import com.facebook.react.ReactActivity
9
+ import com.facebook.react.ReactActivityDelegate
10
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
11
+ import com.facebook.react.defaults.DefaultReactActivityDelegate
12
+ import java.lang.ref.WeakReference
13
+
14
+ /**
15
+ * Full-screen ringing UI shown over the lock screen (or on top of the app when it is
16
+ * open). Hosts the React component registered as [COMPONENT_NAME] and passes the
17
+ * ringing alarm as its initial props. Mirrors Upupup's RingActivity.
18
+ */
19
+ class AlarmRingActivity : ReactActivity() {
20
+
21
+ override fun getMainComponentName(): String = COMPONENT_NAME
22
+
23
+ override fun createReactActivityDelegate(): ReactActivityDelegate =
24
+ object : DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) {
25
+ override fun getLaunchOptions(): Bundle? = AlarmService.current?.toBundle()
26
+ }
27
+
28
+ override fun onCreate(savedInstanceState: Bundle?) {
29
+ // Stale launch (alarm already stopped): don't even spin up the React root.
30
+ if (AlarmService.current == null) {
31
+ super.onCreate(null)
32
+ finish()
33
+ return
34
+ }
35
+ instance = WeakReference(this)
36
+ super.onCreate(savedInstanceState)
37
+ showOverLockScreen()
38
+ }
39
+
40
+ private fun showOverLockScreen() {
41
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
42
+ setShowWhenLocked(true)
43
+ setTurnScreenOn(true)
44
+ } else {
45
+ @Suppress("DEPRECATION")
46
+ window.addFlags(
47
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
48
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
49
+ )
50
+ }
51
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
52
+ }
53
+
54
+ /** The only ways out are Stop and Snooze: swallow the back button. */
55
+ override fun invokeDefaultOnBackPressed() { /* consume */ }
56
+
57
+ override fun onDestroy() {
58
+ if (instance?.get() === this) instance = null
59
+ super.onDestroy()
60
+ }
61
+
62
+ companion object {
63
+ const val COMPONENT_NAME = "AlarmRing"
64
+
65
+ private var instance: WeakReference<AlarmRingActivity>? = null
66
+
67
+ val isOpen: Boolean get() = instance?.get() != null
68
+
69
+ /** Called by [AlarmService] when ringing ends for any reason. */
70
+ fun finishIfOpen() {
71
+ instance?.get()?.finish()
72
+ }
73
+
74
+ /**
75
+ * Brings the ring screen up when an alarm is ringing and the user opens the app
76
+ * (the full-screen intent only fires when the screen is off/locked).
77
+ */
78
+ fun openIfRinging(activity: Activity) {
79
+ if (AlarmService.current != null && !isOpen) {
80
+ activity.startActivity(
81
+ Intent(activity, AlarmRingActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
82
+ )
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,74 @@
1
+ package com.alarm
2
+
3
+ import android.app.AlarmManager
4
+ import android.app.PendingIntent
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.os.Build
8
+ import android.util.Log
9
+
10
+ /**
11
+ * Registers alarms with [AlarmManager.setAlarmClock]: exact trigger, exempt from Doze
12
+ * and battery optimisations, and the system shows the alarm-clock status-bar icon.
13
+ */
14
+ object AlarmScheduler {
15
+ private const val TAG = "AlarmScheduler"
16
+ const val EXTRA_ID = "alarmId"
17
+ const val EXTRA_SNOOZE = "snooze"
18
+
19
+ private fun am(ctx: Context) = ctx.getSystemService(AlarmManager::class.java)
20
+
21
+ fun canScheduleExact(ctx: Context) =
22
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.S || am(ctx).canScheduleExactAlarms()
23
+
24
+ /** Re-syncs every enabled alarm with AlarmManager. Idempotent. */
25
+ fun scheduleAll(ctx: Context) {
26
+ AlarmStore.all(ctx).forEach { a ->
27
+ if (a.enabled) runCatching { schedule(ctx, a) }
28
+ .onFailure { Log.e(TAG, "Could not re-arm ${a.id}", it) }
29
+ else cancel(ctx, a.id)
30
+ }
31
+ }
32
+
33
+ fun schedule(ctx: Context, a: AlarmData) {
34
+ val at = a.nextTrigger()
35
+ if (at > 0) setExact(ctx, at, firePI(ctx, a.id, false))
36
+ else Log.i(TAG, "Alarm ${a.id} has no future trigger; not scheduled")
37
+ }
38
+
39
+ fun scheduleSnooze(ctx: Context, a: AlarmData, minutes: Int) =
40
+ setExact(ctx, System.currentTimeMillis() + minutes * 60_000L, firePI(ctx, a.id, true))
41
+
42
+ fun cancel(ctx: Context, id: String) {
43
+ am(ctx).cancel(firePI(ctx, id, false))
44
+ cancelSnooze(ctx, id)
45
+ }
46
+
47
+ fun cancelSnooze(ctx: Context, id: String) {
48
+ am(ctx).cancel(firePI(ctx, id, true))
49
+ }
50
+
51
+ private fun setExact(ctx: Context, at: Long, pi: PendingIntent) {
52
+ if (!canScheduleExact(ctx)) {
53
+ throw SecurityException("Exact alarm permission (SCHEDULE_EXACT_ALARM) not granted")
54
+ }
55
+ // Tapping the status-bar alarm icon opens the app.
56
+ val launch = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)
57
+ val show = launch?.let {
58
+ PendingIntent.getActivity(ctx, 0, it, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
59
+ }
60
+ am(ctx).setAlarmClock(AlarmManager.AlarmClockInfo(at, show), pi)
61
+ Log.i(TAG, "Alarm set for ${java.util.Date(at)}")
62
+ }
63
+
64
+ private fun firePI(ctx: Context, id: String, snooze: Boolean): PendingIntent {
65
+ val i = Intent(ctx, AlarmReceiver::class.java)
66
+ .setAction(AlarmReceiver.ACTION_FIRE)
67
+ .putExtra(EXTRA_ID, id)
68
+ .putExtra(EXTRA_SNOOZE, snooze)
69
+ return PendingIntent.getBroadcast(
70
+ ctx, (id + if (snooze) "_s" else "").hashCode(), i,
71
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
72
+ )
73
+ }
74
+ }
@@ -0,0 +1,285 @@
1
+ package com.alarm
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.PendingIntent
7
+ import android.app.Service
8
+ import android.content.Context
9
+ import android.content.Intent
10
+ import android.content.pm.ServiceInfo
11
+ import android.media.AudioAttributes
12
+ import android.media.MediaPlayer
13
+ import android.media.RingtoneManager
14
+ import android.net.Uri
15
+ import android.os.Build
16
+ import android.os.Handler
17
+ import android.os.IBinder
18
+ import android.os.Looper
19
+ import android.os.PowerManager
20
+ import android.os.VibrationEffect
21
+ import android.os.Vibrator
22
+ import android.util.Log
23
+ import androidx.core.app.NotificationCompat
24
+ import androidx.core.app.ServiceCompat
25
+
26
+ /**
27
+ * Foreground service that actually rings: looping sound, vibration, a partial wake
28
+ * lock and a full-screen notification that pops [AlarmRingActivity] over the lock
29
+ * screen. Same architecture as Upupup's AlarmService.
30
+ *
31
+ * Why a service and not just a notification from the receiver: a BroadcastReceiver
32
+ * only gets ~10s of execution and the process can be killed right after. A
33
+ * foreground service keeps the process alive (JS runtime included once started),
34
+ * keeps the sound looping and is what lets a killed app ring reliably.
35
+ */
36
+ class AlarmService : Service() {
37
+
38
+ private val handler = Handler(Looper.getMainLooper())
39
+ private var player: MediaPlayer? = null
40
+ private var vibrator: Vibrator? = null
41
+ private var wakeLock: PowerManager.WakeLock? = null
42
+
43
+ override fun onBind(intent: Intent?): IBinder? = null
44
+
45
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
46
+ when (intent?.action) {
47
+ ACTION_SNOOZE -> {
48
+ snooze(intent.getIntExtra(EXTRA_SNOOZE_MINUTES, -1))
49
+ return START_NOT_STICKY
50
+ }
51
+ ACTION_DISMISS -> {
52
+ dismiss()
53
+ return START_NOT_STICKY
54
+ }
55
+ }
56
+
57
+ val id = intent?.getStringExtra(AlarmScheduler.EXTRA_ID)
58
+ val alarm = id?.let { AlarmStore.get(this, it) }
59
+ if (alarm == null) {
60
+ Log.w(TAG, "Started without a known alarm (id=$id); stopping")
61
+ // Must still enter the foreground briefly: we were started with
62
+ // startForegroundService() and the system expects startForeground().
63
+ runCatching { startInForeground(placeholderNotification()) }
64
+ stopSelf()
65
+ return START_NOT_STICKY
66
+ }
67
+ if (current != null) stopRinging() // a second alarm fired while one was ringing
68
+
69
+ current = alarm
70
+ Log.i(TAG, "Ringing alarm ${alarm.id}")
71
+
72
+ startInForeground(buildNotification(alarm))
73
+ acquireWakeLock()
74
+ startSound(alarm)
75
+ if (alarm.vibrate) startVibration()
76
+
77
+ // The full-screen intent only fires when the screen is off/locked and on some
78
+ // OEMs (Xiaomi, Oppo, Vivo...) it is unreliable even then. Launching the ring
79
+ // activity directly works whenever the app has "display over other apps"
80
+ // (SYSTEM_ALERT_WINDOW) or one of our activities is visible; the system
81
+ // silently ignores it otherwise, so it is safe to always try.
82
+ runCatching {
83
+ startActivity(
84
+ Intent(this, AlarmRingActivity::class.java)
85
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
86
+ )
87
+ }.onFailure { Log.w(TAG, "Could not start ring activity directly", it) }
88
+
89
+ AlarmModule.emit(AlarmModule.EVENT_FIRED, alarm)
90
+
91
+ // Safety net: never ring forever. Auto-snooze (or stop) after 5 minutes.
92
+ handler.postDelayed({ if (alarm.snoozeEnabled) snooze(-1) else dismiss() }, AUTO_TIMEOUT_MS)
93
+ return START_NOT_STICKY
94
+ }
95
+
96
+ private fun startInForeground(notification: Notification) {
97
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
98
+ ServiceCompat.startForeground(
99
+ this, NOTIFICATION_ID, notification,
100
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
101
+ )
102
+ } else {
103
+ startForeground(NOTIFICATION_ID, notification)
104
+ }
105
+ }
106
+
107
+ private fun snooze(minutes: Int) {
108
+ val alarm = current ?: run { stopSelf(); return }
109
+ val mins = if (minutes > 0) minutes else alarm.snoozeMinutes
110
+ runCatching { AlarmScheduler.scheduleSnooze(this, alarm, mins) }
111
+ .onFailure { Log.e(TAG, "Could not schedule snooze", it) }
112
+ AlarmModule.emit(AlarmModule.EVENT_SNOOZED, alarm)
113
+ stopRinging()
114
+ stopSelf()
115
+ }
116
+
117
+ private fun dismiss() {
118
+ val alarm = current ?: run { stopSelf(); return }
119
+ // A dismissed snoozed alarm must not ring again from its snooze intent.
120
+ AlarmScheduler.cancelSnooze(this, alarm.id)
121
+ AlarmModule.emit(AlarmModule.EVENT_DISMISSED, alarm)
122
+ stopRinging()
123
+ stopSelf()
124
+ }
125
+
126
+ private fun stopRinging() {
127
+ handler.removeCallbacksAndMessages(null)
128
+ runCatching { player?.stop() }
129
+ runCatching { player?.release() }
130
+ player = null
131
+ vibrator?.cancel()
132
+ vibrator = null
133
+ wakeLock?.let { if (it.isHeld) it.release() }
134
+ wakeLock = null
135
+ current = null
136
+ AlarmRingActivity.finishIfOpen()
137
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
138
+ }
139
+
140
+ override fun onDestroy() {
141
+ stopRinging()
142
+ super.onDestroy()
143
+ }
144
+
145
+ // ---- Sound ----
146
+
147
+ private fun startSound(alarm: AlarmData) {
148
+ val uri = alarm.sound.takeIf { it.isNotBlank() && it != "default" }?.let { Uri.parse(it) }
149
+ ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
150
+ ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
151
+
152
+ try {
153
+ val mp = MediaPlayer()
154
+ player = mp
155
+ mp.apply {
156
+ setAudioAttributes(
157
+ AudioAttributes.Builder()
158
+ .setUsage(AudioAttributes.USAGE_ALARM)
159
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
160
+ .build()
161
+ )
162
+ setDataSource(this@AlarmService, uri)
163
+ isLooping = true
164
+ prepare()
165
+ start()
166
+ }
167
+ } catch (e: Exception) {
168
+ Log.e(TAG, "Could not play $uri, vibrating only", e)
169
+ runCatching { player?.release() }
170
+ player = null
171
+ startVibration() // last resort: at least wake the user somehow
172
+ }
173
+ }
174
+
175
+ // ---- Vibration ----
176
+
177
+ @Suppress("DEPRECATION")
178
+ private fun startVibration() {
179
+ if (vibrator != null) return
180
+ val v = getSystemService(Vibrator::class.java) ?: return
181
+ vibrator = v
182
+ val pattern = longArrayOf(0, 800, 400, 800, 800)
183
+ v.vibrate(
184
+ VibrationEffect.createWaveform(pattern, 0),
185
+ AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).build()
186
+ )
187
+ }
188
+
189
+ // ---- Plumbing ----
190
+
191
+ private fun acquireWakeLock() {
192
+ val pm = getSystemService(PowerManager::class.java)
193
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "rnalarm:ring").apply {
194
+ acquire(AUTO_TIMEOUT_MS + 60_000)
195
+ }
196
+ }
197
+
198
+ private fun ensureChannel(): NotificationManager {
199
+ val manager = getSystemService(NotificationManager::class.java)
200
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
201
+ manager.createNotificationChannel(
202
+ NotificationChannel(CHANNEL_ID, "Alarms", NotificationManager.IMPORTANCE_HIGH).apply {
203
+ setSound(null, null) // the service plays its own sound
204
+ enableVibration(false)
205
+ setBypassDnd(true)
206
+ }
207
+ )
208
+ }
209
+ return manager
210
+ }
211
+
212
+ private fun placeholderNotification(): Notification {
213
+ ensureChannel()
214
+ return NotificationCompat.Builder(this, CHANNEL_ID)
215
+ .setSmallIcon(applicationInfo.icon)
216
+ .setContentTitle("Alarm")
217
+ .build()
218
+ }
219
+
220
+ private fun buildNotification(alarm: AlarmData): Notification {
221
+ ensureChannel()
222
+ val ringIntent = Intent(this, AlarmRingActivity::class.java)
223
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
224
+ val fullScreen = PendingIntent.getActivity(
225
+ this, 1, ringIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
226
+ )
227
+ val b = NotificationCompat.Builder(this, CHANNEL_ID)
228
+ .setSmallIcon(applicationInfo.icon)
229
+ .setContentTitle(alarm.title.ifBlank { "Alarm" })
230
+ .setContentText(alarm.body.ifBlank { "Alarm is ringing" })
231
+ .setPriority(NotificationCompat.PRIORITY_MAX)
232
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
233
+ .setOngoing(true)
234
+ .setAutoCancel(false)
235
+ .setFullScreenIntent(fullScreen, true)
236
+ .setContentIntent(fullScreen)
237
+ .addAction(0, "Stop", commandPI(ACTION_DISMISS, 2))
238
+ if (alarm.snoozeEnabled) {
239
+ b.addAction(0, "Snooze", commandPI(ACTION_SNOOZE, 3))
240
+ }
241
+ return b.build()
242
+ }
243
+
244
+ private fun commandPI(action: String, code: Int): PendingIntent =
245
+ PendingIntent.getService(
246
+ this, code, Intent(this, AlarmService::class.java).setAction(action),
247
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
248
+ )
249
+
250
+ companion object {
251
+ private const val TAG = "AlarmService"
252
+ private const val CHANNEL_ID = "rn_alarm_channel"
253
+ private const val NOTIFICATION_ID = 1
254
+ private const val AUTO_TIMEOUT_MS = 5L * 60 * 1000
255
+
256
+ const val ACTION_SNOOZE = "com.alarm.service.SNOOZE"
257
+ const val ACTION_DISMISS = "com.alarm.service.DISMISS"
258
+ const val EXTRA_SNOOZE_MINUTES = "snoozeMinutes"
259
+
260
+ /** The alarm currently ringing, readable by [AlarmRingActivity] and [AlarmModule]. */
261
+ @Volatile
262
+ var current: AlarmData? = null
263
+ private set
264
+
265
+ fun start(context: Context, alarmId: String) {
266
+ androidx.core.content.ContextCompat.startForegroundService(
267
+ context,
268
+ Intent(context, AlarmService::class.java).putExtra(AlarmScheduler.EXTRA_ID, alarmId)
269
+ )
270
+ }
271
+
272
+ fun snooze(context: Context, minutes: Int = -1) =
273
+ command(context, Intent(context, AlarmService::class.java)
274
+ .setAction(ACTION_SNOOZE).putExtra(EXTRA_SNOOZE_MINUTES, minutes))
275
+
276
+ fun dismiss(context: Context) =
277
+ command(context, Intent(context, AlarmService::class.java).setAction(ACTION_DISMISS))
278
+
279
+ private fun command(context: Context, intent: Intent) {
280
+ if (current == null) return // nothing ringing; avoid starting a service for nothing
281
+ runCatching { context.startService(intent) }
282
+ .onFailure { Log.w(TAG, "Could not send ${intent.action}", it) }
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,13 @@
1
+ package com.alarm
2
+
3
+ import android.content.Context
4
+
5
+ object AlarmStore {
6
+ private fun prefs(ctx: Context) = ctx.getSharedPreferences("rn_alarms", Context.MODE_PRIVATE)
7
+
8
+ fun save(ctx: Context, a: AlarmData) = prefs(ctx).edit().putString(a.id, a.toJson()).apply()
9
+ fun get(ctx: Context, id: String) = prefs(ctx).getString(id, null)?.let { AlarmData.fromJson(it) }
10
+ fun all(ctx: Context) = prefs(ctx).all.values.mapNotNull { (it as? String)?.let(AlarmData::fromJson) }
11
+ fun remove(ctx: Context, id: String) = prefs(ctx).edit().remove(id).apply()
12
+ fun clear(ctx: Context) = prefs(ctx).edit().clear().apply()
13
+ }
@@ -0,0 +1,29 @@
1
+ package com.alarm
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.util.Log
7
+
8
+ /**
9
+ * AlarmManager registrations do not survive a reboot (or an app update, or a clock
10
+ * change that could make a computed trigger wrong). This receiver re-arms them all.
11
+ */
12
+ class BootReceiver : BroadcastReceiver() {
13
+
14
+ override fun onReceive(context: Context, intent: Intent) {
15
+ when (intent.action) {
16
+ Intent.ACTION_BOOT_COMPLETED,
17
+ Intent.ACTION_MY_PACKAGE_REPLACED,
18
+ Intent.ACTION_TIME_CHANGED,
19
+ Intent.ACTION_TIMEZONE_CHANGED -> {
20
+ Log.i(TAG, "Re-arming alarms after ${intent.action}")
21
+ AlarmScheduler.scheduleAll(context)
22
+ }
23
+ }
24
+ }
25
+
26
+ companion object {
27
+ private const val TAG = "BootReceiver"
28
+ }
29
+ }
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Copyright (C) 2014 The Android Open Source Project
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ -->
16
+ <inset xmlns:android="http://schemas.android.com/apk/res/android"
17
+ android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
18
+ android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
19
+ android:insetTop="@dimen/abc_edit_text_inset_top_material"
20
+ android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
21
+ >
22
+
23
+ <selector>
24
+ <!--
25
+ This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
26
+ The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
27
+ NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
28
+
29
+ <item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
30
+
31
+ For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
32
+ -->
33
+ <item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
34
+ <item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
35
+ </selector>
36
+
37
+ </inset>
@@ -0,0 +1,3 @@
1
+ <resources>
2
+ <string name="app_name">ReactNativeAlarm</string>
3
+ </resources>
@@ -0,0 +1,9 @@
1
+ <resources>
2
+
3
+ <!-- Base application theme. -->
4
+ <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
5
+ <!-- Customize your theme here. -->
6
+ <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
7
+ </style>
8
+
9
+ </resources>
@@ -0,0 +1,21 @@
1
+ buildscript {
2
+ ext {
3
+ buildToolsVersion = "37.0.0"
4
+ minSdkVersion = 24
5
+ compileSdkVersion = 37
6
+ targetSdkVersion = 36
7
+ ndkVersion = "27.1.12297006"
8
+ kotlinVersion = "2.2.0"
9
+ }
10
+ repositories {
11
+ google()
12
+ mavenCentral()
13
+ }
14
+ dependencies {
15
+ classpath("com.android.tools.build:gradle")
16
+ classpath("com.facebook.react:react-native-gradle-plugin")
17
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
18
+ }
19
+ }
20
+
21
+ apply plugin: "com.facebook.react.rootproject"
@@ -0,0 +1,7 @@
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
4
+ networkTimeout=10000
5
+ validateDistributionUrl=true
6
+ zipStoreBase=GRADLE_USER_HOME
7
+ zipStorePath=wrapper/dists