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.
- package/.bundle/config +2 -0
- package/AlarmService.d.ts +112 -0
- package/AlarmService.ts +196 -0
- package/README.md +385 -2
- package/android/app/build.gradle +119 -0
- package/android/app/debug.keystore +0 -0
- package/android/app/proguard-rules.pro +10 -0
- package/android/app/src/main/AndroidManifest.xml +81 -0
- package/android/app/src/main/java/com/reactnativealarm/MainActivity.kt +29 -0
- package/android/app/src/main/java/com/reactnativealarm/MainApplication.kt +29 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmData.kt +134 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmModule.kt +263 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmPackage.kt +24 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmReceiver.kt +43 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmRingActivity.kt +86 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmScheduler.kt +74 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmService.kt +285 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmStore.kt +13 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/BootReceiver.kt +29 -0
- package/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/values/strings.xml +3 -0
- package/android/app/src/main/res/values/styles.xml +9 -0
- package/android/build.gradle +21 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +49 -0
- package/android/gradlew +248 -0
- package/android/gradlew.bat +98 -0
- package/android/settings.gradle +6 -0
- package/app.json +4 -0
- package/index.d.ts +20 -0
- package/index.js +12 -0
- package/index.ts +21 -0
- package/ios/.xcode.env +11 -0
- package/ios/Podfile +34 -0
- package/ios/Podfile.lock +2179 -0
- package/ios/ReactNativeAlarm/AppDelegate.swift +14 -0
- package/ios/ReactNativeAlarm/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/ios/ReactNativeAlarm/Images.xcassets/Contents.json +6 -0
- package/ios/ReactNativeAlarm/Info.plist +79 -0
- package/ios/ReactNativeAlarm/LaunchScreen.storyboard +47 -0
- package/ios/ReactNativeAlarm/PrivacyInfo.xcprivacy +37 -0
- package/ios/ReactNativeAlarm/SceneDelegate.swift +56 -0
- package/ios/ReactNativeAlarm.xcodeproj/project.pbxproj +530 -0
- package/ios/ReactNativeAlarm.xcodeproj/xcshareddata/xcschemes/ReactNativeAlarm.xcscheme +88 -0
- package/ios/ReactNativeAlarm.xcworkspace/contents.xcworkspacedata +10 -0
- package/ios/alarm/AlarmData.swift +97 -0
- package/ios/alarm/AlarmKitBackend.swift +212 -0
- package/ios/alarm/AlarmModule.m +53 -0
- package/ios/alarm/AlarmModule.swift +284 -0
- package/ios/alarm/AlarmStore.swift +51 -0
- package/ios/alarm/NotificationBackend.swift +227 -0
- package/package.json +91 -3
- 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
|
+
}
|