react-native-alarmageddon 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/android/build.gradle +69 -0
- package/android/gradle.properties +13 -0
- package/android/src/main/AndroidManifest.xml +55 -0
- package/android/src/main/java/com/rnalarmmodule/AlarmActivity.kt +79 -0
- package/android/src/main/java/com/rnalarmmodule/AlarmModule.kt +485 -0
- package/android/src/main/java/com/rnalarmmodule/AlarmPackage.kt +17 -0
- package/android/src/main/java/com/rnalarmmodule/AlarmReceiver.kt +183 -0
- package/android/src/main/java/com/rnalarmmodule/AlarmService.kt +290 -0
- package/android/src/main/java/com/rnalarmmodule/BootReceiver.kt +156 -0
- package/android/src/main/res/raw/README.md +36 -0
- package/ios/AlarmModule.swift +79 -0
- package/ios/RNAlarmModule-Bridging-Header.h +3 -0
- package/ios/RNAlarmModule.m +48 -0
- package/lib/index.d.ts +147 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +189 -0
- package/lib/index.js.map +1 -0
- package/package.json +63 -0
- package/react-native-alarmageddon.podspec +25 -0
- package/react-native.config.js +12 -0
- package/src/index.ts +317 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
package com.rnalarmmodule
|
|
2
|
+
|
|
3
|
+
import android.app.AlarmManager
|
|
4
|
+
import android.app.PendingIntent
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.SharedPreferences
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.provider.Settings
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import com.facebook.react.bridge.*
|
|
12
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
|
|
13
|
+
import org.json.JSONArray
|
|
14
|
+
import org.json.JSONObject
|
|
15
|
+
import java.lang.ref.WeakReference
|
|
16
|
+
import java.text.SimpleDateFormat
|
|
17
|
+
import java.util.*
|
|
18
|
+
|
|
19
|
+
class AlarmModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
20
|
+
|
|
21
|
+
companion object {
|
|
22
|
+
private const val TAG = "AlarmModule"
|
|
23
|
+
private const val PREFS_NAME = "rn_alarm_module_alarms"
|
|
24
|
+
private const val ALARMS_KEY = "alarms"
|
|
25
|
+
private const val DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"
|
|
26
|
+
private const val ACTIVE_ALARM_KEY = "active_alarm_id"
|
|
27
|
+
|
|
28
|
+
// Weak reference to avoid memory leaks
|
|
29
|
+
private var reactContextRef: WeakReference<ReactApplicationContext>? = null
|
|
30
|
+
|
|
31
|
+
fun getReactContext(): ReactApplicationContext? = reactContextRef?.get()
|
|
32
|
+
|
|
33
|
+
fun emitActiveAlarmId(id: String?) {
|
|
34
|
+
val reactContext = getReactContext()
|
|
35
|
+
if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
|
|
36
|
+
try {
|
|
37
|
+
reactContext
|
|
38
|
+
.getJSModule(RCTDeviceEventEmitter::class.java)
|
|
39
|
+
.emit("activeAlarmId", id)
|
|
40
|
+
} catch (e: Exception) {
|
|
41
|
+
Log.e(TAG, "Failed to emit activeAlarmId event: ${e.message}")
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private val prefs: SharedPreferences = reactContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
48
|
+
private val dateFormat = SimpleDateFormat(DATE_PATTERN, Locale.getDefault())
|
|
49
|
+
|
|
50
|
+
init {
|
|
51
|
+
reactContextRef = WeakReference(reactContext)
|
|
52
|
+
dateFormat.timeZone = TimeZone.getDefault()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override fun getName(): String = "AlarmModule"
|
|
56
|
+
|
|
57
|
+
override fun getConstants(): Map<String, Any> {
|
|
58
|
+
return mapOf(
|
|
59
|
+
"EXACT_ALARM_PERMISSION_REQUIRED" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@ReactMethod
|
|
64
|
+
fun addListener(eventName: String) {
|
|
65
|
+
// Required for NativeEventEmitter
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@ReactMethod
|
|
69
|
+
fun removeListeners(count: Int) {
|
|
70
|
+
// Required for NativeEventEmitter
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@ReactMethod
|
|
74
|
+
fun scheduleAlarm(alarmParams: ReadableMap, promise: Promise) {
|
|
75
|
+
try {
|
|
76
|
+
val id = alarmParams.getString("id") ?: run {
|
|
77
|
+
promise.reject("INVALID_PARAMS", "Alarm ID is required")
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
val datetimeISO = alarmParams.getString("datetimeISO") ?: run {
|
|
81
|
+
promise.reject("INVALID_PARAMS", "datetimeISO is required")
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
val title = alarmParams.getString("title") ?: "Alarm"
|
|
85
|
+
val body = alarmParams.getString("body") ?: ""
|
|
86
|
+
val soundUri = if (alarmParams.hasKey("soundUri")) alarmParams.getString("soundUri") else null
|
|
87
|
+
val vibrate = if (alarmParams.hasKey("vibrate")) alarmParams.getBoolean("vibrate") else true
|
|
88
|
+
val snoozeMinutes = if (alarmParams.hasKey("snoozeMinutes")) alarmParams.getInt("snoozeMinutes") else 5
|
|
89
|
+
val autoStopSeconds = if (alarmParams.hasKey("autoStopSeconds")) alarmParams.getInt("autoStopSeconds") else 60
|
|
90
|
+
|
|
91
|
+
val triggerTime = parseDateTime(datetimeISO)
|
|
92
|
+
if (triggerTime == null) {
|
|
93
|
+
promise.reject("INVALID_DATE", "Could not parse datetimeISO: $datetimeISO")
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Store alarm in SharedPreferences
|
|
98
|
+
val alarm = JSONObject().apply {
|
|
99
|
+
put("id", id)
|
|
100
|
+
put("datetimeISO", datetimeISO)
|
|
101
|
+
put("triggerTime", triggerTime)
|
|
102
|
+
put("title", title)
|
|
103
|
+
put("body", body)
|
|
104
|
+
put("soundUri", soundUri ?: "")
|
|
105
|
+
put("vibrate", vibrate)
|
|
106
|
+
put("snoozeMinutes", snoozeMinutes)
|
|
107
|
+
put("autoStopSeconds", autoStopSeconds)
|
|
108
|
+
}
|
|
109
|
+
saveAlarm(alarm)
|
|
110
|
+
|
|
111
|
+
// Schedule the alarm
|
|
112
|
+
scheduleExactAlarm(id, triggerTime, title, body, soundUri, vibrate, snoozeMinutes, autoStopSeconds)
|
|
113
|
+
|
|
114
|
+
Log.d(TAG, "Alarm scheduled: id=$id, triggerTime=$triggerTime")
|
|
115
|
+
promise.resolve(null)
|
|
116
|
+
} catch (e: Exception) {
|
|
117
|
+
Log.e(TAG, "Failed to schedule alarm: ${e.message}", e)
|
|
118
|
+
promise.reject("SCHEDULE_ERROR", "Failed to schedule alarm: ${e.message}", e)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@ReactMethod
|
|
123
|
+
fun cancelAlarm(id: String, promise: Promise) {
|
|
124
|
+
try {
|
|
125
|
+
cancelScheduledAlarm(id)
|
|
126
|
+
removeAlarm(id)
|
|
127
|
+
Log.d(TAG, "Alarm cancelled: id=$id")
|
|
128
|
+
promise.resolve(null)
|
|
129
|
+
} catch (e: Exception) {
|
|
130
|
+
Log.e(TAG, "Failed to cancel alarm: ${e.message}", e)
|
|
131
|
+
promise.reject("CANCEL_ERROR", "Failed to cancel alarm: ${e.message}", e)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@ReactMethod
|
|
136
|
+
fun listAlarms(promise: Promise) {
|
|
137
|
+
try {
|
|
138
|
+
val alarms = getStoredAlarms()
|
|
139
|
+
val result = Arguments.createArray()
|
|
140
|
+
for (i in 0 until alarms.length()) {
|
|
141
|
+
val alarm = alarms.getJSONObject(i)
|
|
142
|
+
val map = Arguments.createMap().apply {
|
|
143
|
+
putString("id", alarm.getString("id"))
|
|
144
|
+
putString("datetimeISO", alarm.getString("datetimeISO"))
|
|
145
|
+
putString("title", alarm.optString("title", "Alarm"))
|
|
146
|
+
putString("body", alarm.optString("body", ""))
|
|
147
|
+
putBoolean("vibrate", alarm.optBoolean("vibrate", true))
|
|
148
|
+
putInt("snoozeMinutes", alarm.optInt("snoozeMinutes", 5))
|
|
149
|
+
putInt("autoStopSeconds", alarm.optInt("autoStopSeconds", 60))
|
|
150
|
+
}
|
|
151
|
+
result.pushMap(map)
|
|
152
|
+
}
|
|
153
|
+
promise.resolve(result)
|
|
154
|
+
} catch (e: Exception) {
|
|
155
|
+
Log.e(TAG, "Failed to list alarms: ${e.message}", e)
|
|
156
|
+
promise.reject("LIST_ERROR", "Failed to list alarms: ${e.message}", e)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@ReactMethod
|
|
161
|
+
fun requestPermissions(promise: Promise) {
|
|
162
|
+
try {
|
|
163
|
+
val result = Arguments.createMap()
|
|
164
|
+
|
|
165
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
166
|
+
val alarmManager = reactApplicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
167
|
+
val canScheduleExact = alarmManager.canScheduleExactAlarms()
|
|
168
|
+
result.putBoolean("granted", canScheduleExact)
|
|
169
|
+
result.putBoolean("exactAlarmGranted", canScheduleExact)
|
|
170
|
+
|
|
171
|
+
if (!canScheduleExact) {
|
|
172
|
+
// Open settings to grant exact alarm permission
|
|
173
|
+
try {
|
|
174
|
+
val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
|
|
175
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
176
|
+
}
|
|
177
|
+
reactApplicationContext.startActivity(intent)
|
|
178
|
+
} catch (e: Exception) {
|
|
179
|
+
Log.e(TAG, "Failed to open exact alarm settings: ${e.message}")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
result.putBoolean("granted", true)
|
|
184
|
+
result.putBoolean("exactAlarmGranted", true)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
promise.resolve(result)
|
|
188
|
+
} catch (e: Exception) {
|
|
189
|
+
Log.e(TAG, "Failed to request permissions: ${e.message}", e)
|
|
190
|
+
promise.reject("PERMISSION_ERROR", "Failed to request permissions: ${e.message}", e)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@ReactMethod
|
|
195
|
+
fun checkExactAlarmPermission(promise: Promise) {
|
|
196
|
+
try {
|
|
197
|
+
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
198
|
+
val alarmManager = reactApplicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
199
|
+
alarmManager.canScheduleExactAlarms()
|
|
200
|
+
} else {
|
|
201
|
+
true
|
|
202
|
+
}
|
|
203
|
+
promise.resolve(result)
|
|
204
|
+
} catch (e: Exception) {
|
|
205
|
+
Log.e(TAG, "Failed to check exact alarm permission: ${e.message}", e)
|
|
206
|
+
promise.reject("PERMISSION_ERROR", "Failed to check permission: ${e.message}", e)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@ReactMethod
|
|
211
|
+
fun openExactAlarmSettings(promise: Promise) {
|
|
212
|
+
try {
|
|
213
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
214
|
+
val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
|
|
215
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
216
|
+
}
|
|
217
|
+
reactApplicationContext.startActivity(intent)
|
|
218
|
+
}
|
|
219
|
+
promise.resolve(null)
|
|
220
|
+
} catch (e: Exception) {
|
|
221
|
+
Log.e(TAG, "Failed to open exact alarm settings: ${e.message}", e)
|
|
222
|
+
promise.reject("SETTINGS_ERROR", "Failed to open settings: ${e.message}", e)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@ReactMethod
|
|
227
|
+
fun snoozeAlarm(id: String, minutes: Int, promise: Promise) {
|
|
228
|
+
try {
|
|
229
|
+
val alarm = getAlarmById(id)
|
|
230
|
+
if (alarm == null) {
|
|
231
|
+
promise.reject("NOT_FOUND", "Alarm not found: $id")
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
val snoozeTime = System.currentTimeMillis() + (minutes * 60 * 1000L)
|
|
236
|
+
val newDatetimeISO = dateFormat.format(Date(snoozeTime))
|
|
237
|
+
|
|
238
|
+
// Update alarm with new time
|
|
239
|
+
alarm.put("datetimeISO", newDatetimeISO)
|
|
240
|
+
alarm.put("triggerTime", snoozeTime)
|
|
241
|
+
updateAlarm(alarm)
|
|
242
|
+
|
|
243
|
+
// Reschedule
|
|
244
|
+
scheduleExactAlarm(
|
|
245
|
+
id,
|
|
246
|
+
snoozeTime,
|
|
247
|
+
alarm.optString("title", "Alarm"),
|
|
248
|
+
alarm.optString("body", ""),
|
|
249
|
+
alarm.optString("soundUri", null),
|
|
250
|
+
alarm.optBoolean("vibrate", true),
|
|
251
|
+
alarm.optInt("snoozeMinutes", 5),
|
|
252
|
+
alarm.optInt("autoStopSeconds", 60)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
Log.d(TAG, "Alarm snoozed: id=$id, new trigger=$snoozeTime")
|
|
256
|
+
promise.resolve(null)
|
|
257
|
+
} catch (e: Exception) {
|
|
258
|
+
Log.e(TAG, "Failed to snooze alarm: ${e.message}", e)
|
|
259
|
+
promise.reject("SNOOZE_ERROR", "Failed to snooze alarm: ${e.message}", e)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@ReactMethod
|
|
264
|
+
fun stopCurrentAlarm(id: String, promise: Promise) {
|
|
265
|
+
try {
|
|
266
|
+
// Stop the alarm service
|
|
267
|
+
val stopIntent = Intent(reactApplicationContext, AlarmService::class.java).apply {
|
|
268
|
+
action = AlarmService.ACTION_STOP
|
|
269
|
+
putExtra("alarmId", id)
|
|
270
|
+
}
|
|
271
|
+
reactApplicationContext.stopService(stopIntent)
|
|
272
|
+
|
|
273
|
+
// Clear active alarm state
|
|
274
|
+
prefs.edit().remove(ACTIVE_ALARM_KEY).apply()
|
|
275
|
+
emitActiveAlarmId(null)
|
|
276
|
+
|
|
277
|
+
Log.d(TAG, "Alarm stopped: id=$id")
|
|
278
|
+
promise.resolve(null)
|
|
279
|
+
} catch (e: Exception) {
|
|
280
|
+
Log.e(TAG, "Failed to stop alarm: ${e.message}", e)
|
|
281
|
+
promise.reject("STOP_ERROR", "Failed to stop alarm: ${e.message}", e)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@ReactMethod
|
|
286
|
+
fun snoozeCurrentAlarm(id: String, minutes: Int, promise: Promise) {
|
|
287
|
+
try {
|
|
288
|
+
// Stop the current alarm
|
|
289
|
+
val stopIntent = Intent(reactApplicationContext, AlarmService::class.java).apply {
|
|
290
|
+
action = AlarmService.ACTION_STOP
|
|
291
|
+
putExtra("alarmId", id)
|
|
292
|
+
}
|
|
293
|
+
reactApplicationContext.stopService(stopIntent)
|
|
294
|
+
|
|
295
|
+
// Clear active alarm state
|
|
296
|
+
prefs.edit().remove(ACTIVE_ALARM_KEY).apply()
|
|
297
|
+
emitActiveAlarmId(null)
|
|
298
|
+
|
|
299
|
+
// Schedule snooze
|
|
300
|
+
snoozeAlarm(id, minutes, promise)
|
|
301
|
+
} catch (e: Exception) {
|
|
302
|
+
Log.e(TAG, "Failed to snooze current alarm: ${e.message}", e)
|
|
303
|
+
promise.reject("SNOOZE_ERROR", "Failed to snooze current alarm: ${e.message}", e)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@ReactMethod
|
|
308
|
+
fun getCurrentAlarmPlaying(promise: Promise) {
|
|
309
|
+
try {
|
|
310
|
+
val activeAlarmId = prefs.getString(ACTIVE_ALARM_KEY, null)
|
|
311
|
+
if (activeAlarmId != null) {
|
|
312
|
+
val result = Arguments.createMap().apply {
|
|
313
|
+
putString("activeAlarmId", activeAlarmId)
|
|
314
|
+
}
|
|
315
|
+
promise.resolve(result)
|
|
316
|
+
} else {
|
|
317
|
+
promise.resolve(null)
|
|
318
|
+
}
|
|
319
|
+
} catch (e: Exception) {
|
|
320
|
+
Log.e(TAG, "Failed to get current alarm: ${e.message}", e)
|
|
321
|
+
promise.reject("GET_ERROR", "Failed to get current alarm: ${e.message}", e)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private fun parseDateTime(datetimeISO: String): Long? {
|
|
326
|
+
return try {
|
|
327
|
+
// Try parsing with timezone info
|
|
328
|
+
val formats = listOf(
|
|
329
|
+
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.getDefault()),
|
|
330
|
+
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.getDefault()),
|
|
331
|
+
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()),
|
|
332
|
+
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()),
|
|
333
|
+
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault()),
|
|
334
|
+
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()),
|
|
335
|
+
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
for (format in formats) {
|
|
339
|
+
try {
|
|
340
|
+
val date = format.parse(datetimeISO)
|
|
341
|
+
if (date != null) {
|
|
342
|
+
return date.time
|
|
343
|
+
}
|
|
344
|
+
} catch (e: Exception) {
|
|
345
|
+
// Try next format
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
null
|
|
349
|
+
} catch (e: Exception) {
|
|
350
|
+
Log.e(TAG, "Failed to parse date: $datetimeISO", e)
|
|
351
|
+
null
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private fun scheduleExactAlarm(
|
|
356
|
+
id: String,
|
|
357
|
+
triggerTimeMillis: Long,
|
|
358
|
+
title: String,
|
|
359
|
+
body: String,
|
|
360
|
+
soundUri: String?,
|
|
361
|
+
vibrate: Boolean,
|
|
362
|
+
snoozeMinutes: Int,
|
|
363
|
+
autoStopSeconds: Int
|
|
364
|
+
) {
|
|
365
|
+
val alarmManager = reactApplicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
366
|
+
|
|
367
|
+
val intent = Intent(reactApplicationContext, AlarmReceiver::class.java).apply {
|
|
368
|
+
action = "com.rnalarmmodule.ALARM_TRIGGER"
|
|
369
|
+
putExtra("alarmId", id)
|
|
370
|
+
putExtra("title", title)
|
|
371
|
+
putExtra("body", body)
|
|
372
|
+
putExtra("soundUri", soundUri ?: "")
|
|
373
|
+
putExtra("vibrate", vibrate)
|
|
374
|
+
putExtra("snoozeMinutes", snoozeMinutes)
|
|
375
|
+
putExtra("autoStopSeconds", autoStopSeconds)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
val pendingIntent = PendingIntent.getBroadcast(
|
|
379
|
+
reactApplicationContext,
|
|
380
|
+
id.hashCode(),
|
|
381
|
+
intent,
|
|
382
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
387
|
+
if (alarmManager.canScheduleExactAlarms()) {
|
|
388
|
+
alarmManager.setAlarmClock(
|
|
389
|
+
AlarmManager.AlarmClockInfo(triggerTimeMillis, pendingIntent),
|
|
390
|
+
pendingIntent
|
|
391
|
+
)
|
|
392
|
+
} else {
|
|
393
|
+
// Fallback to inexact alarm if exact permission not granted
|
|
394
|
+
alarmManager.setAndAllowWhileIdle(
|
|
395
|
+
AlarmManager.RTC_WAKEUP,
|
|
396
|
+
triggerTimeMillis,
|
|
397
|
+
pendingIntent
|
|
398
|
+
)
|
|
399
|
+
Log.w(TAG, "Exact alarm permission not granted, using inexact alarm")
|
|
400
|
+
}
|
|
401
|
+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
402
|
+
alarmManager.setAlarmClock(
|
|
403
|
+
AlarmManager.AlarmClockInfo(triggerTimeMillis, pendingIntent),
|
|
404
|
+
pendingIntent
|
|
405
|
+
)
|
|
406
|
+
} else {
|
|
407
|
+
alarmManager.setExact(
|
|
408
|
+
AlarmManager.RTC_WAKEUP,
|
|
409
|
+
triggerTimeMillis,
|
|
410
|
+
pendingIntent
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
} catch (e: SecurityException) {
|
|
414
|
+
Log.e(TAG, "Security exception scheduling alarm: ${e.message}", e)
|
|
415
|
+
throw e
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private fun cancelScheduledAlarm(id: String) {
|
|
420
|
+
val alarmManager = reactApplicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
421
|
+
|
|
422
|
+
val intent = Intent(reactApplicationContext, AlarmReceiver::class.java).apply {
|
|
423
|
+
action = "com.rnalarmmodule.ALARM_TRIGGER"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
val pendingIntent = PendingIntent.getBroadcast(
|
|
427
|
+
reactApplicationContext,
|
|
428
|
+
id.hashCode(),
|
|
429
|
+
intent,
|
|
430
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
alarmManager.cancel(pendingIntent)
|
|
434
|
+
pendingIntent.cancel()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private fun getStoredAlarms(): JSONArray {
|
|
438
|
+
val alarmsJson = prefs.getString(ALARMS_KEY, "[]") ?: "[]"
|
|
439
|
+
return JSONArray(alarmsJson)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private fun saveAlarm(alarm: JSONObject) {
|
|
443
|
+
val alarms = getStoredAlarms()
|
|
444
|
+
val id = alarm.getString("id")
|
|
445
|
+
|
|
446
|
+
// Remove existing alarm with same ID
|
|
447
|
+
val filteredAlarms = JSONArray()
|
|
448
|
+
for (i in 0 until alarms.length()) {
|
|
449
|
+
val existingAlarm = alarms.getJSONObject(i)
|
|
450
|
+
if (existingAlarm.getString("id") != id) {
|
|
451
|
+
filteredAlarms.put(existingAlarm)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
filteredAlarms.put(alarm)
|
|
456
|
+
prefs.edit().putString(ALARMS_KEY, filteredAlarms.toString()).apply()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private fun updateAlarm(alarm: JSONObject) {
|
|
460
|
+
saveAlarm(alarm)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private fun removeAlarm(id: String) {
|
|
464
|
+
val alarms = getStoredAlarms()
|
|
465
|
+
val filteredAlarms = JSONArray()
|
|
466
|
+
for (i in 0 until alarms.length()) {
|
|
467
|
+
val existingAlarm = alarms.getJSONObject(i)
|
|
468
|
+
if (existingAlarm.getString("id") != id) {
|
|
469
|
+
filteredAlarms.put(existingAlarm)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
prefs.edit().putString(ALARMS_KEY, filteredAlarms.toString()).apply()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private fun getAlarmById(id: String): JSONObject? {
|
|
476
|
+
val alarms = getStoredAlarms()
|
|
477
|
+
for (i in 0 until alarms.length()) {
|
|
478
|
+
val alarm = alarms.getJSONObject(i)
|
|
479
|
+
if (alarm.getString("id") == id) {
|
|
480
|
+
return alarm
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.rnalarmmodule
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class AlarmPackage : ReactPackage {
|
|
9
|
+
|
|
10
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
11
|
+
return listOf(AlarmModule(reactContext))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
15
|
+
return emptyList()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
package com.rnalarmmodule
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.os.PowerManager
|
|
8
|
+
import android.util.Log
|
|
9
|
+
|
|
10
|
+
class AlarmReceiver : BroadcastReceiver() {
|
|
11
|
+
|
|
12
|
+
companion object {
|
|
13
|
+
private const val TAG = "AlarmReceiver"
|
|
14
|
+
const val ACTION_TRIGGER = "com.rnalarmmodule.ALARM_TRIGGER"
|
|
15
|
+
const val ACTION_STOP = "com.rnalarmmodule.ALARM_STOP"
|
|
16
|
+
const val ACTION_SNOOZE = "com.rnalarmmodule.ALARM_SNOOZE"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
20
|
+
Log.d(TAG, "onReceive: action=${intent.action}")
|
|
21
|
+
|
|
22
|
+
// Acquire wake lock to ensure device stays awake
|
|
23
|
+
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
24
|
+
val wakeLock = powerManager.newWakeLock(
|
|
25
|
+
PowerManager.PARTIAL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
26
|
+
"AlarmModule:AlarmWakeLock"
|
|
27
|
+
)
|
|
28
|
+
wakeLock.acquire(60 * 1000L) // 60 seconds max
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
when (intent.action) {
|
|
32
|
+
ACTION_TRIGGER -> handleAlarmTrigger(context, intent)
|
|
33
|
+
ACTION_STOP -> handleAlarmStop(context, intent)
|
|
34
|
+
ACTION_SNOOZE -> handleAlarmSnooze(context, intent)
|
|
35
|
+
else -> {
|
|
36
|
+
Log.w(TAG, "Unknown action: ${intent.action}")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (e: Exception) {
|
|
40
|
+
Log.e(TAG, "Error handling alarm: ${e.message}", e)
|
|
41
|
+
} finally {
|
|
42
|
+
if (wakeLock.isHeld) {
|
|
43
|
+
wakeLock.release()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private fun handleAlarmTrigger(context: Context, intent: Intent) {
|
|
49
|
+
val alarmId = intent.getStringExtra("alarmId") ?: return
|
|
50
|
+
val title = intent.getStringExtra("title") ?: "Alarm"
|
|
51
|
+
val body = intent.getStringExtra("body") ?: ""
|
|
52
|
+
val soundUri = intent.getStringExtra("soundUri") ?: ""
|
|
53
|
+
val vibrate = intent.getBooleanExtra("vibrate", true)
|
|
54
|
+
val snoozeMinutes = intent.getIntExtra("snoozeMinutes", 5)
|
|
55
|
+
val autoStopSeconds = intent.getIntExtra("autoStopSeconds", 60)
|
|
56
|
+
|
|
57
|
+
Log.d(TAG, "Alarm triggered: id=$alarmId, title=$title")
|
|
58
|
+
|
|
59
|
+
// Store active alarm ID in SharedPreferences
|
|
60
|
+
val prefs = context.getSharedPreferences("rn_alarm_module_alarms", Context.MODE_PRIVATE)
|
|
61
|
+
prefs.edit().putString("active_alarm_id", alarmId).apply()
|
|
62
|
+
|
|
63
|
+
// Start the alarm service
|
|
64
|
+
val serviceIntent = Intent(context, AlarmService::class.java).apply {
|
|
65
|
+
action = AlarmService.ACTION_START
|
|
66
|
+
putExtra("alarmId", alarmId)
|
|
67
|
+
putExtra("title", title)
|
|
68
|
+
putExtra("body", body)
|
|
69
|
+
putExtra("soundUri", soundUri)
|
|
70
|
+
putExtra("vibrate", vibrate)
|
|
71
|
+
putExtra("snoozeMinutes", snoozeMinutes)
|
|
72
|
+
putExtra("autoStopSeconds", autoStopSeconds)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
76
|
+
context.startForegroundService(serviceIntent)
|
|
77
|
+
} else {
|
|
78
|
+
context.startService(serviceIntent)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try to emit event to React Native
|
|
82
|
+
AlarmModule.emitActiveAlarmId(alarmId)
|
|
83
|
+
|
|
84
|
+
// Launch full-screen activity to wake up screen
|
|
85
|
+
launchAlarmActivity(context, alarmId, title, body, snoozeMinutes)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun handleAlarmStop(context: Context, intent: Intent) {
|
|
89
|
+
val alarmId = intent.getStringExtra("alarmId") ?: return
|
|
90
|
+
Log.d(TAG, "Stopping alarm: id=$alarmId")
|
|
91
|
+
|
|
92
|
+
// Stop the alarm service
|
|
93
|
+
val serviceIntent = Intent(context, AlarmService::class.java).apply {
|
|
94
|
+
action = AlarmService.ACTION_STOP
|
|
95
|
+
putExtra("alarmId", alarmId)
|
|
96
|
+
}
|
|
97
|
+
context.stopService(serviceIntent)
|
|
98
|
+
|
|
99
|
+
// Clear active alarm state
|
|
100
|
+
val prefs = context.getSharedPreferences("rn_alarm_module_alarms", Context.MODE_PRIVATE)
|
|
101
|
+
prefs.edit().remove("active_alarm_id").apply()
|
|
102
|
+
|
|
103
|
+
// Emit null to indicate alarm stopped
|
|
104
|
+
AlarmModule.emitActiveAlarmId(null)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun handleAlarmSnooze(context: Context, intent: Intent) {
|
|
108
|
+
val alarmId = intent.getStringExtra("alarmId") ?: return
|
|
109
|
+
val snoozeMinutes = intent.getIntExtra("snoozeMinutes", 5)
|
|
110
|
+
Log.d(TAG, "Snoozing alarm: id=$alarmId, minutes=$snoozeMinutes")
|
|
111
|
+
|
|
112
|
+
// Stop current alarm
|
|
113
|
+
handleAlarmStop(context, intent)
|
|
114
|
+
|
|
115
|
+
// Reschedule the alarm
|
|
116
|
+
val snoozeTime = System.currentTimeMillis() + (snoozeMinutes * 60 * 1000L)
|
|
117
|
+
|
|
118
|
+
val triggerIntent = Intent(context, AlarmReceiver::class.java).apply {
|
|
119
|
+
action = ACTION_TRIGGER
|
|
120
|
+
putExtra("alarmId", alarmId)
|
|
121
|
+
putExtra("title", intent.getStringExtra("title") ?: "Alarm")
|
|
122
|
+
putExtra("body", intent.getStringExtra("body") ?: "")
|
|
123
|
+
putExtra("soundUri", intent.getStringExtra("soundUri") ?: "")
|
|
124
|
+
putExtra("vibrate", intent.getBooleanExtra("vibrate", true))
|
|
125
|
+
putExtra("snoozeMinutes", snoozeMinutes)
|
|
126
|
+
putExtra("autoStopSeconds", intent.getIntExtra("autoStopSeconds", 60))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
val pendingIntent = android.app.PendingIntent.getBroadcast(
|
|
130
|
+
context,
|
|
131
|
+
alarmId.hashCode(),
|
|
132
|
+
triggerIntent,
|
|
133
|
+
android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as android.app.AlarmManager
|
|
137
|
+
|
|
138
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
139
|
+
if (alarmManager.canScheduleExactAlarms()) {
|
|
140
|
+
alarmManager.setAlarmClock(
|
|
141
|
+
android.app.AlarmManager.AlarmClockInfo(snoozeTime, pendingIntent),
|
|
142
|
+
pendingIntent
|
|
143
|
+
)
|
|
144
|
+
} else {
|
|
145
|
+
alarmManager.setAndAllowWhileIdle(
|
|
146
|
+
android.app.AlarmManager.RTC_WAKEUP,
|
|
147
|
+
snoozeTime,
|
|
148
|
+
pendingIntent
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
152
|
+
alarmManager.setAlarmClock(
|
|
153
|
+
android.app.AlarmManager.AlarmClockInfo(snoozeTime, pendingIntent),
|
|
154
|
+
pendingIntent
|
|
155
|
+
)
|
|
156
|
+
} else {
|
|
157
|
+
alarmManager.setExact(
|
|
158
|
+
android.app.AlarmManager.RTC_WAKEUP,
|
|
159
|
+
snoozeTime,
|
|
160
|
+
pendingIntent
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
Log.d(TAG, "Alarm snoozed and rescheduled for: $snoozeTime")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private fun launchAlarmActivity(context: Context, alarmId: String, title: String, body: String, snoozeMinutes: Int) {
|
|
168
|
+
try {
|
|
169
|
+
val activityIntent = Intent(context, AlarmActivity::class.java).apply {
|
|
170
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
171
|
+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
172
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
173
|
+
putExtra("alarmId", alarmId)
|
|
174
|
+
putExtra("title", title)
|
|
175
|
+
putExtra("body", body)
|
|
176
|
+
putExtra("snoozeMinutes", snoozeMinutes)
|
|
177
|
+
}
|
|
178
|
+
context.startActivity(activityIntent)
|
|
179
|
+
} catch (e: Exception) {
|
|
180
|
+
Log.e(TAG, "Failed to launch alarm activity: ${e.message}", e)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|