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,119 @@
1
+ apply plugin: "com.android.application"
2
+ apply plugin: "org.jetbrains.kotlin.android"
3
+ apply plugin: "com.facebook.react"
4
+
5
+ /**
6
+ * This is the configuration block to customize your React Native Android app.
7
+ * By default you don't need to apply any configuration, just uncomment the lines you need.
8
+ */
9
+ react {
10
+ /* Folders */
11
+ // The root of your project, i.e. where "package.json" lives. Default is '../..'
12
+ // root = file("../../")
13
+ // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
14
+ // reactNativeDir = file("../../node_modules/react-native")
15
+ // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
16
+ // codegenDir = file("../../node_modules/@react-native/codegen")
17
+ // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
18
+ // cliFile = file("../../node_modules/react-native/cli.js")
19
+
20
+ /* Variants */
21
+ // The list of variants to that are debuggable. For those we're going to
22
+ // skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized".
23
+ // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
24
+ // debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"]
25
+
26
+ /* Bundling */
27
+ // A list containing the node command and its flags. Default is just 'node'.
28
+ // nodeExecutableAndArgs = ["node"]
29
+ //
30
+ // The command to run when bundling. By default is 'bundle'
31
+ // bundleCommand = "ram-bundle"
32
+ //
33
+ // The path to the CLI configuration file. Default is empty.
34
+ // bundleConfig = file(../rn-cli.config.js)
35
+ //
36
+ // The name of the generated asset file containing your JS bundle
37
+ // bundleAssetName = "MyApplication.android.bundle"
38
+ //
39
+ // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
40
+ // entryFile = file("../js/MyApplication.android.js")
41
+ //
42
+ // A list of extra flags to pass to the 'bundle' commands.
43
+ // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
44
+ // extraPackagerArgs = []
45
+
46
+ /* Hermes Commands */
47
+ // The hermes compiler command to run. By default it is 'hermesc'
48
+ // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
49
+ //
50
+ // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
51
+ // hermesFlags = ["-O", "-output-source-map"]
52
+
53
+ /* Autolinking */
54
+ autolinkLibrariesWithApp()
55
+ }
56
+
57
+ /**
58
+ * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
59
+ */
60
+ def enableProguardInReleaseBuilds = false
61
+
62
+ /**
63
+ * The preferred build flavor of JavaScriptCore (JSC)
64
+ *
65
+ * For example, to use the international variant, you can use:
66
+ * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
67
+ *
68
+ * The international variant includes ICU i18n library and necessary data
69
+ * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
70
+ * give correct results when using with locales other than en-US. Note that
71
+ * this variant is about 6MiB larger per architecture than default.
72
+ */
73
+ def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
74
+
75
+ android {
76
+ ndkVersion rootProject.ext.ndkVersion
77
+ buildToolsVersion rootProject.ext.buildToolsVersion
78
+ compileSdk rootProject.ext.compileSdkVersion
79
+
80
+ namespace "com.reactnativealarm"
81
+ defaultConfig {
82
+ applicationId "com.reactnativealarm"
83
+ minSdkVersion rootProject.ext.minSdkVersion
84
+ targetSdkVersion rootProject.ext.targetSdkVersion
85
+ versionCode 1
86
+ versionName "1.0"
87
+ }
88
+ signingConfigs {
89
+ debug {
90
+ storeFile file('debug.keystore')
91
+ storePassword 'android'
92
+ keyAlias 'androiddebugkey'
93
+ keyPassword 'android'
94
+ }
95
+ }
96
+ buildTypes {
97
+ debug {
98
+ signingConfig signingConfigs.debug
99
+ }
100
+ release {
101
+ // Caution! In production, you need to generate your own keystore file.
102
+ // see https://reactnative.dev/docs/signed-apk-android.
103
+ signingConfig signingConfigs.debug
104
+ minifyEnabled enableProguardInReleaseBuilds
105
+ proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
106
+ }
107
+ }
108
+ }
109
+
110
+ dependencies {
111
+ // The version of react-native is set by the React Native Gradle Plugin
112
+ implementation("com.facebook.react:react-android")
113
+
114
+ if (hermesEnabled.toBoolean()) {
115
+ implementation("com.facebook.react:hermes-android")
116
+ } else {
117
+ implementation jscFlavor
118
+ }
119
+ }
Binary file
@@ -0,0 +1,10 @@
1
+ # Add project specific ProGuard rules here.
2
+ # By default, the flags in this file are appended to flags specified
3
+ # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4
+ # You can edit the include path and order by changing the proguardFiles
5
+ # directive in build.gradle.
6
+ #
7
+ # For more details, see
8
+ # http://developer.android.com/guide/developing/tools/proguard.html
9
+
10
+ # Add any project specific keep options here:
@@ -0,0 +1,81 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+
3
+ <uses-permission android:name="android.permission.INTERNET" />
4
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
5
+ <!-- Exact alarms: USE_EXACT_ALARM is granted automatically to alarm-clock apps on
6
+ Android 13+; SCHEDULE_EXACT_ALARM covers Android 12 (user-grantable). -->
7
+ <uses-permission android:name="android.permission.USE_EXACT_ALARM" />
8
+ <uses-permission
9
+ android:name="android.permission.SCHEDULE_EXACT_ALARM"
10
+ android:maxSdkVersion="32" />
11
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
12
+ <uses-permission android:name="android.permission.VIBRATE" />
13
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
14
+ <!-- Lets the ringing service pop the alarm screen instantly from the background;
15
+ the full-screen notification alone is unreliable on many devices. -->
16
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
17
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
18
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
19
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
20
+
21
+ <application
22
+ android:name=".MainApplication"
23
+ android:label="@string/app_name"
24
+ android:icon="@mipmap/ic_launcher"
25
+ android:roundIcon="@mipmap/ic_launcher_round"
26
+ android:allowBackup="false"
27
+ android:theme="@style/AppTheme"
28
+ android:usesCleartextTraffic="${usesCleartextTraffic}"
29
+ android:supportsRtl="true">
30
+ <activity
31
+ android:name=".MainActivity"
32
+ android:label="@string/app_name"
33
+ android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
34
+ android:launchMode="singleTask"
35
+ android:windowSoftInputMode="adjustResize"
36
+ android:exported="true">
37
+ <intent-filter>
38
+ <action android:name="android.intent.action.MAIN" />
39
+ <category android:name="android.intent.category.LAUNCHER" />
40
+ </intent-filter>
41
+ </activity>
42
+
43
+ <!-- Shown over the lock screen while the alarm rings. excludeFromRecents +
44
+ taskAffinity keep it out of the user's app history / back stack. -->
45
+ <activity
46
+ android:name="com.alarm.AlarmRingActivity"
47
+ android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
48
+ android:excludeFromRecents="true"
49
+ android:exported="false"
50
+ android:launchMode="singleInstance"
51
+ android:showWhenLocked="true"
52
+ android:taskAffinity=""
53
+ android:theme="@style/AppTheme"
54
+ android:turnScreenOn="true" />
55
+
56
+ <!-- Foreground service that plays the alarm: keeps the process alive and the
57
+ sound looping no matter what happens to the UI. -->
58
+ <service
59
+ android:name="com.alarm.AlarmService"
60
+ android:exported="false"
61
+ android:foregroundServiceType="mediaPlayback" />
62
+
63
+ <!-- AlarmManager fires here at the exact trigger time. -->
64
+ <receiver
65
+ android:name="com.alarm.AlarmReceiver"
66
+ android:exported="false" />
67
+
68
+ <!-- Reschedules every enabled alarm after a reboot, an app update or a clock change. -->
69
+ <receiver
70
+ android:name="com.alarm.BootReceiver"
71
+ android:enabled="true"
72
+ android:exported="true">
73
+ <intent-filter>
74
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
75
+ <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
76
+ <action android:name="android.intent.action.TIME_SET" />
77
+ <action android:name="android.intent.action.TIMEZONE_CHANGED" />
78
+ </intent-filter>
79
+ </receiver>
80
+ </application>
81
+ </manifest>
@@ -0,0 +1,29 @@
1
+ package com.reactnativealarm
2
+
3
+ import com.facebook.react.ReactActivity
4
+ import com.facebook.react.ReactActivityDelegate
5
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
6
+ import com.facebook.react.defaults.DefaultReactActivityDelegate
7
+ import com.alarm.AlarmRingActivity
8
+
9
+ class MainActivity : ReactActivity() {
10
+
11
+ /**
12
+ * Returns the name of the main component registered from JavaScript. This is used to schedule
13
+ * rendering of the component.
14
+ */
15
+ override fun getMainComponentName(): String = "ReactNativeAlarm"
16
+
17
+ /**
18
+ * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
19
+ * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
20
+ */
21
+ override fun createReactActivityDelegate(): ReactActivityDelegate =
22
+ DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
23
+
24
+ override fun onResume() {
25
+ super.onResume()
26
+ // An alarm is ringing and the user opened the app: show the ring screen on top.
27
+ AlarmRingActivity.openIfRinging(this)
28
+ }
29
+ }
@@ -0,0 +1,29 @@
1
+ package com.reactnativealarm
2
+
3
+ import android.app.Application
4
+ import com.facebook.react.PackageList
5
+ import com.facebook.react.ReactApplication
6
+ import com.facebook.react.ReactHost
7
+ import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
8
+ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
9
+ import com.alarm.AlarmPackage
10
+
11
+ class MainApplication : Application(), ReactApplication {
12
+
13
+ override val reactHost: ReactHost by lazy {
14
+ getDefaultReactHost(
15
+ context = applicationContext,
16
+ packageList =
17
+ PackageList(this).packages.apply {
18
+ // Packages that cannot be autolinked yet can be added manually here, for example:
19
+ // add(MyReactNativePackage())
20
+ add(AlarmPackage())
21
+ },
22
+ )
23
+ }
24
+
25
+ override fun onCreate() {
26
+ super.onCreate()
27
+ loadReactNative(this)
28
+ }
29
+ }
@@ -0,0 +1,134 @@
1
+ package com.alarm
2
+
3
+ import android.os.Bundle
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.ReadableMap
6
+ import com.facebook.react.bridge.WritableMap
7
+ import org.json.JSONArray
8
+ import org.json.JSONObject
9
+ import java.util.Calendar
10
+
11
+ data class AlarmData(
12
+ val id: String,
13
+ val timestamp: Long,
14
+ val title: String,
15
+ val body: String,
16
+ val sound: String,
17
+ val vibrate: Boolean,
18
+ val snoozeEnabled: Boolean,
19
+ val snoozeMinutes: Int,
20
+ val repeatType: String,
21
+ val weekdays: List<Int>,
22
+ val intervalMs: Long,
23
+ val data: String,
24
+ var enabled: Boolean = true
25
+ ) {
26
+ fun nextTrigger(from: Long = System.currentTimeMillis()): Long {
27
+ if (!enabled) return 0
28
+ return when (repeatType) {
29
+ "daily" -> nextMatching(from) { true }
30
+ "weekly" -> nextMatching(from) { weekdays.isEmpty() || it in weekdays }
31
+ "interval" -> when {
32
+ intervalMs <= 0 -> 0
33
+ timestamp > from -> timestamp
34
+ else -> timestamp + ((from - timestamp) / intervalMs + 1) * intervalMs
35
+ }
36
+ else -> if (timestamp > from) timestamp else 0
37
+ }
38
+ }
39
+
40
+ private fun nextMatching(from: Long, ok: (Int) -> Boolean): Long {
41
+ val base = Calendar.getInstance().apply { timeInMillis = timestamp }
42
+ val c = Calendar.getInstance().apply {
43
+ timeInMillis = from
44
+ set(Calendar.HOUR_OF_DAY, base.get(Calendar.HOUR_OF_DAY))
45
+ set(Calendar.MINUTE, base.get(Calendar.MINUTE))
46
+ set(Calendar.SECOND, 0)
47
+ set(Calendar.MILLISECOND, 0)
48
+ }
49
+ repeat(8) {
50
+ if (c.timeInMillis > from && ok(c.get(Calendar.DAY_OF_WEEK))) return c.timeInMillis
51
+ c.add(Calendar.DAY_OF_YEAR, 1)
52
+ }
53
+ return 0
54
+ }
55
+
56
+ fun toJson(): String = JSONObject().apply {
57
+ put("id", id); put("timestamp", timestamp); put("title", title); put("body", body)
58
+ put("sound", sound); put("vibrate", vibrate); put("snoozeEnabled", snoozeEnabled)
59
+ put("snoozeMinutes", snoozeMinutes); put("repeatType", repeatType)
60
+ put("weekdays", JSONArray(weekdays)); put("intervalMs", intervalMs)
61
+ put("data", data); put("enabled", enabled)
62
+ }.toString()
63
+
64
+ fun toWritableMap(): WritableMap = Arguments.createMap().apply {
65
+ putString("alarmId", id)
66
+ putDouble("timestamp", timestamp.toDouble())
67
+ putString("title", title)
68
+ putString("body", body)
69
+ putString("sound", sound)
70
+ putBoolean("vibrate", vibrate)
71
+ putBoolean("snoozeEnabled", snoozeEnabled)
72
+ putDouble("snoozeMinutes", snoozeMinutes.toDouble())
73
+ putMap("repeat", Arguments.createMap().apply {
74
+ putString("type", repeatType)
75
+ putArray("weekdays", Arguments.createArray().apply { weekdays.forEach { pushInt(it) } })
76
+ putDouble("intervalMs", intervalMs.toDouble())
77
+ })
78
+ putString("data", data)
79
+ putBoolean("enabled", enabled)
80
+ putDouble("nextTriggerAt", nextTrigger().toDouble())
81
+ }
82
+
83
+ /** Initial props for the React ring screen hosted by [AlarmRingActivity]. */
84
+ fun toBundle(): Bundle = Bundle().apply {
85
+ putString("alarmId", id)
86
+ putDouble("timestamp", timestamp.toDouble())
87
+ putString("title", title)
88
+ putString("body", body)
89
+ putBoolean("snoozeEnabled", snoozeEnabled)
90
+ putInt("snoozeMinutes", snoozeMinutes)
91
+ putString("data", data)
92
+ }
93
+
94
+ companion object {
95
+ fun fromJson(s: String): AlarmData {
96
+ val o = JSONObject(s)
97
+ val wd = o.optJSONArray("weekdays")
98
+ return AlarmData(
99
+ o.getString("id"), o.getLong("timestamp"), o.optString("title"), o.optString("body"),
100
+ o.optString("sound", "default"), o.optBoolean("vibrate", true),
101
+ o.optBoolean("snoozeEnabled", true), o.optInt("snoozeMinutes", 5),
102
+ o.optString("repeatType", "none"),
103
+ if (wd == null) emptyList() else (0 until wd.length()).map { wd.getInt(it) },
104
+ o.optLong("intervalMs", 0), o.optString("data"), o.optBoolean("enabled", true)
105
+ )
106
+ }
107
+
108
+ fun fromMap(m: ReadableMap, id: String): AlarmData {
109
+ val r = if (m.hasKey("repeat") && !m.isNull("repeat")) m.getMap("repeat") else null
110
+ val wd = r?.takeIf { it.hasKey("weekdays") }?.getArray("weekdays")
111
+ ?.let { a -> (0 until a.size()).map { a.getInt(it) } } ?: emptyList()
112
+ return AlarmData(
113
+ id = id,
114
+ timestamp = m.getDouble("timestamp").toLong(),
115
+ title = m.str("title"),
116
+ body = m.str("body"),
117
+ sound = m.str("sound", "default"),
118
+ vibrate = m.bool("vibrate", true),
119
+ snoozeEnabled = m.bool("snoozeEnabled", true),
120
+ snoozeMinutes = if (m.hasKey("snoozeMinutes")) m.getDouble("snoozeMinutes").toInt() else 5,
121
+ repeatType = r?.str("type", "none") ?: "none",
122
+ weekdays = wd,
123
+ intervalMs = if (r != null && r.hasKey("intervalMs")) r.getDouble("intervalMs").toLong() else 0,
124
+ data = m.str("data")
125
+ )
126
+ }
127
+
128
+ private fun ReadableMap.str(k: String, d: String = "") =
129
+ if (hasKey(k) && !isNull(k)) getString(k) ?: d else d
130
+
131
+ private fun ReadableMap.bool(k: String, d: Boolean) =
132
+ if (hasKey(k) && !isNull(k)) getBoolean(k) else d
133
+ }
134
+ }
@@ -0,0 +1,263 @@
1
+ package com.alarm
2
+
3
+ import android.Manifest
4
+ import android.app.NotificationManager
5
+ import android.content.Intent
6
+ import android.content.pm.PackageManager
7
+ import android.net.Uri
8
+ import android.os.Build
9
+ import android.provider.Settings
10
+ import androidx.core.app.NotificationManagerCompat
11
+ import androidx.core.content.ContextCompat
12
+ import com.facebook.react.bridge.*
13
+ import com.facebook.react.modules.core.DeviceEventManagerModule
14
+ import com.facebook.react.modules.core.PermissionAwareActivity
15
+ import java.lang.ref.WeakReference
16
+ import java.util.UUID
17
+
18
+ class AlarmModule(private val ctx: ReactApplicationContext) : ReactContextBaseJavaModule(ctx) {
19
+
20
+ init { instance = WeakReference(this) }
21
+
22
+ override fun getName() = NAME
23
+
24
+ override fun invalidate() {
25
+ if (instance?.get() === this) instance = null
26
+ super.invalidate()
27
+ }
28
+
29
+ // ---------- Permissions ----------
30
+
31
+ private fun notificationsGranted(): Boolean =
32
+ if (Build.VERSION.SDK_INT >= 33)
33
+ ContextCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) ==
34
+ PackageManager.PERMISSION_GRANTED
35
+ else NotificationManagerCompat.from(ctx).areNotificationsEnabled()
36
+
37
+ private fun canDrawOverlays(): Boolean = Settings.canDrawOverlays(ctx)
38
+
39
+ private fun canUseFullScreenIntent(): Boolean =
40
+ Build.VERSION.SDK_INT < 34 ||
41
+ ctx.getSystemService(NotificationManager::class.java).canUseFullScreenIntent()
42
+
43
+ private fun permissionMap(): WritableMap = Arguments.createMap().apply {
44
+ val ok = notificationsGranted()
45
+ putString("status", if (ok) "granted" else "denied")
46
+ putBoolean("notifications", ok)
47
+ putBoolean("canScheduleExact", AlarmScheduler.canScheduleExact(ctx))
48
+ putBoolean("canDrawOverlays", canDrawOverlays())
49
+ putBoolean("canUseFullScreenIntent", canUseFullScreenIntent())
50
+ }
51
+
52
+ private fun launchSettings(intent: Intent) {
53
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
54
+ (ctx.currentActivity ?: ctx).startActivity(intent)
55
+ }
56
+
57
+ /**
58
+ * Opens the settings screen for the first missing special permission, in the order
59
+ * that matters most for a ringing alarm. Only one screen at a time; the caller
60
+ * re-checks and calls again if needed.
61
+ */
62
+ private fun openNextMissingSpecialPermission(): Boolean {
63
+ val pkg = Uri.parse("package:${ctx.packageName}")
64
+ return when {
65
+ // Android 12 only: on 13+ USE_EXACT_ALARM is granted automatically.
66
+ Build.VERSION.SDK_INT in 31..32 && !AlarmScheduler.canScheduleExact(ctx) -> {
67
+ launchSettings(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, pkg)); true
68
+ }
69
+ // "Display over other apps": lets the ringing service pop the alarm screen
70
+ // instantly, even from the background. The full-screen notification alone is
71
+ // unreliable on many devices (notably Xiaomi/MIUI).
72
+ !canDrawOverlays() -> {
73
+ launchSettings(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, pkg)); true
74
+ }
75
+ // Android 14+: full-screen intents can be revoked per app.
76
+ !canUseFullScreenIntent() -> {
77
+ launchSettings(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT).setData(pkg)); true
78
+ }
79
+ else -> false
80
+ }
81
+ }
82
+
83
+ /**
84
+ * OEM "Autostart" permission (Xiaomi/MIUI, Oppo, Vivo, Huawei...). Without it the
85
+ * system refuses to start the app's process to deliver the AlarmManager broadcast
86
+ * once the app has been killed, so the alarm silently never rings. There is no public
87
+ * API to query it; this opens the vendor settings screen when one exists.
88
+ * Resolves true if a screen was opened, false if this device has none.
89
+ */
90
+ @ReactMethod
91
+ fun openAutoStartSettings(promise: Promise) {
92
+ val candidates = listOf(
93
+ "com.miui.securitycenter" to "com.miui.permcenter.autostart.AutoStartManagementActivity",
94
+ "com.coloros.safecenter" to "com.coloros.safecenter.permission.startup.StartupAppListActivity",
95
+ "com.oppo.safe" to "com.oppo.safe.permission.startup.StartupAppListActivity",
96
+ "com.vivo.permissionmanager" to "com.vivo.permissionmanager.activity.BgStartUpManagerActivity",
97
+ "com.huawei.systemmanager" to "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity",
98
+ "com.samsung.android.lool" to "com.samsung.android.sm.ui.battery.BatteryActivity",
99
+ )
100
+ for ((pkg, cls) in candidates) {
101
+ val intent = Intent().setComponent(android.content.ComponentName(pkg, cls))
102
+ if (ctx.packageManager.resolveActivity(intent, 0) == null) continue
103
+ try {
104
+ launchSettings(intent)
105
+ return promise.resolve(true)
106
+ } catch (_: Exception) { /* try the next one */ }
107
+ }
108
+ promise.resolve(false)
109
+ }
110
+
111
+ @ReactMethod
112
+ fun canScheduleExactAlarms(promise: Promise) =
113
+ promise.resolve(AlarmScheduler.canScheduleExact(ctx))
114
+
115
+ @ReactMethod
116
+ fun checkPermission(promise: Promise) = promise.resolve(permissionMap())
117
+
118
+ @ReactMethod
119
+ fun requestPermission(promise: Promise) {
120
+ val activity = ctx.currentActivity
121
+ if (Build.VERSION.SDK_INT >= 33 && !notificationsGranted() && activity is PermissionAwareActivity) {
122
+ activity.requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 9871) { _, _, _ ->
123
+ openNextMissingSpecialPermission()
124
+ promise.resolve(permissionMap())
125
+ true
126
+ }
127
+ return
128
+ }
129
+ openNextMissingSpecialPermission()
130
+ promise.resolve(permissionMap())
131
+ }
132
+
133
+ // ---------- Scheduling ----------
134
+
135
+ @ReactMethod
136
+ fun scheduleAlarm(config: ReadableMap, promise: Promise) {
137
+ try {
138
+ val id = if (config.hasKey("alarmId") && !config.isNull("alarmId"))
139
+ config.getString("alarmId")!! else UUID.randomUUID().toString()
140
+ val alarm = AlarmData.fromMap(config, id)
141
+ AlarmScheduler.cancel(ctx, id)
142
+ AlarmStore.save(ctx, alarm)
143
+ AlarmScheduler.schedule(ctx, alarm)
144
+ promise.resolve(id)
145
+ } catch (e: Exception) {
146
+ promise.reject("E_SCHEDULE", e.message, e)
147
+ }
148
+ }
149
+
150
+ @ReactMethod
151
+ fun updateAlarm(alarmId: String, config: ReadableMap, promise: Promise) {
152
+ try {
153
+ if (AlarmStore.get(ctx, alarmId) == null) return promise.resolve(false)
154
+ val alarm = AlarmData.fromMap(config, alarmId)
155
+ AlarmScheduler.cancel(ctx, alarmId)
156
+ AlarmStore.save(ctx, alarm)
157
+ AlarmScheduler.schedule(ctx, alarm)
158
+ promise.resolve(true)
159
+ } catch (e: Exception) {
160
+ promise.reject("E_UPDATE", e.message, e)
161
+ }
162
+ }
163
+
164
+ @ReactMethod
165
+ fun cancelAlarm(alarmId: String, promise: Promise) {
166
+ val existed = AlarmStore.get(ctx, alarmId) != null
167
+ AlarmScheduler.cancel(ctx, alarmId)
168
+ if (AlarmService.current?.id == alarmId) AlarmService.dismiss(ctx)
169
+ AlarmStore.remove(ctx, alarmId)
170
+ promise.resolve(existed)
171
+ }
172
+
173
+ @ReactMethod
174
+ fun cancelAllAlarms(promise: Promise) {
175
+ AlarmStore.all(ctx).forEach { AlarmScheduler.cancel(ctx, it.id) }
176
+ AlarmService.dismiss(ctx)
177
+ AlarmStore.clear(ctx)
178
+ promise.resolve(null)
179
+ }
180
+
181
+ // ---------- Control (while ringing) ----------
182
+
183
+ @ReactMethod
184
+ fun stopAlarm(alarmId: String, promise: Promise) {
185
+ AlarmService.dismiss(ctx)
186
+ promise.resolve(null)
187
+ }
188
+
189
+ @ReactMethod
190
+ fun snoozeAlarm(alarmId: String, minutes: Double, promise: Promise) {
191
+ val ringing = AlarmService.current
192
+ if (ringing != null) {
193
+ AlarmService.snooze(ctx, minutes.toInt())
194
+ return promise.resolve(null)
195
+ }
196
+ // Not ringing: schedule a one-off snooze for a stored alarm.
197
+ val alarm = AlarmStore.get(ctx, alarmId)
198
+ ?: return promise.reject("E_NOT_FOUND", "Alarm $alarmId not found")
199
+ try {
200
+ AlarmScheduler.scheduleSnooze(ctx, alarm, minutes.toInt())
201
+ promise.resolve(null)
202
+ } catch (e: Exception) {
203
+ promise.reject("E_SNOOZE", e.message, e)
204
+ }
205
+ }
206
+
207
+ /** The alarm currently ringing (if any), so JS can show the ring UI on launch. */
208
+ @ReactMethod
209
+ fun getRingingAlarm(promise: Promise) =
210
+ promise.resolve(AlarmService.current?.toWritableMap())
211
+
212
+ // ---------- Queries ----------
213
+
214
+ @ReactMethod
215
+ fun getAlarm(alarmId: String, promise: Promise) =
216
+ promise.resolve(AlarmStore.get(ctx, alarmId)?.toWritableMap())
217
+
218
+ @ReactMethod
219
+ fun getAllAlarms(promise: Promise) {
220
+ val arr = Arguments.createArray()
221
+ AlarmStore.all(ctx).sortedBy { it.timestamp }.forEach { arr.pushMap(it.toWritableMap()) }
222
+ promise.resolve(arr)
223
+ }
224
+
225
+ @ReactMethod
226
+ fun isScheduled(alarmId: String, promise: Promise) {
227
+ val a = AlarmStore.get(ctx, alarmId)
228
+ promise.resolve(a != null && a.enabled && a.nextTrigger() > 0)
229
+ }
230
+
231
+ // ---------- Events ----------
232
+
233
+ // Required by NativeEventEmitter on the JS side.
234
+ @ReactMethod fun addListener(eventName: String) {}
235
+ @ReactMethod fun removeListeners(count: Double) {}
236
+
237
+ private fun send(eventName: String, a: AlarmData) {
238
+ if (!ctx.hasActiveReactInstance()) return
239
+ val map = Arguments.createMap().apply {
240
+ putString("alarmId", a.id)
241
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
242
+ putString("title", a.title)
243
+ putString("body", a.body)
244
+ putString("data", a.data)
245
+ }
246
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(eventName, map)
247
+ }
248
+
249
+ companion object {
250
+ const val NAME = "Alarm"
251
+ const val EVENT_FIRED = "onAlarmFired"
252
+ const val EVENT_DISMISSED = "onAlarmDismissed"
253
+ const val EVENT_SNOOZED = "onAlarmSnoozed"
254
+
255
+ private var instance: WeakReference<AlarmModule>? = null
256
+
257
+ /** Called from native alarm code; no-op if the JS runtime isn't alive. */
258
+ fun emit(eventName: String, a: AlarmData) {
259
+ val module = instance?.get() ?: return
260
+ runCatching { module.send(eventName, a) }
261
+ }
262
+ }
263
+ }