diff --git a/.npmpackagejsonlintrc.json b/.npmpackagejsonlintrc.json
index 831d68fc4..7d53cdbe9 100644
--- a/.npmpackagejsonlintrc.json
+++ b/.npmpackagejsonlintrc.json
@@ -13,7 +13,8 @@
"@joplin/turndown",
"@joplin/turndown-plugin-gfm",
"@joplin/tools",
- "@joplin/react-native-saf-x"
+ "@joplin/react-native-saf-x",
+ "@joplin/react-native-alarm-notification"
]
}
]
diff --git a/joplin.code-workspace b/joplin.code-workspace
index 0c54ad30f..d784f5dc2 100644
--- a/joplin.code-workspace
+++ b/joplin.code-workspace
@@ -317,6 +317,9 @@
"packages/app-tools/github_oauth_token.txt": true,
"packages/generator-joplin/generators/app/templates/api/": true,
"packages/htmlpack/dist/": true,
+ "packages/react-native-alarm-notification/android/build": true,
+ "packages/react-native-saf-x/android/build": true,
+ "packages/react-native-saf-x/android/wrapper": true,
"packages/renderer/**/.vscode/": true,
"packages/renderer/**/copyLib.bat": true,
"packages/renderer/**/node_modules/": true,
diff --git a/packages/app-mobile/ios/Podfile.lock b/packages/app-mobile/ios/Podfile.lock
index 8d87c1571..9613d69fc 100644
--- a/packages/app-mobile/ios/Podfile.lock
+++ b/packages/app-mobile/ios/Podfile.lock
@@ -306,7 +306,7 @@ PODS:
- React-jsinspector (0.70.6)
- React-logger (0.70.6):
- glog
- - react-native-alarm-notification (1.0.7):
+ - react-native-alarm-notification (2.10.0):
- React
- react-native-camera (4.2.1):
- React-Core
@@ -490,7 +490,7 @@ DEPENDENCIES:
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- - react-native-alarm-notification (from `../node_modules/joplin-rn-alarm-notification`)
+ - "react-native-alarm-notification (from `../node_modules/@joplin/react-native-alarm-notification`)"
- react-native-camera (from `../node_modules/react-native-camera`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
@@ -597,7 +597,7 @@ EXTERNAL SOURCES:
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-alarm-notification:
- :path: "../node_modules/joplin-rn-alarm-notification"
+ :path: "../node_modules/@joplin/react-native-alarm-notification"
react-native-camera:
:path: "../node_modules/react-native-camera"
react-native-document-picker:
@@ -714,7 +714,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: b4a65947391c658450151275aa406f2b8263178f
React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
- react-native-alarm-notification: 4e150e89c1707e057bc5e8c87ab005f1ea4b8d52
+ react-native-alarm-notification: 2218b44c1207344a90e584709f13c7b324073bf4
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
diff --git a/packages/app-mobile/metro.config.js b/packages/app-mobile/metro.config.js
index d152ed9f0..16acb4e8b 100644
--- a/packages/app-mobile/metro.config.js
+++ b/packages/app-mobile/metro.config.js
@@ -16,6 +16,21 @@
const path = require('path');
+const localPackages = {
+ '@joplin/lib': path.resolve(__dirname, '../lib/'),
+ '@joplin/renderer': path.resolve(__dirname, '../renderer/'),
+ '@joplin/tools': path.resolve(__dirname, '../tools/'),
+ '@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
+ '@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
+ '@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
+ '@joplin/react-native-alarm-notification': path.resolve(__dirname, '../react-native-alarm-notification/'),
+};
+
+const watchedFolders = [];
+for (const [, v] of Object.entries(localPackages)) {
+ watchedFolders.push(v);
+}
+
module.exports = {
transformer: {
getTransformOptions: async () => ({
@@ -26,26 +41,19 @@ module.exports = {
}),
},
resolver: {
- // This configuration allows you to build React-Native modules and
- // * test them without having to publish the module. Any exports provided
- // * by your source should be added to the "target" parameter. Any import
- // * not matched by a key in target will have to be located in the embedded
- // * app's node_modules directory.
+ // This configuration allows you to build React-Native modules and test
+ // them without having to publish the module. Any exports provided by
+ // your source should be added to the "target" parameter. Any import not
+ // matched by a key in target will have to be located in the embedded
+ // app's node_modules directory.
//
extraNodeModules: new Proxy(
- // The first argument to the Proxy constructor is passed as
- // * "target" to the "get" method below.
- // * Put the names of the libraries included in your reusable
- // * module as they would be imported when the module is actually used.
+ // The first argument to the Proxy constructor is passed as "target"
+ // to the "get" method below. Put the names of the libraries
+ // included in your reusable module as they would be imported when
+ // the module is actually used.
//
- {
- '@joplin/lib': path.resolve(__dirname, '../lib/'),
- '@joplin/renderer': path.resolve(__dirname, '../renderer/'),
- '@joplin/tools': path.resolve(__dirname, '../tools/'),
- '@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
- '@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
- '@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
- },
+ localPackages,
{
get: (target, name) => {
if (target.hasOwnProperty(name)) {
@@ -57,12 +65,5 @@ module.exports = {
),
},
projectRoot: path.resolve(__dirname),
- watchFolders: [
- path.resolve(__dirname, '../lib'),
- path.resolve(__dirname, '../renderer'),
- path.resolve(__dirname, '../tools'),
- path.resolve(__dirname, '../fork-htmlparser2'),
- path.resolve(__dirname, '../fork-uslug'),
- path.resolve(__dirname, '../react-native-saf-x'),
- ],
+ watchFolders: watchedFolders,
};
diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json
index 7217d3281..91b87a26e 100644
--- a/packages/app-mobile/package.json
+++ b/packages/app-mobile/package.json
@@ -19,6 +19,7 @@
},
"dependencies": {
"@joplin/lib": "~2.10",
+ "@joplin/react-native-alarm-notification": "~2.10",
"@joplin/react-native-saf-x": "~2.10",
"@joplin/renderer": "~2.10",
"@react-native-community/clipboard": "1.5.1",
@@ -32,7 +33,6 @@
"constants-browserify": "1.0.0",
"crypto-browserify": "3.12.0",
"events": "3.3.0",
- "joplin-rn-alarm-notification": "1.0.7",
"jsc-android": "241213.1.0",
"lodash": "4.17.21",
"md5": "2.3.0",
diff --git a/packages/app-mobile/services/AlarmServiceDriver.android.ts b/packages/app-mobile/services/AlarmServiceDriver.android.ts
index a13b03504..a80a738f4 100644
--- a/packages/app-mobile/services/AlarmServiceDriver.android.ts
+++ b/packages/app-mobile/services/AlarmServiceDriver.android.ts
@@ -1,13 +1,13 @@
import Logger from '@joplin/lib/Logger';
import { Notification } from '@joplin/lib/models/Alarm';
-const ReactNativeAN = require('joplin-rn-alarm-notification').default;
+const ReactNativeAN = require('@joplin/react-native-alarm-notification').default;
export default class AlarmServiceDriver {
private logger_: Logger;
- constructor(logger: Logger) {
+ public constructor(logger: Logger) {
this.logger_ = logger;
}
diff --git a/packages/react-native-alarm-notification/.gitattributes b/packages/react-native-alarm-notification/.gitattributes
new file mode 100644
index 000000000..d42ff1835
--- /dev/null
+++ b/packages/react-native-alarm-notification/.gitattributes
@@ -0,0 +1 @@
+*.pbxproj -text
diff --git a/packages/react-native-alarm-notification/.gitignore b/packages/react-native-alarm-notification/.gitignore
new file mode 100644
index 000000000..f4a80fa4a
--- /dev/null
+++ b/packages/react-native-alarm-notification/.gitignore
@@ -0,0 +1,44 @@
+# OSX
+#
+.DS_Store
+
+# node.js
+#
+node_modules/
+npm-debug.log
+yarn-error.log
+
+# Xcode
+#
+build/
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata
+*.xccheckout
+*.moved-aside
+DerivedData
+*.hmap
+*.ipa
+*.xcuserstate
+project.xcworkspace
+
+# Android/IntelliJ
+#
+**/build/
+**/.idea
+**/.gradle/
+**/gradle/
+gradle*
+local.properties
+*.iml
+
+# BUCK
+buck-out/
+\.buckd/
+*.keystore
diff --git a/packages/react-native-alarm-notification/LICENSE b/packages/react-native-alarm-notification/LICENSE
new file mode 100644
index 000000000..9e1f29d46
--- /dev/null
+++ b/packages/react-native-alarm-notification/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Chukwuemeka Ihedoro
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/react-native-alarm-notification/README.md b/packages/react-native-alarm-notification/README.md
new file mode 100644
index 000000000..d21427f90
--- /dev/null
+++ b/packages/react-native-alarm-notification/README.md
@@ -0,0 +1,5 @@
+# React Native Alarm Notification
+
+This is a fork of [https://github.com/emekalites/react-native-alarm-notification](https://github.com/emekalites/react-native-alarm-notification) with a few bugfixes/improvements for Android.
+
+It's made specifically for [Joplin](https://github.com/laurent22) and while all basic features should work (set/dismiss alarm, set text/icon, etc) it's not fully compatible with the orignal package.
\ No newline at end of file
diff --git a/packages/react-native-alarm-notification/android/build.gradle b/packages/react-native-alarm-notification/android/build.gradle
new file mode 100644
index 000000000..08c35faf9
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/build.gradle
@@ -0,0 +1,145 @@
+import groovy.json.JsonSlurper
+
+// android/build.gradle
+
+// based on:
+//
+// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle
+// original location:
+// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle
+//
+// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle
+// original location:
+// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle
+
+def DEFAULT_COMPILE_SDK_VERSION = 31
+def DEFAULT_BUILD_TOOLS_VERSION = '31.0.0'
+def DEFAULT_MIN_SDK_VERSION = 21
+def DEFAULT_TARGET_SDK_VERSION = 31
+
+def safeExtGet(prop, fallback) {
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+
+buildscript {
+ // The Android Gradle plugin is only required when opening the android folder stand-alone.
+ // This avoids unnecessary downloads and potential conflicts when the library is included as a
+ // module dependency in an application project.
+ // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies
+ if (project == rootProject) {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.4.1'
+ }
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+
+android {
+ compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
+ buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
+ targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
+ versionCode 1
+ versionName "1.0"
+ }
+ lintOptions {
+ abortOnError false
+ }
+}
+
+repositories {
+ // ref: https://www.baeldung.com/maven-local-repository
+ mavenLocal()
+ maven {
+ // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
+ url "$rootDir/../node_modules/react-native/android"
+ }
+ maven {
+ // Android JSC is installed from npm
+ url "$rootDir/../node_modules/jsc-android/dist"
+ }
+ google()
+ mavenCentral()
+}
+
+dependencies {
+ //noinspection GradleDynamicVersion
+ implementation 'com.facebook.react:react-native:+' // From node_modules
+ implementation 'com.google.code.gson:gson:2.8.8'
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.robolectric:robolectric:4.6'
+}
+
+def configureReactNativePom(def pom) {
+ def packageJson = new JsonSlurper().parseText(file('../package.json').text)
+
+ pom.project {
+ name packageJson.title
+ artifactId packageJson.name
+ version = packageJson.version
+ group = "com.emekalites.react.alarm.notification"
+ description packageJson.description
+ url packageJson.repository.baseUrl
+
+ licenses {
+ license {
+ name packageJson.license
+ url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename
+ distribution 'repo'
+ }
+ }
+ }
+}
+
+afterEvaluate { project ->
+ // some Gradle build hooks ref:
+ // https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html
+ task androidJavadoc(type: Javadoc) {
+ source = android.sourceSets.main.java.srcDirs
+ classpath += files(android.bootClasspath)
+ // Must be removed due to error "Configuration with name 'compile' not found."
+ // classpath += files(project.getConfigurations().getByName('compile').asList())
+ include '**/*.java'
+ }
+
+ task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
+ classifier = 'javadoc'
+ from androidJavadoc.destinationDir
+ }
+
+ task androidSourcesJar(type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.java.srcDirs
+ include '**/*.java'
+ }
+
+ android.libraryVariants.all { variant ->
+ def name = variant.name.capitalize()
+ def javaCompileTask = variant.javaCompileProvider.get()
+
+ task "jar${name}"(type: Jar, dependsOn: javaCompileTask) {
+ from javaCompileTask.destinationDir
+ }
+ }
+
+ artifacts {
+ archives androidSourcesJar
+ archives androidJavadocJar
+ }
+
+ task installArchives(type: Upload) {
+ configuration = configurations.archives
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/AndroidManifest.xml b/packages/react-native-alarm-notification/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..52c118708
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/AndroidManifest.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/ANModule.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/ANModule.java
new file mode 100644
index 000000000..c6887f717
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/ANModule.java
@@ -0,0 +1,198 @@
+package com.emekalites.react.alarm.notification;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.facebook.react.bridge.ActivityEventListener;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+@SuppressWarnings("unused")
+public class ANModule extends ReactContextBaseJavaModule implements ActivityEventListener {
+
+ private static final String E_SCHEDULE_ALARM_FAILED = "E_SCHEDULE_ALARM_FAILED";
+
+ private static ReactApplicationContext mReactContext;
+
+ private final AlarmUtil alarmUtil;
+ private final AlarmModelCodec codec = new AlarmModelCodec();
+
+ ANModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ mReactContext = reactContext;
+ alarmUtil = new AlarmUtil((Application) reactContext.getApplicationContext());
+ reactContext.addActivityEventListener(this);
+ }
+
+ static ReactApplicationContext getReactAppContext() {
+ return mReactContext;
+ }
+
+ @NonNull
+ @Override
+ public String getName() {
+ return "RNAlarmNotification";
+ }
+
+ // Required for rn built in EventEmitter Calls.
+ @ReactMethod
+ public void addListener(String eventName) {
+
+ }
+
+ @ReactMethod
+ public void removeListeners(Integer count) {
+
+ }
+
+ private AlarmDatabase getAlarmDB() {
+ return new AlarmDatabase(mReactContext);
+ }
+
+ @ReactMethod
+ public void scheduleAlarm(ReadableMap details, Promise promise) {
+ try {
+ Bundle bundle = Arguments.toBundle(details);
+ AlarmModel alarm = AlarmModel.fromBundle(bundle);
+
+ // check if alarm has been set at this time
+ boolean containAlarm = alarmUtil.checkAlarm(getAlarmDB().getAlarmList(1), alarm);
+ if (!containAlarm) {
+ int id = getAlarmDB().insert(alarm);
+ alarm.setId(id);
+
+ alarmUtil.setAlarm(alarm);
+
+ WritableMap map = Arguments.createMap();
+ map.putInt("id", id);
+
+ promise.resolve(map);
+ } else {
+ promise.reject(E_SCHEDULE_ALARM_FAILED, "duplicate alarm set at date");
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Could not schedule alarm", e);
+ promise.reject(E_SCHEDULE_ALARM_FAILED, e);
+ }
+ }
+
+ @ReactMethod
+ public void deleteAlarm(int alarmID) {
+ alarmUtil.deleteAlarm(alarmID);
+ }
+
+ @ReactMethod
+ public void deleteRepeatingAlarm(int alarmID) {
+ alarmUtil.deleteRepeatingAlarm(alarmID);
+ }
+
+ @ReactMethod
+ public void sendNotification(ReadableMap details) {
+ try {
+ Bundle bundle = Arguments.toBundle(details);
+ AlarmModel alarm = AlarmModel.fromBundle(bundle);
+
+ int id = getAlarmDB().insert(alarm);
+ alarm.setId(id);
+
+ alarmUtil.sendNotification(alarm);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Could not send notification", e);
+ }
+ }
+
+ @ReactMethod
+ public void removeFiredNotification(int id) {
+ alarmUtil.removeFiredNotification(id);
+ }
+
+ @ReactMethod
+ public void removeAllFiredNotifications() {
+ alarmUtil.removeAllFiredNotifications();
+ }
+
+ @ReactMethod
+ public void getScheduledAlarms(Promise promise) throws JSONException {
+ ArrayList alarms = alarmUtil.getAlarms();
+ WritableArray array = Arguments.createArray();
+ for (AlarmModel alarm : alarms) {
+ // TODO triple conversion alarm -> string -> json -> map
+ // this is ugly but I don't have time to fix it now
+ WritableMap alarmMap = alarmUtil.convertJsonToMap(new JSONObject(codec.toJson(alarm)));
+ array.pushMap(alarmMap);
+ }
+ promise.resolve(array);
+ }
+
+ @Override
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
+
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ if (Constants.NOTIFICATION_ACTION_CLICK.equals(intent.getAction())) {
+ Bundle bundle = intent.getExtras();
+ try {
+ if (bundle != null) {
+ int alarmId = bundle.getInt(Constants.NOTIFICATION_ID);
+ alarmUtil.removeFiredNotification(alarmId);
+ alarmUtil.doCancelAlarm(alarmId);
+
+ WritableMap response = Arguments.fromBundle(bundle.getBundle("data"));
+ mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit("OnNotificationOpened", response);
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Couldn't convert bundle to JSON", e);
+ }
+ }
+ }
+
+ @ReactMethod
+ public void getAlarmInfo(Promise promise) {
+ if (getCurrentActivity() == null) {
+ promise.resolve(null);
+ return;
+ }
+
+ Intent intent = getCurrentActivity().getIntent();
+ if (intent != null) {
+ if (Constants.NOTIFICATION_ACTION_CLICK.equals(intent.getAction()) &&
+ intent.getExtras() != null) {
+ Bundle bundle = intent.getExtras();
+ WritableMap response = Arguments.fromBundle(bundle.getBundle("data"));
+ promise.resolve(response);
+
+ // cleanup
+
+ // other libs may not expect the intent to be null so set an empty intent here
+ getCurrentActivity().setIntent(new Intent());
+
+ int alarmId = bundle.getInt(Constants.NOTIFICATION_ID);
+ alarmUtil.removeFiredNotification(alarmId);
+ alarmUtil.doCancelAlarm(alarmId);
+
+ return;
+ }
+ }
+ promise.resolve(null);
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/ANPackage.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/ANPackage.java
new file mode 100644
index 000000000..f51b1b93d
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/ANPackage.java
@@ -0,0 +1,31 @@
+package com.emekalites.react.alarm.notification;
+
+import androidx.annotation.NonNull;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.Collections;
+import java.util.List;
+
+@SuppressWarnings("unused")
+public class ANPackage implements ReactPackage {
+
+ @Override
+ public List createNativeModules(@NonNull ReactApplicationContext reactContext) {
+ return Collections.singletonList(new ANModule(reactContext));
+ }
+
+ // Deprecated RN 0.47
+ public List> createJSModules() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createViewManagers(@NonNull ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmBootReceiver.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmBootReceiver.java
new file mode 100644
index 000000000..9931d6e8e
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmBootReceiver.java
@@ -0,0 +1,34 @@
+package com.emekalites.react.alarm.notification;
+
+import android.app.Application;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+public class AlarmBootReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) ||
+ "android.intent.action.QUICKBOOT_POWERON".equals(intent.getAction()) ||
+ "android.intent.action.LOCKED_BOOT_COMPLETED".equals(intent.getAction()) ||
+ "com.htc.intent.action.QUICKBOOT_POWERON".equals(intent.getAction())) {
+
+ Log.i(Constants.TAG, "Rescheduling after boot, intent=" + intent);
+
+ try (AlarmDatabase alarmDB = new AlarmDatabase(context)) {
+ ArrayList alarms = alarmDB.getAlarmList(1);
+ AlarmUtil alarmUtil = new AlarmUtil((Application) context.getApplicationContext());
+
+ for (AlarmModel alarm : alarms) {
+ alarmUtil.setAlarm(alarm);
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Could not reschedule alarms on boot", e);
+ }
+ }
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmDatabase.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmDatabase.java
new file mode 100644
index 000000000..15160d785
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmDatabase.java
@@ -0,0 +1,153 @@
+package com.emekalites.react.alarm.notification;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+public class AlarmDatabase extends SQLiteOpenHelper implements AutoCloseable {
+
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "rnandb";
+
+ private static final String TABLE_NAME = "alarmtbl";
+
+ private static final String COL_ID = "id";
+ private static final String COL_DATA = "gson_data";
+ private static final String COL_ACTIVE = "active";
+
+ private final String CREATE_TABLE_ALARM = "CREATE TABLE " + TABLE_NAME + " ("
+ + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + COL_DATA + " TEXT, "
+ + COL_ACTIVE + " INTEGER) ";
+
+ private final AlarmModelCodec codec = new AlarmModelCodec();
+
+ AlarmDatabase(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_ALARM);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL(String.format(" DROP TABLE IF EXISTS %s", CREATE_TABLE_ALARM));
+ onCreate(db);
+ }
+
+ AlarmModel getAlarm(int _id) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ AlarmModel alarm = null;
+
+ String selectQuery = "SELECT * FROM " + TABLE_NAME + " WHERE " + COL_ID + " = " + _id;
+
+ try (Cursor cursor = db.rawQuery(selectQuery, null)) {
+ cursor.moveToFirst();
+
+ int id = cursor.getInt(0);
+ String data = cursor.getString(1);
+ int active = cursor.getInt(2);
+
+ Log.d(Constants.TAG, "get alarm -> id:" + id + ", active:" + active + ", " + data);
+
+
+ alarm = codec.fromJson(data);
+ alarm.setId(id);
+ alarm.setActive(active);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "getAlarm: exception", e);
+ }
+
+ return alarm;
+ }
+
+ int insert(AlarmModel alarm) {
+
+ try (SQLiteDatabase db = this.getWritableDatabase()) {
+ ContentValues values = new ContentValues();
+
+ String data = codec.toJson(alarm);
+ Log.i(Constants.TAG, "insert alarm: " + data);
+
+ values.put(COL_DATA, data);
+ values.put(COL_ACTIVE, alarm.getActive());
+
+ return (int) db.insert(TABLE_NAME, null, values);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Error inserting into DB", e);
+ return 0;
+ }
+ }
+
+ void update(AlarmModel alarm) {
+ String where = COL_ID + " = " + alarm.getId();
+ try (SQLiteDatabase db = this.getWritableDatabase()) {
+ ContentValues values = new ContentValues();
+
+ String data = codec.toJson(alarm);
+ Log.d(Constants.TAG, "update alarm: " + data);
+
+ values.put(COL_ID, alarm.getId());
+ values.put(COL_DATA, data);
+ values.put(COL_ACTIVE, alarm.getActive());
+
+ db.update(TABLE_NAME, values, where, null);
+
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Error updating alarm " + alarm, e);
+ }
+ }
+
+ void delete(int id) {
+ String where = COL_ID + "=" + id;
+ try (SQLiteDatabase db = this.getWritableDatabase()) {
+ db.delete(TABLE_NAME, where, null);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Error deleting alarm with id " + id, e);
+ }
+ }
+
+ ArrayList getAlarmList(int isActive) {
+ String selectQuery = "SELECT * FROM " + TABLE_NAME;
+
+ if (isActive == 1) {
+ selectQuery += " WHERE " + COL_ACTIVE + " = " + isActive;
+ }
+
+ SQLiteDatabase db = this.getWritableDatabase();
+ ArrayList alarms = new ArrayList<>();
+
+ try (Cursor cursor = db.rawQuery(selectQuery, null)) {
+ if (cursor.moveToFirst()) {
+ do {
+ int id = cursor.getInt(0);
+ String data = cursor.getString(1);
+ int active = cursor.getInt(2);
+
+ Log.d(Constants.TAG, "get alarm -> id:" + id + ", active:" + active + ", " + data);
+
+ AlarmModel alarm = codec.fromJson(data);
+ alarm.setId(id);
+ alarm.setActive(active);
+
+ alarms.add(alarm);
+ } while (cursor.moveToNext());
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "getAlarmList: exception cause " + e.getCause() + " message " + e.getMessage());
+ }
+
+ return alarms;
+ }
+
+ ArrayList getAlarmList() {
+ return getAlarmList(0);
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmDismissReceiver.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmDismissReceiver.java
new file mode 100644
index 000000000..3bae73678
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmDismissReceiver.java
@@ -0,0 +1,28 @@
+package com.emekalites.react.alarm.notification;
+
+import android.app.Application;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+
+public class AlarmDismissReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ AlarmUtil alarmUtil = new AlarmUtil((Application) context.getApplicationContext());
+ try {
+ int notificationId = intent.getExtras().getInt(Constants.NOTIFICATION_ID);
+ if (ANModule.getReactAppContext() != null) {
+ // TODO also send all user-provided args back
+ ANModule.getReactAppContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit("OnNotificationDismissed", "{\"id\": \"" + notificationId + "\"}");
+ }
+ alarmUtil.removeFiredNotification(notificationId);
+ alarmUtil.doCancelAlarm(notificationId);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Exception when handling notification dismiss. " + e);
+ }
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModel.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModel.java
new file mode 100644
index 000000000..1e8fdb6a3
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModel.java
@@ -0,0 +1,444 @@
+package com.emekalites.react.alarm.notification;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+
+public class AlarmModel implements Serializable {
+ private int id;
+
+ private int minute;
+ private int hour;
+ private int second;
+
+ private int day;
+ private int month;
+ private int year;
+
+ private int alarmId;
+ private String title;
+ private String message;
+ private String channel;
+ private String ticker;
+ private boolean autoCancel;
+ private boolean vibrate;
+ private int vibration;
+ private String smallIcon;
+ private String largeIcon;
+ private boolean playSound;
+ private String soundName;
+ private String soundNames; // separate sounds with comma eg (sound1.mp3,sound2.mp3)
+ private String color;
+ private String scheduleType;
+ private String interval; // hourly, daily, weekly
+ private int intervalValue;
+ private int snoozeInterval; // in minutes
+ private String tag;
+ private Bundle data;
+ private boolean loopSound;
+ private boolean useBigText;
+ private boolean hasButton;
+ private double volume;
+ private boolean bypassDnd;
+
+ private int active = 1; // 1 = yes, 0 = no
+
+ private AlarmModel() {}
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public int getSecond() {
+ return second;
+ }
+
+ public void setSecond(int second) {
+ this.second = second;
+ }
+
+ public int getMinute() {
+ return minute;
+ }
+
+ public void setMinute(int minute) {
+ this.minute = minute;
+ }
+
+ public int getHour() {
+ return hour;
+ }
+
+ public void setHour(int hour) {
+ this.hour = hour;
+ }
+
+ public int getDay() {
+ return day;
+ }
+
+ public void setDay(int day) {
+ this.day = day;
+ }
+
+ public int getMonth() {
+ return month;
+ }
+
+ public void setMonth(int month) {
+ this.month = month;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public void setYear(int year) {
+ this.year = year;
+ }
+
+ public int getAlarmId() {
+ return alarmId;
+ }
+
+ public void setAlarmId(int alarmId) {
+ this.alarmId = alarmId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public String getChannel() {
+ return channel;
+ }
+
+ public void setChannel(String channel) {
+ this.channel = channel;
+ }
+
+ public String getTicker() {
+ return ticker;
+ }
+
+ public void setTicker(String ticker) {
+ this.ticker = ticker;
+ }
+
+ public boolean isAutoCancel() {
+ return autoCancel;
+ }
+
+ public void setAutoCancel(boolean autoCancel) {
+ this.autoCancel = autoCancel;
+ }
+
+ public boolean isVibrate() {
+ return vibrate;
+ }
+
+ public void setVibrate(boolean vibrate) {
+ this.vibrate = vibrate;
+ }
+
+ public int getVibration() {
+ return vibration;
+ }
+
+ public void setVibration(int vibration) {
+ this.vibration = vibration;
+ }
+
+ public String getSmallIcon() {
+ return smallIcon;
+ }
+
+ public void setSmallIcon(String smallIcon) {
+ this.smallIcon = smallIcon;
+ }
+
+ public String getLargeIcon() {
+ return largeIcon;
+ }
+
+ public void setLargeIcon(String largeIcon) {
+ this.largeIcon = largeIcon;
+ }
+
+ public boolean isPlaySound() {
+ return playSound;
+ }
+
+ public void setPlaySound(boolean playSound) {
+ this.playSound = playSound;
+ }
+
+ public String getSoundName() {
+ return soundName;
+ }
+
+ public void setSoundName(String soundName) {
+ this.soundName = soundName;
+ }
+
+ public String getSoundNames() {
+ return soundNames;
+ }
+
+ public void setSoundNames(String soundNames) {
+ this.soundNames = soundNames;
+ }
+
+ public String getColor() {
+ return color;
+ }
+
+ public void setColor(String color) {
+ this.color = color;
+ }
+
+ public String getScheduleType() {
+ return scheduleType;
+ }
+
+ public void setScheduleType(String scheduleType) {
+ this.scheduleType = scheduleType;
+ }
+
+ public String getInterval() {
+ return interval;
+ }
+
+ public void setInterval(String interval) {
+ this.interval = interval;
+ }
+
+ public int getIntervalValue() {
+ return intervalValue;
+ }
+
+ public void setIntervalValue(int intervalValue) {
+ this.intervalValue = intervalValue;
+ }
+
+ public String getTag() {
+ return tag;
+ }
+
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ public Bundle getData() {
+ return data;
+ }
+
+ public void setData(Bundle data) {
+ this.data = data;
+ }
+
+ public int getActive() {
+ return active;
+ }
+
+ public void setActive(int active) {
+ this.active = active;
+ }
+
+ public int getSnoozeInterval() {
+ return snoozeInterval;
+ }
+
+ public void setSnoozeInterval(int snoozeInterval) {
+ this.snoozeInterval = snoozeInterval;
+ }
+
+ public boolean isLoopSound() {
+ return loopSound;
+ }
+
+ public void setLoopSound(boolean loopSound) {
+ this.loopSound = loopSound;
+ }
+
+ public boolean isUseBigText() {
+ return useBigText;
+ }
+
+ public void setUseBigText(boolean useBigText) {
+ this.useBigText = useBigText;
+ }
+
+ public boolean isHasButton() {
+ return hasButton;
+ }
+
+ public void setHasButton(boolean hasButton) {
+ this.hasButton = hasButton;
+ }
+
+ public double getVolume() {
+ return volume;
+ }
+
+ public void setVolume(double volume) {
+ if (volume > 1 || volume < 0) {
+ this.volume = 0.5;
+ } else {
+ this.volume = volume;
+ }
+ }
+
+ public boolean isBypassDnd() {
+ return bypassDnd;
+ }
+
+ public void setBypassDnd(boolean bypassDnd) {
+ this.bypassDnd = bypassDnd;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "AlarmModel{" +
+ "id=" + id +
+ ", second=" + second +
+ ", minute=" + minute +
+ ", hour=" + hour +
+ ", day=" + day +
+ ", month=" + month +
+ ", year=" + year +
+ ", alarmId=" + alarmId +
+ ", title='" + title + '\'' +
+ ", message='" + message + '\'' +
+ ", channel='" + channel + '\'' +
+ ", ticker='" + ticker + '\'' +
+ ", autoCancel=" + autoCancel +
+ ", vibrate=" + vibrate +
+ ", vibration=" + vibration +
+ ", smallIcon='" + smallIcon + '\'' +
+ ", largeIcon='" + largeIcon + '\'' +
+ ", playSound=" + playSound +
+ ", soundName='" + soundName + '\'' +
+ ", soundNames='" + soundNames + '\'' +
+ ", color='" + color + '\'' +
+ ", scheduleType='" + scheduleType + '\'' +
+ ", interval=" + interval +
+ ", intervalValue=" + intervalValue +
+ ", snoozeInterval=" + snoozeInterval +
+ ", tag='" + tag + '\'' +
+ ", data='" + data + '\'' +
+ ", loopSound=" + loopSound +
+ ", useBigText=" + useBigText +
+ ", hasButton=" + hasButton +
+ ", volume=" + volume +
+ ", bypassDnd=" + bypassDnd +
+ ", active=" + active +
+ '}';
+ }
+
+ public static AlarmModel fromBundle(@NonNull Bundle bundle) {
+ AlarmModel alarm = new AlarmModel();
+
+ long time = System.currentTimeMillis() / 1000;
+ alarm.setAlarmId((int) time);
+
+ alarm.setActive(1);
+ alarm.setAutoCancel(bundle.getBoolean("auto_cancel", true));
+ alarm.setChannel(bundle.getString("channel", "my_channel_id"));
+ alarm.setColor(bundle.getString("color", "red"));
+
+ Bundle data = bundle.getBundle("data");
+ alarm.setData(data);
+
+ alarm.setInterval(bundle.getString("repeat_interval", "hourly"));
+ alarm.setLargeIcon(bundle.getString("large_icon", ""));
+ alarm.setLoopSound(bundle.getBoolean("loop_sound", false));
+ alarm.setMessage(bundle.getString("message", "My Notification Message"));
+ alarm.setPlaySound(bundle.getBoolean("play_sound", true));
+ alarm.setScheduleType(bundle.getString("schedule_type", "once"));
+ alarm.setSmallIcon(bundle.getString("small_icon", "ic_launcher"));
+ alarm.setSnoozeInterval((int) bundle.getDouble("snooze_interval", 1.0));
+ alarm.setSoundName(bundle.getString("sound_name", null));
+ alarm.setSoundNames(bundle.getString("sound_names", null));
+ alarm.setTag(bundle.getString("tag", ""));
+ alarm.setTicker(bundle.getString("ticker", ""));
+ alarm.setTitle(bundle.getString("title", "My Notification Title"));
+ alarm.setVibrate(bundle.getBoolean("vibrate", true));
+ alarm.setHasButton(bundle.getBoolean("has_button", false));
+ alarm.setVibration((int) bundle.getDouble("vibration", 100.0));
+ alarm.setUseBigText(bundle.getBoolean("use_big_text", false));
+ alarm.setVolume(bundle.getDouble("volume", 0.5));
+ alarm.setIntervalValue((int) bundle.getDouble("interval_value", 1));
+ alarm.setBypassDnd(bundle.getBoolean("bypass_dnd", false));
+
+ String datetime = bundle.getString("fire_date");
+ SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.ENGLISH);
+ Calendar calendar = new GregorianCalendar();
+
+ try {
+ calendar.setTime(sdf.parse(datetime));
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+
+ alarm.setAlarmDateTime(calendar);
+ return alarm;
+ }
+
+ void setAlarmDateTime(Calendar calendar) {
+ setSecond(calendar.get(Calendar.SECOND));
+ setMinute(calendar.get(Calendar.MINUTE));
+ setHour(calendar.get(Calendar.HOUR_OF_DAY));
+ setDay(calendar.get(Calendar.DAY_OF_MONTH));
+ setMonth(calendar.get(Calendar.MONTH) + 1);
+ setYear(calendar.get(Calendar.YEAR));
+ }
+
+ Calendar getAlarmDateTime() {
+ Calendar calendar = new GregorianCalendar();
+ calendar.set(Calendar.HOUR_OF_DAY, getHour());
+ calendar.set(Calendar.MINUTE, getMinute());
+ calendar.set(Calendar.SECOND, getSecond());
+ calendar.set(Calendar.DAY_OF_MONTH, getDay());
+ calendar.set(Calendar.MONTH, getMonth() - 1);
+ calendar.set(Calendar.YEAR, getYear());
+ return calendar;
+ }
+
+ Calendar snooze() {
+ Calendar calendar = getAlarmDateTime();
+ calendar.add(Calendar.MINUTE, getSnoozeInterval());
+ setAlarmDateTime(calendar);
+ return calendar;
+ }
+
+ boolean isSameTime(AlarmModel alarm) {
+ return this.getHour() == alarm.getHour() && this.getMinute() == alarm.getMinute() &&
+ this.getSecond() == alarm.getSecond() && this.getDay() == alarm.getDay() &&
+ this.getMonth() == alarm.getMonth() && this.getYear() == alarm.getYear();
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModelCodec.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModelCodec.java
new file mode 100644
index 000000000..35129ced5
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModelCodec.java
@@ -0,0 +1,114 @@
+package com.emekalites.react.alarm.notification;
+
+import static com.google.gson.stream.JsonToken.END_OBJECT;
+
+import android.os.Bundle;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+
+public class AlarmModelCodec {
+
+ private final Gson gson = new GsonBuilder()
+ .registerTypeAdapterFactory(new TypeAdapterFactory() {
+
+ @Override
+ public TypeAdapter create(final Gson gson, TypeToken typeToken) {
+ if (!Bundle.class.isAssignableFrom(typeToken.getRawType())) {
+ return null;
+ }
+ return (TypeAdapter) new TypeAdapter() {
+ @Override
+ public void write(JsonWriter writer, Bundle bundle) throws IOException {
+ if (bundle == null) {
+ writer.nullValue();
+ return;
+ }
+ writer.beginObject();
+ for (String key : bundle.keySet()) {
+ writer.name(key);
+ Object value = bundle.get(key);
+ if (value == null) {
+ writer.nullValue();
+ } else {
+ gson.toJson(value, value.getClass(), writer);
+ }
+ }
+ writer.endObject();
+ }
+
+ @Override
+ public Bundle read(JsonReader reader) throws IOException {
+ switch (reader.peek()) {
+ case NULL:
+ reader.nextNull();
+ return null;
+ case BEGIN_OBJECT:
+ return readBundle(reader);
+ default:
+ throw new IOException("Could not read bundle at " + reader.getPath());
+ }
+ }
+
+ private Bundle readBundle(JsonReader reader) throws IOException {
+ reader.beginObject();
+ Bundle bundle = new Bundle();
+ JsonToken nextToken;
+ while ((nextToken = reader.peek()) != END_OBJECT) {
+ switch (nextToken) {
+ case NAME:
+ readNextKeyValue(reader, bundle);
+ case END_OBJECT:
+ break;
+ }
+ }
+ reader.endObject();
+ return bundle;
+ }
+
+ private void readNextKeyValue(JsonReader reader, Bundle bundle) throws IOException {
+ String name = reader.nextName();
+ // only support a small subset of possible types, enough for Joplin
+ switch (reader.peek()) {
+ case STRING:
+ bundle.putString(name, reader.nextString());
+ break;
+ case BOOLEAN:
+ bundle.putBoolean(name, reader.nextBoolean());
+ case NUMBER:
+ double doubleVal = reader.nextDouble();
+ if (Math.round(doubleVal) == doubleVal) {
+ long longVal = (long) doubleVal;
+ if (longVal >= Integer.MIN_VALUE && longVal <= Integer.MAX_VALUE) {
+ bundle.putInt(name, (int) longVal);
+ } else {
+ bundle.putLong(name, longVal);
+ }
+ } else {
+ bundle.putDouble(name, doubleVal);
+ }
+ break;
+ }
+ }
+ };
+ }
+ })
+ .create();
+
+
+ public String toJson(AlarmModel alarmModel) {
+ return gson.toJson(alarmModel);
+ }
+
+ public AlarmModel fromJson(String json) {
+ return gson.fromJson(json, AlarmModel.class);
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmReceiver.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmReceiver.java
new file mode 100644
index 000000000..ddd08ab3f
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmReceiver.java
@@ -0,0 +1,79 @@
+package com.emekalites.react.alarm.notification;
+
+import android.app.Application;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+
+import java.util.ArrayList;
+
+public class AlarmReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent != null) {
+ final AlarmDatabase alarmDB = new AlarmDatabase(context);
+ AlarmUtil alarmUtil = new AlarmUtil((Application) context.getApplicationContext());
+
+ try {
+ String intentType = intent.getExtras().getString("intentType");
+ if (Constants.ADD_INTENT.equals(intentType)) {
+ int id = intent.getExtras().getInt("PendingId");
+ try {
+ AlarmModel alarm = alarmDB.getAlarm(id);
+ alarmUtil.sendNotification(alarm);
+ alarmUtil.setBootReceiver();
+
+ ArrayList alarms = alarmDB.getAlarmList(1);
+ Log.d(Constants.TAG, "alarm start: " + alarm.toString() + ", alarms left: " + alarms.size());
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Failed to add alarm", e);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Received invalid intent", e);
+ }
+
+ String action = intent.getAction();
+ if (action != null) {
+ Log.i(Constants.TAG, "ACTION: " + action);
+ switch (action) {
+ case Constants.NOTIFICATION_ACTION_SNOOZE:
+ int id = intent.getExtras().getInt("SnoozeAlarmId");
+
+ try {
+ AlarmModel alarm = alarmDB.getAlarm(id);
+ alarmUtil.snoozeAlarm(alarm);
+ Log.i(Constants.TAG, "alarm snoozed: " + alarm.toString());
+
+ alarmUtil.removeFiredNotification(alarm.getId());
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Failed to snooze alarm", e);
+ }
+ break;
+
+ case Constants.NOTIFICATION_ACTION_DISMISS:
+ id = intent.getExtras().getInt("AlarmId");
+
+ try {
+ AlarmModel alarm = alarmDB.getAlarm(id);
+ Log.i(Constants.TAG, "Cancel alarm: " + alarm.toString());
+
+ // emit notification dismissed
+ // TODO also send all user-provided args back
+ ANModule.getReactAppContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("OnNotificationDismissed", "{\"id\": \"" + alarm.getId() + "\"}");
+
+ alarmUtil.removeFiredNotification(alarm.getId());
+ alarmUtil.cancelAlarm(alarm, false); // TODO why false?
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Failed to dismiss alarm", e);
+ }
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmUtil.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmUtil.java
new file mode 100644
index 000000000..8dc782347
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmUtil.java
@@ -0,0 +1,476 @@
+package com.emekalites.react.alarm.notification;
+
+import android.app.AlarmManager;
+import android.app.Application;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.media.AudioManager;
+import android.media.RingtoneManager;
+import android.os.Build;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.core.app.NotificationCompat;
+
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Iterator;
+
+import static com.emekalites.react.alarm.notification.Constants.ADD_INTENT;
+import static com.emekalites.react.alarm.notification.Constants.NOTIFICATION_ACTION_DISMISS;
+import static com.emekalites.react.alarm.notification.Constants.NOTIFICATION_ACTION_SNOOZE;
+
+class AlarmUtil {
+
+ private static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
+ private int defaultFlags = 0;
+
+ private final Context context;
+ private final AlarmDatabase alarmDB;
+
+ AlarmUtil(Application context) {
+ this.context = context;
+ this.alarmDB = new AlarmDatabase(context);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ defaultFlags = PendingIntent.FLAG_IMMUTABLE;
+ }
+ }
+
+ private Class> getMainActivityClass() {
+ try {
+ String packageName = context.getPackageName();
+ Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
+ String className = launchIntent.getComponent().getClassName();
+ Log.d(Constants.TAG, "main activity classname: " + className);
+ return Class.forName(className);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Could not load main activity class", e);
+ return null;
+ }
+ }
+
+ private AlarmManager getAlarmManager() {
+ return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ }
+
+ private NotificationManager getNotificationManager() {
+ return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ boolean checkAlarm(ArrayList alarms, AlarmModel alarm) {
+ for (AlarmModel aAlarm : alarms) {
+ if (aAlarm.isSameTime(alarm) && aAlarm.getActive() == 1) {
+ Toast.makeText(context, "You have already set this Alarm", Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void setBootReceiver() {
+ ArrayList alarms = alarmDB.getAlarmList(1);
+ if (alarms.size() > 0) {
+ enableBootReceiver(context);
+ } else {
+ disableBootReceiver(context);
+ }
+ }
+
+ void setAlarm(AlarmModel alarm) {
+ Log.i(Constants.TAG, "Set alarm " + alarm);
+
+ Calendar calendar = alarm.getAlarmDateTime();
+ int alarmId = alarm.getAlarmId();
+
+ Intent intent = new Intent(context, AlarmReceiver.class);
+ intent.putExtra("intentType", ADD_INTENT);
+ intent.putExtra("PendingId", alarm.getId());
+
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(context, alarmId, intent, defaultFlags);
+ AlarmManager alarmManager = this.getAlarmManager();
+
+ String scheduleType = alarm.getScheduleType();
+
+ if (scheduleType.equals("once")) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
+ } else {
+ alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
+ }
+ } else if (scheduleType.equals("repeat")) {
+ long interval = this.getInterval(alarm.getInterval(), alarm.getIntervalValue());
+ alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), interval, alarmIntent);
+ } else {
+ Log.w(Constants.TAG, "Schedule type should either be once or repeat");
+ return;
+ }
+
+ this.setBootReceiver();
+ }
+
+ void snoozeAlarm(AlarmModel alarm) {
+ Log.i(Constants.TAG, "Snooze alarm: " + alarm.toString());
+
+ Calendar calendar = alarm.snooze();
+
+ long time = System.currentTimeMillis() / 1000;
+
+ alarm.setAlarmId((int) time);
+ // TODO looks like this sets a new id and then tries to update the row in DB
+ // how's that supposed to work?
+ alarmDB.update(alarm);
+
+ int alarmId = alarm.getAlarmId();
+
+ Intent intent = new Intent(context, AlarmReceiver.class);
+ intent.putExtra("intentType", ADD_INTENT);
+ intent.putExtra("PendingId", alarm.getId());
+
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(context, alarmId, intent, defaultFlags);
+ AlarmManager alarmManager = this.getAlarmManager();
+
+ String scheduleType = alarm.getScheduleType();
+
+ if (scheduleType.equals("once")) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
+ } else {
+ alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
+ }
+ } else if (scheduleType.equals("repeat")) {
+ long interval = this.getInterval(alarm.getInterval(), alarm.getIntervalValue());
+
+ alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), interval, alarmIntent);
+ } else {
+ Log.w(Constants.TAG, "Schedule type should either be once or repeat");
+ }
+ }
+
+ long getInterval(String interval, int value) {
+ long duration = 1;
+
+ switch (interval) {
+ case "minutely":
+ duration = value;
+ break;
+ case "hourly":
+ duration = 60 * value;
+ break;
+ case "daily":
+ duration = 60 * 24;
+ break;
+ case "weekly":
+ duration = 60 * 24 * 7;
+ break;
+ }
+
+ return duration * 60 * 1000;
+ }
+
+ void doCancelAlarm(int id) {
+ try {
+ AlarmModel alarm = alarmDB.getAlarm(id);
+ this.cancelAlarm(alarm, false);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Could not cancel alarm with id " + id, e);
+ }
+ }
+
+ void deleteAlarm(int id) {
+ try {
+ AlarmModel alarm = alarmDB.getAlarm(id);
+ this.cancelAlarm(alarm, true);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Could not delete alarm with id " + id, e);
+ }
+ }
+
+ void deleteRepeatingAlarm(int id) {
+ try {
+ AlarmModel alarm = alarmDB.getAlarm(id);
+
+ String scheduleType = alarm.getScheduleType();
+ if (scheduleType.equals("repeat")) {
+ this.stopAlarm(alarm);
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Could not delete repeating alarm with id " + id, e);
+ }
+ }
+
+ void cancelAlarm(AlarmModel alarm, boolean delete) {
+ String scheduleType = alarm.getScheduleType();
+ if (scheduleType.equals("once") || delete) {
+ this.stopAlarm(alarm);
+ }
+ }
+
+ void stopAlarm(AlarmModel alarm) {
+ AlarmManager alarmManager = this.getAlarmManager();
+
+ int alarmId = alarm.getAlarmId();
+
+ Intent intent = new Intent(context, AlarmReceiver.class);
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(context, alarmId, intent, defaultFlags | PendingIntent.FLAG_UPDATE_CURRENT);
+ alarmManager.cancel(alarmIntent);
+
+ alarmDB.delete(alarm.getId());
+
+ this.setBootReceiver();
+ }
+
+ private void enableBootReceiver(Context context) {
+ ComponentName receiver = new ComponentName(context, AlarmBootReceiver.class);
+ PackageManager pm = context.getPackageManager();
+
+ int setting = pm.getComponentEnabledSetting(receiver);
+ if (setting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED ||
+ setting == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
+ Log.i(Constants.TAG, "Enable boot receiver");
+ pm.setComponentEnabledSetting(receiver,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP);
+ } else {
+ Log.i(Constants.TAG, "Boot receiver already enabled");
+ }
+ }
+
+ private void disableBootReceiver(Context context) {
+ Log.i(Constants.TAG, "Disable boot receiver");
+
+ ComponentName receiver = new ComponentName(context, AlarmBootReceiver.class);
+ PackageManager pm = context.getPackageManager();
+
+ pm.setComponentEnabledSetting(receiver,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ }
+
+ private PendingIntent createOnDismissedIntent(Context context, int notificationId) {
+ Intent intent = new Intent(context, AlarmDismissReceiver.class);
+ intent.putExtra(Constants.NOTIFICATION_ID, notificationId);
+ return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, defaultFlags);
+ }
+
+ void sendNotification(AlarmModel alarm) {
+ try {
+ Class> intentClass = getMainActivityClass();
+
+ if (intentClass == null) {
+ Log.e(Constants.TAG, "No activity class found for the notification");
+ return;
+ }
+
+ NotificationManager mNotificationManager = getNotificationManager();
+ int notificationID = alarm.getAlarmId();
+
+ // title
+ String title = alarm.getTitle();
+ if (title == null || title.equals("")) {
+ ApplicationInfo appInfo = context.getApplicationInfo();
+ title = context.getPackageManager().getApplicationLabel(appInfo).toString();
+ }
+
+ // message
+ // TODO move to AlarmModel constructor?
+ String message = alarm.getMessage();
+ if (message == null || message.equals("")) {
+ Log.e(Constants.TAG, "Cannot send to notification centre because there is no 'message' found");
+ return;
+ }
+
+ // channel
+ // TODO move to AlarmModel constructor?
+ String channelID = alarm.getChannel();
+ if (channelID == null || channelID.equals("")) {
+ Log.e(Constants.TAG, "Cannot send to notification centre because there is no 'channel' found");
+ return;
+ }
+
+ Resources res = context.getResources();
+ String packageName = context.getPackageName();
+
+ //icon
+ // TODO move to AlarmModel constructor?
+ int smallIconResId;
+ String smallIcon = alarm.getSmallIcon();
+ if (smallIcon != null && !smallIcon.equals("")) {
+ smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName);
+ } else {
+ smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
+ }
+
+ Intent intent = new Intent(context, intentClass);
+ intent.setAction(Constants.NOTIFICATION_ACTION_CLICK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+
+ intent.putExtra(Constants.NOTIFICATION_ID, alarm.getId());
+ intent.putExtra("data", alarm.getData());
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, notificationID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, channelID)
+ .setSmallIcon(smallIconResId)
+ .setContentTitle(title)
+ .setContentText(message)
+ .setTicker(alarm.getTicker())
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setAutoCancel(alarm.isAutoCancel())
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
+ .setSound(null)
+ .setDeleteIntent(createOnDismissedIntent(context, alarm.getId()));
+
+ if (alarm.isPlaySound()) {
+ // TODO use user-supplied sound if available
+ mBuilder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), AudioManager.STREAM_NOTIFICATION);
+ }
+
+ long vibration = alarm.getVibration();
+
+ long[] vibrationPattern = vibration == 0 ? DEFAULT_VIBRATE_PATTERN : new long[]{0, vibration, 1000, vibration};
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel mChannel = new NotificationChannel(channelID, "Alarm Notify", NotificationManager.IMPORTANCE_HIGH);
+ mChannel.enableLights(true);
+
+ String color = alarm.getColor();
+ if (color != null && !color.equals("")) {
+ mChannel.setLightColor(Color.parseColor(color));
+ }
+
+ if (mChannel.canBypassDnd()) {
+ mChannel.setBypassDnd(alarm.isBypassDnd());
+ }
+
+ if (alarm.isVibrate()) {
+ mChannel.setVibrationPattern(vibrationPattern);
+ mChannel.enableVibration(true);
+ }
+
+ mNotificationManager.createNotificationChannel(mChannel);
+ mBuilder.setChannelId(channelID);
+ } else {
+ // set vibration
+ mBuilder.setVibrate(alarm.isVibrate() ? vibrationPattern : null);
+ }
+
+ //color
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ String color = alarm.getColor();
+ if (color != null && !color.equals("")) {
+ mBuilder.setColor(Color.parseColor(color));
+ }
+ }
+
+ mBuilder.setContentIntent(pendingIntent);
+
+ if (alarm.isHasButton()) {
+ Intent dismissIntent = new Intent(context, AlarmReceiver.class);
+ dismissIntent.setAction(NOTIFICATION_ACTION_DISMISS);
+ dismissIntent.putExtra("AlarmId", alarm.getId());
+ PendingIntent pendingDismiss = PendingIntent.getBroadcast(context, notificationID, dismissIntent, defaultFlags | PendingIntent.FLAG_UPDATE_CURRENT);
+ NotificationCompat.Action dismissAction = new NotificationCompat.Action(android.R.drawable.ic_lock_idle_alarm, "DISMISS", pendingDismiss);
+ mBuilder.addAction(dismissAction);
+
+ Intent snoozeIntent = new Intent(context, AlarmReceiver.class);
+ snoozeIntent.setAction(NOTIFICATION_ACTION_SNOOZE);
+ snoozeIntent.putExtra("SnoozeAlarmId", alarm.getId());
+ PendingIntent pendingSnooze = PendingIntent.getBroadcast(context, notificationID, snoozeIntent, defaultFlags | PendingIntent.FLAG_UPDATE_CURRENT);
+ NotificationCompat.Action snoozeAction = new NotificationCompat.Action(R.drawable.ic_snooze, "SNOOZE", pendingSnooze);
+ mBuilder.addAction(snoozeAction);
+ }
+
+ //use big text
+ if (alarm.isUseBigText()) {
+ mBuilder = mBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message));
+ }
+
+ //large icon
+ String largeIcon = alarm.getLargeIcon();
+ if (largeIcon != null && !largeIcon.equals("") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ int largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName);
+ Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
+ if (largeIconResId != 0) {
+ mBuilder.setLargeIcon(largeIconBitmap);
+ }
+ }
+
+ // set tag and push notification
+ Notification notification = mBuilder.build();
+
+ String tag = alarm.getTag();
+ if (tag != null && !tag.equals("")) {
+ mNotificationManager.notify(tag, notificationID, notification);
+ } else {
+ Log.i(Constants.TAG, "Notification done");
+ mNotificationManager.notify(notificationID, notification);
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Failed to send notification", e);
+ }
+ }
+
+ void removeFiredNotification(int id) {
+ try {
+ AlarmModel alarm = alarmDB.getAlarm(id);
+ getNotificationManager().cancel(alarm.getAlarmId());
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Could not remove fired notification with id " + id, e);
+ }
+ }
+
+ void removeAllFiredNotifications() {
+ getNotificationManager().cancelAll();
+ }
+
+ ArrayList getAlarms() {
+ return alarmDB.getAlarmList(1);
+ }
+
+ WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException {
+ WritableMap map = new WritableNativeMap();
+
+ Iterator iterator = jsonObject.keys();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ Object value = jsonObject.get(key);
+ if (value instanceof JSONObject) {
+ map.putMap(key, convertJsonToMap((JSONObject) value));
+ } else if (value instanceof Boolean) {
+ map.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Integer) {
+ map.putInt(key, (Integer) value);
+ } else if (value instanceof Double) {
+ map.putDouble(key, (Double) value);
+ } else if (value instanceof String) {
+ map.putString(key, (String) value);
+ } else {
+ map.putString(key, value.toString());
+ }
+ }
+ return map;
+ }
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/Constants.java b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/Constants.java
new file mode 100644
index 000000000..b517ccc7b
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/Constants.java
@@ -0,0 +1,14 @@
+package com.emekalites.react.alarm.notification;
+
+class Constants {
+ static final String TAG = "RNAlarmNotification";
+
+ // TODO convert to action
+ static final String ADD_INTENT = "com.emekalites.react.alarm.notification.ADD_INTENT";
+
+ static final String NOTIFICATION_ID = "com.emekalites.react.alarm.notification.NOTIFICATION_ID";
+
+ static final String NOTIFICATION_ACTION_DISMISS = "com.emekalites.react.alarm.notification.ACTION_DISMISS";
+ static final String NOTIFICATION_ACTION_SNOOZE = "com.emekalites.react.alarm.notification.ACTION_SNOOZE";
+ static final String NOTIFICATION_ACTION_CLICK = "com.emekalites.react.alarm.notification.ACTION_CLICK";
+}
diff --git a/packages/react-native-alarm-notification/android/src/main/res/drawable/ic_snooze.xml b/packages/react-native-alarm-notification/android/src/main/res/drawable/ic_snooze.xml
new file mode 100644
index 000000000..53e126281
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/main/res/drawable/ic_snooze.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/packages/react-native-alarm-notification/android/src/test/java/com/emekalites/react/alarm/notification/AlarmModelTest.java b/packages/react-native-alarm-notification/android/src/test/java/com/emekalites/react/alarm/notification/AlarmModelTest.java
new file mode 100644
index 000000000..132fb48cf
--- /dev/null
+++ b/packages/react-native-alarm-notification/android/src/test/java/com/emekalites/react/alarm/notification/AlarmModelTest.java
@@ -0,0 +1,33 @@
+package com.emekalites.react.alarm.notification;
+
+import android.os.Bundle;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AlarmModelTest {
+
+ @Test
+ public void testToJson() {
+ Bundle bundle = new Bundle();
+ bundle.putString("title", "alarm title");
+ bundle.putString("fire_date", "2021-11-27 13:48:00");
+
+ Bundle data = new Bundle();
+ data.putString("string_data_key", "string_data_value");
+ data.putInt("int_data_key", 42);
+
+ bundle.putBundle("data", data);
+
+ AlarmModel alarm = AlarmModel.fromBundle(bundle);
+
+ AlarmModelCodec codec = new AlarmModelCodec();
+
+ String json = codec.toJson(alarm);
+ System.out.println(json);
+ System.out.println(codec.fromJson(json));
+ }
+
+}
\ No newline at end of file
diff --git a/packages/react-native-alarm-notification/index.js b/packages/react-native-alarm-notification/index.js
new file mode 100644
index 000000000..1f95be08e
--- /dev/null
+++ b/packages/react-native-alarm-notification/index.js
@@ -0,0 +1,174 @@
+import { NativeModules } from 'react-native';
+
+const { RNAlarmNotification } = NativeModules;
+const ReactNativeAN = {};
+
+const parseDateString = (string) => {
+ const splits = string.split(' ');
+ const dateSplits = splits[0].split('-');
+ const timeSplits = splits[1].split(':');
+
+ const year = dateSplits[2];
+ const month = dateSplits[1];
+ const day = dateSplits[0];
+
+ const hours = timeSplits[0];
+ const minutes = timeSplits[1];
+ const seconds = timeSplits[2];
+
+ return new Date(year, month - 1, day, hours, minutes, seconds);
+};
+
+ReactNativeAN.scheduleAlarm = async (details) => {
+ if (!details.fire_date || (details.fire_date && details.fire_date === '')) {
+ throw new Error('failed to schedule alarm because fire date is missing');
+ }
+
+ const past = parseDateString(details.fire_date);
+ const today = new Date();
+ if (past < today) {
+ throw new Error(
+ 'failed to schedule alarm because fire date is in the past'
+ );
+ }
+
+ const repeatInterval = details.repeat_interval || 'hourly';
+ const intervalValue = details.interval_value || 1;
+ if (isNaN(intervalValue)) {
+ throw new Error('interval value should be a number');
+ }
+
+ if (
+ repeatInterval === 'minutely' &&
+ (intervalValue < 1 || intervalValue > 59)
+ ) {
+ throw new Error('interval value should be between 1 and 59 minutes');
+ }
+
+ if (
+ repeatInterval === 'hourly' &&
+ (intervalValue < 1 || intervalValue > 23)
+ ) {
+ throw new Error('interval value should be between 1 and 23 hours');
+ }
+
+ const data = {
+ ...details,
+ has_button: details.has_button || false,
+ vibrate: details.vibrate || true,
+ play_sound: details.play_sound || true,
+ schedule_type: details.schedule_type || 'once',
+ repeat_interval: details.repeat_interval || 'hourly',
+ interval_value: details.interval_value || 1,
+ volume: details.volume || 0.5,
+ sound_name: details.sound_name || '',
+ snooze_interval: details.snooze_interval || 1,
+ data: details.data || '',
+ };
+
+ return await RNAlarmNotification.scheduleAlarm(data);
+};
+
+ReactNativeAN.sendNotification = (details) => {
+ const data = {
+ ...details,
+ has_button: false,
+ vibrate: details.vibrate || true,
+ play_sound: details.play_sound || true,
+ schedule_type: details.schedule_type || 'once',
+ volume: details.volume || 0.5,
+ sound_name: details.sound_name || '',
+ snooze_interval: details.snooze_interval || 1,
+ data: details.data || '',
+ };
+
+ RNAlarmNotification.sendNotification(data);
+};
+
+ReactNativeAN.deleteAlarm = (id) => {
+ if (!id) {
+ throw new Error('id is required to delete alarm');
+ }
+
+ RNAlarmNotification.deleteAlarm(id);
+};
+
+ReactNativeAN.deleteRepeatingAlarm = (id) => {
+ if (!id) {
+ throw new Error('id is required to delete alarm');
+ }
+
+ RNAlarmNotification.deleteRepeatingAlarm(id);
+};
+
+ReactNativeAN.stopAlarmSound = () => {
+ return RNAlarmNotification.stopAlarmSound();
+};
+
+ReactNativeAN.removeFiredNotification = (id) => {
+ if (!id) {
+ throw new Error('id is required to remove notification');
+ }
+
+ RNAlarmNotification.removeFiredNotification(id);
+};
+
+ReactNativeAN.removeAllFiredNotifications = () => {
+ RNAlarmNotification.removeAllFiredNotifications();
+};
+
+ReactNativeAN.getScheduledAlarms = async () => {
+ return await RNAlarmNotification.getScheduledAlarms();
+};
+
+// ios request permission
+ReactNativeAN.requestPermissions = async (permissions) => {
+ let requestedPermissions = {
+ alert: true,
+ badge: true,
+ sound: true,
+ };
+
+ if (permissions) {
+ requestedPermissions = {
+ alert: !!permissions.alert,
+ badge: !!permissions.badge,
+ sound: !!permissions.sound,
+ };
+ }
+
+ return await RNAlarmNotification.requestPermissions(requestedPermissions);
+};
+
+// ios check permission
+ReactNativeAN.checkPermissions = (callback) => {
+ RNAlarmNotification.checkPermissions(callback);
+};
+
+ReactNativeAN.parseDate = (rawDate) => {
+ let hours;
+ let day;
+ let month;
+
+ if (rawDate.getHours().toString().length === 1) {
+ hours = `0${rawDate.getHours()}`;
+ } else {
+ hours = `${rawDate.getHours()}`;
+ }
+
+ if (rawDate.getDate().toString().length === 1) {
+ day = `0${rawDate.getDate()}`;
+ } else {
+ day = `${rawDate.getDate()}`;
+ }
+
+ if (rawDate.getMonth().toString().length === 1) {
+ month = `0${rawDate.getMonth() + 1}`;
+ } else {
+ month = `${rawDate.getMonth() + 1}`;
+ }
+
+ return `${day}-${month}-${rawDate.getFullYear()} ${hours}:${rawDate.getMinutes()}:${rawDate.getSeconds()}`;
+};
+
+export default ReactNativeAN;
diff --git a/packages/react-native-alarm-notification/ios/RnAlarmNotification.h b/packages/react-native-alarm-notification/ios/RnAlarmNotification.h
new file mode 100644
index 000000000..793cb98f5
--- /dev/null
+++ b/packages/react-native-alarm-notification/ios/RnAlarmNotification.h
@@ -0,0 +1,9 @@
+#import
+#import
+
+#import
+
+@interface RnAlarmNotification : RCTEventEmitter
++ (void)didReceiveNotificationResponse:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0));
++ (void)didReceiveNotification:(UNNotification *)notification API_AVAILABLE(ios(10.0));
+@end
diff --git a/packages/react-native-alarm-notification/ios/RnAlarmNotification.m b/packages/react-native-alarm-notification/ios/RnAlarmNotification.m
new file mode 100644
index 000000000..0fb8d6745
--- /dev/null
+++ b/packages/react-native-alarm-notification/ios/RnAlarmNotification.m
@@ -0,0 +1,794 @@
+#import "RnAlarmNotification.h"
+
+#import
+
+#import
+#import
+#import
+#import
+#import
+#import
+
+static NSString *const kLocalNotificationReceived = @"LocalNotificationReceived";
+static NSString *const kLocalNotificationDismissed = @"LocalNotificationDismissed";
+
+static AVAudioPlayer *player;
+static id _sharedInstance = nil;
+
+@implementation RnAlarmNotification
+
++(instancetype)sharedInstance {
+ static dispatch_once_t p;
+ dispatch_once(&p, ^{
+ _sharedInstance = [[self alloc] init];
+ });
+ return _sharedInstance;
+}
+
+API_AVAILABLE(ios(10.0))
+static NSDictionary *RCTFormatUNNotification(UNNotification *notification) {
+ NSMutableDictionary *formattedNotification = [NSMutableDictionary dictionary];
+ UNNotificationContent *content = notification.request.content;
+
+ formattedNotification[@"id"] = notification.request.identifier;
+ formattedNotification[@"data"] = RCTNullIfNil([content.userInfo objectForKey:@"data"]);
+
+ return formattedNotification;
+}
+
+static NSDateComponents *parseDate(NSString *dateString) {
+ NSArray *fire_date = [dateString componentsSeparatedByString:@" "];
+ NSString *date = fire_date[0];
+ NSString *time = fire_date[1];
+
+ NSArray *splitDate = [date componentsSeparatedByString:@"-"];
+ NSArray *splitHour = [time componentsSeparatedByString:@":"];
+
+ NSString *strNumDay = splitDate[0];
+ NSString *strNumMonth = splitDate[1];
+ NSString *strNumYear = splitDate[2];
+
+ NSString *strNumHour = splitHour[0];
+ NSString *strNumMinute = splitHour[1];
+ NSString *strNumSecond = splitHour[2];
+
+ // Configure the trigger for date
+ NSDateComponents *fireDate = [[NSDateComponents alloc] init];
+ fireDate.day = [strNumDay intValue];
+ fireDate.month = [strNumMonth intValue];
+ fireDate.year = [strNumYear intValue];
+ fireDate.hour = [strNumHour intValue];
+ fireDate.minute = [strNumMinute intValue];
+ fireDate.second = [strNumSecond intValue];
+ fireDate.timeZone = [NSTimeZone defaultTimeZone];
+
+ return fireDate;
+}
+
+static NSDateComponents *dateToComponents(NSDate *date) {
+ NSDateComponents *fireDate = [[NSDateComponents alloc] init];
+
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+
+ [formatter setDateFormat:@"yyyy"];
+ NSString *year = [formatter stringFromDate:date];
+
+ [formatter setDateFormat:@"MM"];
+ NSString *month = [formatter stringFromDate:date];
+
+ [formatter setDateFormat:@"dd"];
+ NSString *day = [formatter stringFromDate:date];
+
+ [formatter setDateFormat:@"HH"];
+ NSString *hour = [formatter stringFromDate:date];
+
+ [formatter setDateFormat:@"mm"];
+ NSString *minute = [formatter stringFromDate:date];
+
+ [formatter setDateFormat:@"ss"];
+ NSString *second = [formatter stringFromDate:date];
+
+ fireDate.day = [day intValue];
+ fireDate.month = [month intValue];
+ fireDate.year = [year intValue];
+ fireDate.hour = [hour intValue];
+ fireDate.minute = [minute intValue];
+ fireDate.second = [second intValue];
+ fireDate.timeZone = [NSTimeZone defaultTimeZone];
+
+ return fireDate;
+}
+
+static NSString *stringify(NSDictionary *notification) {
+ NSError *error;
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:notification options:0 error:&error];
+
+ if (! jsonData) {
+ NSLog(@"Got an error: %@", error);
+ return @"bad json";
+ } else {
+ NSString * jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+
+ return jsonString;
+ }
+}
+
+- (dispatch_queue_t)methodQueue {
+ return dispatch_get_main_queue();
+}
+
+RCT_EXPORT_MODULE(RNAlarmNotification);
+
++ (void)vibratePhone {
+ NSLog(@"vibratePhone %@", @"here");
+ if([[UIDevice currentDevice].model isEqualToString:@"iPhone"]) {
+ AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
+ } else {
+ AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
+ }
+}
+
++ (void) didReceiveNotification:(UNNotification *)notification API_AVAILABLE(ios(10.0)){
+ NSLog(@"content: %@", notification.request.content.userInfo);
+ NSLog(@"alarm id: %@", notification.request.identifier);
+
+ NSNumber *vibrate = [notification.request.content.userInfo objectForKey:@"vibrate"];
+ NSLog(@"vibrate: %@", vibrate);
+
+ NSNumber *sound = [notification.request.content.userInfo objectForKey:@"sound"];
+
+ if([vibrate isEqualToNumber: [NSNumber numberWithInt: 1]]){
+ NSLog(@"do vibrate now");
+ [RnAlarmNotification vibratePhone];
+ }
+
+ if([sound isEqualToNumber: [NSNumber numberWithInt: 1]]){
+ [RnAlarmNotification playSound:notification];
+ }
+
+ NSString *scheduleType = [notification.request.content.userInfo objectForKey:@"schedule_type"];
+ if([scheduleType isEqualToString:@"repeat"]){
+ [RnAlarmNotification repeatAlarm:notification];
+ }
+}
+
++ (void)didReceiveNotificationResponse:(UNNotificationResponse *)response
+API_AVAILABLE(ios(10.0)) {
+ NSLog(@"show notification");
+ [[UIApplication sharedApplication] setIdleTimerDisabled:NO];
+ if ([response.notification.request.content.categoryIdentifier isEqualToString:@"CUSTOM_ACTIONS"]) {
+ if ([response.actionIdentifier isEqualToString:@"SNOOZE_ACTION"]) {
+ [RnAlarmNotification snoozeAlarm:response.notification];
+ } else if ([response.actionIdentifier isEqualToString:@"DISMISS_ACTION"]) {
+ NSLog(@"do dismiss");
+ [RnAlarmNotification stopSound];
+
+ NSMutableDictionary *notification = [NSMutableDictionary dictionary];
+ notification[@"id"] = response.notification.request.identifier;
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:kLocalNotificationDismissed
+ object:self
+ userInfo:notification];
+ }
+ }
+
+ // send notification
+ [[NSNotificationCenter defaultCenter] postNotificationName:kLocalNotificationReceived
+ object:self
+ userInfo:RCTFormatUNNotification(response.notification)];
+}
+
+- (void)startObserving {
+ // receive notification
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(handleLocalNotificationReceived:) name:kLocalNotificationReceived
+ object:nil];
+
+ // dismiss notification
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(handleLocalNotificationDismissed:) name:kLocalNotificationDismissed
+ object:nil];
+}
+
+- (NSArray *)supportedEvents {
+ return @[@"OnNotificationOpened", @"OnNotificationDismissed"];
+}
+
+- (void)handleLocalNotificationReceived:(NSNotification *)notification {
+ // send to js
+ [self sendEventWithName:@"OnNotificationOpened" body: stringify(notification.userInfo)];
+}
+
+- (void)handleLocalNotificationDismissed:(NSNotification *)notification {
+ // send to js
+ [self sendEventWithName:@"OnNotificationDismissed" body: stringify(notification.userInfo)];
+}
+
+- (void)stopObserving {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
++ (void)playSound:(UNNotification *)notification API_AVAILABLE(ios(10.0)){
+ @try {
+ NSString *soundName = [notification.request.content.userInfo objectForKey:@"sound_name"];
+ NSNumber *loopSound = [notification.request.content.userInfo objectForKey:@"loop_sound"];
+ NSString *volume = [notification.request.content.userInfo objectForKey:@"volume"];
+
+ NSLog(@"do play sound now: %@", soundName);
+ NSLog(@"loop sound: %@", loopSound);
+ NSLog(@"volume sound: %@", volume);
+
+// AVAudioSession *session = [AVAudioSession sharedInstance];
+// [session setCategory:AVAudioSessionCategoryPlayback
+// withOptions:AVAudioSessionCategoryOptionMixWithOthers
+// error:nil];
+// [session setActive:true error:nil];
+ //[session setMode:AVAudioSessionModeDefault error:nil]; // optional
+
+// NSError *playerError = nil;
+
+// if([RnAlarmNotification checkStringIsNotEmpty:soundName]){
+// NSLog(@"soundName: %@", soundName);
+//
+// NSString *path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:soundName];
+//
+// NSString* soundPathEscaped = [path stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
+// NSURL *soundUri = [NSURL URLWithString:soundPathEscaped];
+//
+// NSLog(@"sound path: %@", soundUri);
+//
+// if(player){
+// [player stop];
+// player = nil;
+// }
+//
+// player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundUri
+// error:&playerError];
+//
+// if(playerError) {
+// NSLog(@"[AppDelegate] audioPlayerError: %@", playerError);
+// } else if (player){
+// @synchronized(self){
+// player.delegate = (id)self;;
+// player.enableRate = YES;
+// [player prepareToPlay];
+//
+// NSLog(@"sound volume: %@", RCTNullIfNil(volume));
+// // set volume
+// player.volume = [volume floatValue];
+//
+// NSLog(@"sound loop: %@", loopSound);
+// // enable/disable loop
+// if ([loopSound isEqualToNumber: [NSNumber numberWithInt: 1]]) {
+// player.numberOfLoops = -1;
+// } else {
+// player.numberOfLoops = 0;
+// }
+//
+// [player play];
+// }
+// }
+// }
+ } @catch(NSException *exception){
+ NSLog(@"%@", exception.reason);
+ }
+}
+
++ (void)stopSound {
+ @try {
+ if (player) {
+ [player stop];
+ player.currentTime = 0;
+ }
+ } @catch(NSException *exception){
+ NSLog(@"%@", exception.reason);
+ }
+}
+
++ (void)repeatAlarm:(UNNotification *)notification API_AVAILABLE(ios(10.0)) {
+ [RnAlarmNotification stopSound];
+
+ @try {
+ if (@available(iOS 10.0, *)) {
+ UNNotificationContent *contentInfo = notification.request.content;
+ UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
+
+ content.title = contentInfo.title;
+ content.body = contentInfo.body;
+
+ NSString *scheduleType = [contentInfo.userInfo objectForKey:@"schedule_type"];
+ NSLog(@"schedule type: %@", scheduleType);
+
+ NSNumber *has_button = [contentInfo.userInfo objectForKey:@"has_button"];
+
+ // set buttons
+ if([has_button isEqualToNumber: [NSNumber numberWithInt: 1]]){
+ content.categoryIdentifier = @"CUSTOM_ACTIONS";
+ }
+
+ // set alarm date
+ NSString *fire_date = [contentInfo.userInfo objectForKey:@"fire_date"];
+
+ NSDateComponents *fireDate = parseDate(fire_date);
+
+ NSString *repeat_interval = [contentInfo.userInfo objectForKey:@"repeat_interval"];
+ NSNumber *interval_value = [contentInfo.userInfo objectForKey:@"interval_value"];
+ NSLog(@"schedule repeat interval %@", repeat_interval);
+
+ if([repeat_interval isEqualToString:@"minutely"]){
+ fireDate.minute = fireDate.minute + [interval_value intValue];
+ } else if([repeat_interval isEqualToString:@"hourly"]) {
+ fireDate.hour = fireDate.hour + [interval_value intValue];
+ } else if([repeat_interval isEqualToString:@"daily"]) {
+ fireDate.day = fireDate.day + 1;
+ } else if([repeat_interval isEqualToString:@"weekly"]) {
+ fireDate.weekday = fireDate.weekday + 1;
+ }
+
+ NSLog(@"------ next fire date: %@", fireDate);
+
+ // date to string
+ NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
+ NSDate *dateString = [gregorianCalendar dateFromComponents:fireDate];
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+ [formatter setDateFormat:@"dd-MM-yyyy HH:mm:ss"];
+ NSString *stringFromDate = [formatter stringFromDate:dateString];
+ NSLog(@"%@", stringFromDate);
+
+ UNCalendarNotificationTrigger* trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:fireDate repeats:NO];
+
+ // alarm id
+ NSString *alarmId = [contentInfo.userInfo objectForKey:@"alarmId"];
+
+ NSString *soundName = [contentInfo.userInfo objectForKey:@"sound_name"];
+ NSNumber *playSound = [contentInfo.userInfo objectForKey:@"sound"];
+
+ content.userInfo = @{
+ @"alarmId": alarmId,
+ @"sound": playSound,
+ @"vibrate": [contentInfo.userInfo objectForKey:@"vibrate"],
+ @"data": [contentInfo.userInfo objectForKey:@"data"],
+ @"fire_date": stringFromDate,
+ @"sound_name": soundName,
+ @"loop_sound": [contentInfo.userInfo objectForKey:@"loop_sound"],
+ @"volume": [contentInfo.userInfo objectForKey:@"volume"],
+ @"has_button": [contentInfo.userInfo objectForKey:@"has_button"],
+ @"schedule_type": [contentInfo.userInfo objectForKey:@"schedule_type"],
+ @"repeat_interval": [contentInfo.userInfo objectForKey:@"repeat_interval"],
+ @"interval_value": [contentInfo.userInfo objectForKey:@"interval_value"],
+ @"snooze_interval": [contentInfo.userInfo objectForKey:@"snooze_interval"]
+ };
+
+ if([playSound isEqualToNumber: [NSNumber numberWithInt: 1]]) {
+ BOOL notEmpty = [RnAlarmNotification checkStringIsNotEmpty:soundName];
+ if(notEmpty != YES){
+ content.sound = UNNotificationSound.defaultSound;
+ } else {
+ content.sound = [UNNotificationSound soundNamed:soundName];
+ }
+ }
+
+ // Create the request object.
+ UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:alarmId content:content trigger:trigger];
+
+ UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
+
+ [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+ if (error != nil) {
+ NSLog(@"error: %@", error.localizedDescription);
+ }
+ }];
+
+ NSDictionary *alarm = [NSDictionary dictionaryWithObjectsAndKeys: alarmId, @"id", nil];
+ NSLog(@"repeat alarm: %@", alarm);
+ } else {
+ // Fallback on earlier versions
+ }
+ } @catch(NSException *exception){
+ NSLog(@"error: %@", exception.reason);
+ }
+}
+
++ (void)snoozeAlarm:(UNNotification *)notification API_AVAILABLE(ios(10.0)) {
+ NSLog(@"do snooze");
+ [RnAlarmNotification stopSound];
+
+ @try {
+ if (@available(iOS 10.0, *)) {
+ UNNotificationContent *contentInfo = notification.request.content;
+ UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
+
+ content.title = contentInfo.title;
+ content.body = contentInfo.body;
+
+ NSNumber *has_button = [contentInfo.userInfo objectForKey:@"has_button"];
+ NSNumber *snooze_interval = [contentInfo.userInfo objectForKey:@"snooze_interval"];
+
+ // set buttons
+ if([has_button isEqualToNumber: [NSNumber numberWithInt: 1]]){
+ content.categoryIdentifier = @"CUSTOM_ACTIONS";
+ }
+
+ // set alarm date
+ int interval = [snooze_interval intValue];
+ NSTimeInterval snoozeInterval = interval * 60;
+
+ NSDate *now = [NSDate date];
+ NSDate *newDate = [now dateByAddingTimeInterval:snoozeInterval];
+ NSLog(@"new fire date after snooze: %@", newDate);
+
+ NSDateComponents *newFireDate = dateToComponents(newDate);
+
+ UNCalendarNotificationTrigger* trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:newFireDate repeats:NO];
+
+ NSString *alarmId = [NSString stringWithFormat: @"%ld", (long) NSDate.date.timeIntervalSince1970];
+
+ NSString *soundName = [contentInfo.userInfo objectForKey:@"sound_name"];
+ NSNumber *playSound = [contentInfo.userInfo objectForKey:@"sound"];
+
+ content.userInfo = @{
+ @"alarmId": alarmId,
+ @"sound": playSound,
+ @"vibrate": [contentInfo.userInfo objectForKey:@"vibrate"],
+ @"data": [contentInfo.userInfo objectForKey:@"data"],
+ @"fire_date": [contentInfo.userInfo objectForKey:@"fire_date"],
+ @"sound_name": soundName,
+ @"loop_sound": [contentInfo.userInfo objectForKey:@"loop_sound"],
+ @"volume": [contentInfo.userInfo objectForKey:@"volume"],
+ @"has_button": [contentInfo.userInfo objectForKey:@"has_button"],
+ @"schedule_type": [contentInfo.userInfo objectForKey:@"schedule_type"],
+ @"repeat_interval": [contentInfo.userInfo objectForKey:@"repeat_interval"],
+ @"interval_value": [contentInfo.userInfo objectForKey:@"interval_value"],
+ @"snooze_interval": [contentInfo.userInfo objectForKey:@"snooze_interval"]
+ };
+
+ if([playSound isEqualToNumber: [NSNumber numberWithInt: 1]]) {
+ BOOL notEmpty = [RnAlarmNotification checkStringIsNotEmpty:soundName];
+ if(notEmpty != YES){
+ NSLog(@"use default sound");
+ content.sound = UNNotificationSound.defaultSound;
+ } else {
+ content.sound = [UNNotificationSound soundNamed:soundName];
+ }
+ }
+
+ // Create the request object.
+ UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:alarmId content:content trigger:trigger];
+
+ UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
+
+ [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+ if (error != nil) {
+ NSLog(@"error: %@", error.localizedDescription);
+ }
+ }];
+
+ NSDictionary *alarm = [NSDictionary dictionaryWithObjectsAndKeys: alarmId, @"id", nil];
+ NSLog(@"snooze alarm: %@", alarm);
+ } else {
+ // Fallback on earlier versions
+ }
+ } @catch(NSException *exception){
+ NSLog(@"error: %@", exception.reason);
+ }
+}
+
++ (BOOL) checkStringIsNotEmpty:(NSString*)string {
+ if (string == (id)[NSNull null] || string.length == 0) return NO;
+ return YES;
+}
+
+RCT_EXPORT_METHOD(scheduleAlarm: (NSDictionary *)details resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
+ @try {
+ if (@available(iOS 10.0, *)) {
+ UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
+
+ content.title = [NSString localizedUserNotificationStringForKey:details[@"title"] arguments:nil];
+ content.body = [NSString localizedUserNotificationStringForKey:details[@"message"] arguments:nil];
+
+ // set buttons
+ if([details[@"has_button"] isEqualToNumber: [NSNumber numberWithInt: 1]]){
+ content.categoryIdentifier = @"CUSTOM_ACTIONS";
+ }
+
+ // set alarm date
+ NSDateComponents *fireDate = parseDate(details[@"fire_date"]);
+
+ UNCalendarNotificationTrigger* trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:fireDate repeats:NO];
+
+ // alarm id
+ NSString *alarmId = [NSString stringWithFormat: @"%ld", (long) NSDate.date.timeIntervalSince1970];
+
+ NSString *volume = [details[@"volume"] stringValue];
+
+ content.userInfo = @{
+ @"alarmId": alarmId,
+ @"sound": details[@"play_sound"],
+ @"vibrate": details[@"vibrate"],
+ @"data": details[@"data"],
+ @"fire_date": details[@"fire_date"],
+ @"sound_name": details[@"sound_name"],
+ @"loop_sound": details[@"loop_sound"],
+ @"volume": volume,
+ @"has_button": details[@"has_button"],
+ @"schedule_type": details[@"schedule_type"],
+ @"repeat_interval": details[@"repeat_interval"],
+ @"interval_value": details[@"interval_value"],
+ @"snooze_interval": details[@"snooze_interval"]
+ };
+
+ if([details[@"play_sound"] isEqualToNumber: [NSNumber numberWithInt: 1]]) {
+ BOOL notEmpty = [RnAlarmNotification checkStringIsNotEmpty:details[@"sound_name"]];
+ if(notEmpty != YES){
+ content.sound = UNNotificationSound.defaultSound;
+ } else {
+ content.sound = [UNNotificationSound soundNamed:details[@"sound_name"]];
+ }
+ }
+
+ // Create the request object.
+ UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:alarmId content:content trigger:trigger];
+
+ UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
+
+ [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+ if (error != nil) {
+ NSLog(@"error: %@", error.localizedDescription);
+ reject(@"error", nil, error);
+ }
+ }];
+
+ NSDictionary *alarm = [NSDictionary dictionaryWithObjectsAndKeys: alarmId, @"id", nil];
+
+ resolve(alarm);
+ } else {
+ // Fallback on earlier versions
+ }
+ } @catch(NSException *exception){
+ NSLog(@"%@", exception.reason);
+ NSMutableDictionary * info = [NSMutableDictionary dictionary];
+ [info setValue:exception.name forKey:@"ExceptionName"];
+ [info setValue:exception.reason forKey:@"ExceptionReason"];
+ [info setValue:exception.callStackSymbols forKey:@"ExceptionCallStackSymbols"];
+ [info setValue:exception.userInfo forKey:@"ExceptionUserInfo"];
+
+ NSError *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:info];
+ reject(@"error", nil, error);
+ }
+}
+
+RCT_EXPORT_METHOD(sendNotification: (NSDictionary *)details) {
+ @try {
+ NSLog(@"send notification now");
+ if (@available(iOS 10.0, *)) {
+ UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
+ content.title = [NSString localizedUserNotificationStringForKey:details[@"title"] arguments:nil];
+ content.body = [NSString localizedUserNotificationStringForKey:details[@"message"] arguments:nil];
+
+ // set buttons
+ if([details[@"has_button"] isEqualToNumber: [NSNumber numberWithInt: 1]]){
+ content.categoryIdentifier = @"CUSTOM_ACTIONS";
+ }
+
+ // alarm id
+ NSString *alarmId = [NSString stringWithFormat: @"%ld", (long) NSDate.date.timeIntervalSince1970];
+
+ NSString *volume = [details[@"volume"] stringValue];
+
+ content.userInfo = @{
+ @"alarmId": alarmId,
+ @"sound": details[@"play_sound"],
+ @"vibrate": details[@"vibrate"],
+ @"data": details[@"data"],
+ @"sound_name": details[@"sound_name"],
+ @"loop_sound": details[@"loop_sound"],
+ @"volume": volume,
+ @"schedule_type": @"once"
+ };
+
+ if([details[@"play_sound"] isEqualToNumber: [NSNumber numberWithInt: 1]]) {
+ BOOL notEmpty = [RnAlarmNotification checkStringIsNotEmpty:details[@"sound_name"]];
+ if(notEmpty != YES){
+ content.sound = UNNotificationSound.defaultSound;
+ } else {
+ content.sound = [UNNotificationSound soundNamed:details[@"sound_name"]];
+ }
+ }
+
+ // Create the request object.
+ UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:alarmId content:content trigger:nil];
+
+ UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
+
+ [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { }];
+ } else {
+ // Fallback on earlier versions
+ }
+ } @catch(NSException *exception){
+ NSLog(@"error: %@", exception.reason);
+ }
+}
+
+RCT_EXPORT_METHOD(deleteAlarm: (NSInteger *)id){
+ NSLog(@"delete alarm: %li", (long) id);
+ if (@available(iOS 10.0, *)) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ NSArray *array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%li", (long)id], nil];
+ [center removePendingNotificationRequestsWithIdentifiers:array];
+ } else {
+ // Fallback on earlier versions
+ }
+}
+
+RCT_EXPORT_METHOD(deleteRepeatingAlarm: (NSInteger *)id){
+ NSLog(@"delete alarm: %li", (long) id);
+ if (@available(iOS 10.0, *)) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ NSArray *array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%li", (long)id], nil];
+ [center removePendingNotificationRequestsWithIdentifiers:array];
+ } else {
+ // Fallback on earlier versions
+ }
+}
+
+RCT_EXPORT_METHOD(stopAlarmSound){
+ NSLog(@"stop alarm sound");
+ [RnAlarmNotification stopSound];
+}
+
+RCT_EXPORT_METHOD(removeFiredNotification: (NSInteger)id){
+ NSLog(@"remove fired notification: %li", (long) id);
+ if (@available(iOS 10.0, *)) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ NSArray *array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%li", (long)id], nil];
+ [center removeDeliveredNotificationsWithIdentifiers:array];
+ } else {
+ // Fallback on earlier versions
+ }
+}
+
+RCT_EXPORT_METHOD(removeAllFiredNotifications){
+ NSLog(@"remove all notifications");
+ if (@available(iOS 10.0, *)) {
+ if ([UNUserNotificationCenter class]) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ [center removeAllDeliveredNotifications];
+ }
+ } else {
+ // Fallback on earlier versions
+ }
+}
+
+API_AVAILABLE(ios(10.0))
+static NSDictionary *RCTFormatUNNotificationRequest(UNNotificationRequest *request)
+{
+ NSMutableDictionary *formattedNotification = [NSMutableDictionary dictionary];
+ UNNotificationContent *content = request.content;
+
+ NSDateComponents *fireDate = parseDate(content.userInfo[@"fire_date"]);
+
+ formattedNotification[@"id"] = request.identifier;
+ formattedNotification[@"day"] = [NSString stringWithFormat:@"%li", (long)fireDate.day];
+ formattedNotification[@"month"] = [NSString stringWithFormat:@"%li", (long)fireDate.month];
+ formattedNotification[@"year"] = [NSString stringWithFormat:@"%li", (long)fireDate.year];
+ formattedNotification[@"hour"] = [NSString stringWithFormat:@"%li", (long)fireDate.hour];
+ formattedNotification[@"minute"] =[NSString stringWithFormat:@"%li", (long)fireDate.minute];
+ formattedNotification[@"second"] = [NSString stringWithFormat:@"%li", (long)fireDate.second];
+
+ return formattedNotification;
+}
+
+RCT_EXPORT_METHOD(getScheduledAlarms: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
+ NSLog(@"get all notifications");
+ if (@available(iOS 10.0, *)) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+
+ [center getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) {
+ NSLog(@"count%lu",(unsigned long)requests.count);
+
+ NSMutableArray *formattedNotifications = [NSMutableArray new];
+
+ for (UNNotificationRequest *request in requests) {
+ [formattedNotifications addObject:RCTFormatUNNotificationRequest(request)];
+ }
+ resolve(formattedNotifications);
+ }];
+ } else {
+ resolve(nil);
+ }
+}
+
+RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions
+ resolver:(RCTPromiseResolveBlock)resolve
+ rejecter:(RCTPromiseRejectBlock)reject) {
+ if (RCTRunningInAppExtension()) {
+ reject(@"E_UNABLE_TO_REQUEST_PERMISSIONS", nil, RCTErrorWithMessage(@"Requesting push notifications is currently unavailable in an app extension"));
+ return;
+ }
+
+ UIUserNotificationType types = UIUserNotificationTypeNone;
+ if (permissions) {
+ if ([RCTConvert BOOL:permissions[@"alert"]]) {
+ types |= UIUserNotificationTypeAlert;
+ }
+ if ([RCTConvert BOOL:permissions[@"badge"]]) {
+ types |= UIUserNotificationTypeBadge;
+ }
+ if ([RCTConvert BOOL:permissions[@"sound"]]) {
+ types |= UIUserNotificationTypeSound;
+ }
+ } else {
+ types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound;
+ }
+
+ if (@available(iOS 10.0, *)) {
+ UNNotificationCategory* generalCategory = [UNNotificationCategory
+ categoryWithIdentifier:@"GENERAL"
+ actions:@[]
+ intentIdentifiers:@[]
+ options:UNNotificationCategoryOptionCustomDismissAction];
+
+ UNNotificationAction* snoozeAction = [UNNotificationAction
+ actionWithIdentifier:@"SNOOZE_ACTION"
+ title:@"SNOOZE"
+ options:UNNotificationActionOptionNone];
+
+ UNNotificationAction* stopAction = [UNNotificationAction
+ actionWithIdentifier:@"DISMISS_ACTION"
+ title:@"DISMISS"
+ options:UNNotificationActionOptionForeground];
+
+ UNNotificationCategory* customCategory = [UNNotificationCategory
+ categoryWithIdentifier:@"CUSTOM_ACTIONS"
+ actions:@[snoozeAction, stopAction]
+ intentIdentifiers:@[]
+ options:UNNotificationCategoryOptionNone];
+
+ UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
+
+ [center setNotificationCategories:[NSSet setWithObjects:generalCategory, customCategory, nil]];
+
+ [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UIUserNotificationTypeBadge + UNAuthorizationOptionSound) completionHandler:^(BOOL granted, NSError *_Nullable error) {
+
+ if (error != NULL) {
+ reject(@"-1", @"Error - Push authorization request failed.", error);
+ } else {
+ dispatch_async(dispatch_get_main_queue(), ^(void){
+ [RCTSharedApplication() registerForRemoteNotifications];
+ });
+ [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
+ resolve(RCTPromiseResolveValueForUNNotificationSettings(settings));
+ }];
+ }
+ }];
+ } else {
+ // Fallback on earlier versions
+ resolve(nil);
+ }
+}
+
+RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) {
+ if (RCTRunningInAppExtension()) {
+ callback(@[RCTSettingsDictForUNNotificationSettings(NO, NO, NO, NO, NO)]);
+ return;
+ }
+
+ if (@available(iOS 10.0, *)) {
+ [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
+ callback(@[RCTPromiseResolveValueForUNNotificationSettings(settings)]);
+ }];
+ } else {
+ // Fallback on earlier versions
+ }
+}
+
+API_AVAILABLE(ios(10.0))
+static inline NSDictionary *RCTPromiseResolveValueForUNNotificationSettings(UNNotificationSettings* _Nonnull settings) {
+ return RCTSettingsDictForUNNotificationSettings(settings.alertSetting == UNNotificationSettingEnabled, settings.badgeSetting == UNNotificationSettingEnabled, settings.soundSetting == UNNotificationSettingEnabled, settings.lockScreenSetting == UNNotificationSettingEnabled, settings.notificationCenterSetting == UNNotificationSettingEnabled);
+}
+
+static inline NSDictionary *RCTSettingsDictForUNNotificationSettings(BOOL alert, BOOL badge, BOOL sound, BOOL lockScreen, BOOL notificationCenter) {
+ return @{@"alert": @(alert), @"badge": @(badge), @"sound": @(sound), @"lockScreen": @(lockScreen), @"notificationCenter": @(notificationCenter)};
+}
+
+@end
diff --git a/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcodeproj/project.pbxproj b/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..0df0a3a5d
--- /dev/null
+++ b/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcodeproj/project.pbxproj
@@ -0,0 +1,290 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 8D0B4EFD24DC22BA00A18E45 /* RnAlarmNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D0B4EFB24DC22B900A18E45 /* RnAlarmNotification.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 58B511D91A9E6C8500147676 /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "include/$(PRODUCT_NAME)";
+ dstSubfolderSpec = 16;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 134814201AA4EA6300B7C361 /* libRnAlarmNotification.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRnAlarmNotification.a; sourceTree = BUILT_PRODUCTS_DIR; };
+ 8D0B4EFB24DC22B900A18E45 /* RnAlarmNotification.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RnAlarmNotification.m; sourceTree = ""; };
+ 8D0B4EFC24DC22B900A18E45 /* RnAlarmNotification.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RnAlarmNotification.h; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 58B511D81A9E6C8500147676 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 134814211AA4EA7D00B7C361 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 134814201AA4EA6300B7C361 /* libRnAlarmNotification.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 58B511D21A9E6C8500147676 = {
+ isa = PBXGroup;
+ children = (
+ 134814211AA4EA7D00B7C361 /* Products */,
+ 8D0B4EFC24DC22B900A18E45 /* RnAlarmNotification.h */,
+ 8D0B4EFB24DC22B900A18E45 /* RnAlarmNotification.m */,
+ );
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 58B511DA1A9E6C8500147676 /* RnAlarmNotification */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RnAlarmNotification" */;
+ buildPhases = (
+ 58B511D71A9E6C8500147676 /* Sources */,
+ 58B511D81A9E6C8500147676 /* Frameworks */,
+ 58B511D91A9E6C8500147676 /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = RnAlarmNotification;
+ productName = RCTDataManager;
+ productReference = 134814201AA4EA6300B7C361 /* libRnAlarmNotification.a */;
+ productType = "com.apple.product-type.library.static";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 58B511D31A9E6C8500147676 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0920;
+ ORGANIZATIONNAME = Facebook;
+ TargetAttributes = {
+ 58B511DA1A9E6C8500147676 = {
+ CreatedOnToolsVersion = 6.1.1;
+ };
+ };
+ };
+ buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RnAlarmNotification" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 58B511D21A9E6C8500147676;
+ productRefGroup = 58B511D21A9E6C8500147676;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 58B511DA1A9E6C8500147676 /* RnAlarmNotification */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 58B511D71A9E6C8500147676 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D0B4EFD24DC22BA00A18E45 /* RnAlarmNotification.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 58B511ED1A9E6C8500147676 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
+ LIBRARY_SEARCH_PATHS = (
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
+ "\"$(inherited)\"",
+ );
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ 58B511EE1A9E6C8500147676 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
+ LIBRARY_SEARCH_PATHS = (
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
+ "\"$(inherited)\"",
+ );
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 58B511F01A9E6C8500147676 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../../../React/**",
+ "$(SRCROOT)/../../react-native/React/**",
+ );
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = RnAlarmNotification;
+ SKIP_INSTALL = YES;
+ };
+ name = Debug;
+ };
+ 58B511F11A9E6C8500147676 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../../../React/**",
+ "$(SRCROOT)/../../react-native/React/**",
+ );
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = RnAlarmNotification;
+ SKIP_INSTALL = YES;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RnAlarmNotification" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 58B511ED1A9E6C8500147676 /* Debug */,
+ 58B511EE1A9E6C8500147676 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RnAlarmNotification" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 58B511F01A9E6C8500147676 /* Debug */,
+ 58B511F11A9E6C8500147676 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 58B511D31A9E6C8500147676 /* Project object */;
+}
diff --git a/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcworkspace/contents.xcworkspacedata b/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..762793c6b
--- /dev/null
+++ b/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/packages/react-native-alarm-notification/ios/RnAlarmNotification.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/packages/react-native-alarm-notification/package.json b/packages/react-native-alarm-notification/package.json
new file mode 100644
index 000000000..487ccebd4
--- /dev/null
+++ b/packages/react-native-alarm-notification/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@joplin/react-native-alarm-notification",
+ "title": "React Native Alarm Notification for Joplin. Forked from https://github.com/emekalites/react-native-alarm-notification",
+ "version": "2.10.0",
+ "description": "schedule alarm with notification in react-native",
+ "main": "index.js",
+ "private": true,
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/roman-r-m/react-native-alarm-notification.git",
+ "baseUrl": "https://github.com/roman-r-m/react-native-alarm-notification"
+ },
+ "license": "MIT",
+ "licenseFilename": "LICENSE",
+ "readmeFilename": "README.md",
+ "devDependencies": {
+ "react": "18.2.0",
+ "react-native": "0.70.6"
+ }
+}
diff --git a/packages/react-native-alarm-notification/react-native-alarm-notification.podspec b/packages/react-native-alarm-notification/react-native-alarm-notification.podspec
new file mode 100644
index 000000000..2001fa8e9
--- /dev/null
+++ b/packages/react-native-alarm-notification/react-native-alarm-notification.podspec
@@ -0,0 +1,25 @@
+require "json"
+
+package = JSON.parse(File.read(File.join(__dir__, "package.json")))
+
+Pod::Spec.new do |s|
+ s.name = "react-native-alarm-notification"
+ s.version = package["version"]
+ s.summary = package["description"]
+ s.description = <<-DESC
+ react-native-alarm-notification
+ DESC
+ s.homepage = "https://github.com/laurent22/joplin/tree/dev/packages/react-native-alarm-notification"
+ s.license = { :type => "MIT", :file => "LICENSE" }
+ s.authors = { "Chukwuemeka Ihedoro" => "caihedoro@gmail.com" }
+ s.platforms = { :ios => "9.0" }
+ s.source = { :git => "https://github.com/laurent22/joplin.git" }
+
+ s.source_files = "ios/**/*.{h,c,m,swift}"
+ s.requires_arc = true
+
+ s.dependency "React"
+ # ...
+ # s.dependency "..."
+end
+
diff --git a/packages/tools/setupNewRelease.ts b/packages/tools/setupNewRelease.ts
index bb912b08f..abc57628a 100644
--- a/packages/tools/setupNewRelease.ts
+++ b/packages/tools/setupNewRelease.ts
@@ -131,13 +131,14 @@ async function main() {
await updatePackageVersion(`${rootDir}/packages/app-mobile/package.json`, majorMinorVersion, options);
await updatePackageVersion(`${rootDir}/packages/generator-joplin/package.json`, majorMinorVersion, options);
await updatePackageVersion(`${rootDir}/packages/htmlpack/package.json`, majorMinorVersion, options);
- await updatePackageVersion(`${rootDir}/packages/react-native-saf-x/package.json`, majorMinorVersion, options);
await updatePackageVersion(`${rootDir}/packages/lib/package.json`, majorMinorVersion, options);
+ await updatePackageVersion(`${rootDir}/packages/pdf-viewer/package.json`, majorMinorVersion, options);
await updatePackageVersion(`${rootDir}/packages/plugin-repo-cli/package.json`, majorMinorVersion, options);
+ await updatePackageVersion(`${rootDir}/packages/react-native-alarm-notification/package.json`, majorMinorVersion, options);
+ await updatePackageVersion(`${rootDir}/packages/react-native-saf-x/package.json`, majorMinorVersion, options);
await updatePackageVersion(`${rootDir}/packages/renderer/package.json`, majorMinorVersion, options);
await updatePackageVersion(`${rootDir}/packages/server/package.json`, majorMinorVersion, options);
await updatePackageVersion(`${rootDir}/packages/tools/package.json`, majorMinorVersion, options);
- await updatePackageVersion(`${rootDir}/packages/pdf-viewer/package.json`, majorMinorVersion, options);
if (options.updateVersion) {
await updateGradleVersion(`${rootDir}/packages/app-mobile/android/app/build.gradle`, majorMinorVersion);
diff --git a/yarn.lock b/yarn.lock
index be593e59b..77d65f24b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4774,6 +4774,7 @@ __metadata:
"@codemirror/state": 6.1.4
"@codemirror/view": 6.7.1
"@joplin/lib": ~2.10
+ "@joplin/react-native-alarm-notification": ~2.10
"@joplin/react-native-saf-x": ~2.10
"@joplin/renderer": ~2.10
"@joplin/tools": ~2.10
@@ -4800,7 +4801,6 @@ __metadata:
jest: 29.3.1
jest-environment-jsdom: 29.3.1
jetifier: 2.0.0
- joplin-rn-alarm-notification: 1.0.7
jsc-android: 241213.1.0
jsdom: 20.0.0
lodash: 4.17.21
@@ -5051,6 +5051,15 @@ __metadata:
languageName: unknown
linkType: soft
+"@joplin/react-native-alarm-notification@workspace:packages/react-native-alarm-notification, @joplin/react-native-alarm-notification@~2.10":
+ version: 0.0.0-use.local
+ resolution: "@joplin/react-native-alarm-notification@workspace:packages/react-native-alarm-notification"
+ dependencies:
+ react: 18.2.0
+ react-native: 0.70.6
+ languageName: unknown
+ linkType: soft
+
"@joplin/react-native-saf-x@workspace:packages/react-native-saf-x, @joplin/react-native-saf-x@~2.10":
version: 0.0.0-use.local
resolution: "@joplin/react-native-saf-x@workspace:packages/react-native-saf-x"
@@ -20466,13 +20475,6 @@ __metadata:
languageName: node
linkType: hard
-"joplin-rn-alarm-notification@npm:1.0.7":
- version: 1.0.7
- resolution: "joplin-rn-alarm-notification@npm:1.0.7"
- checksum: 68e9cb15fc9dcd4d6b7a523967adcad8bc120da260d80c9cb541128b3e499f529dd7b879f5e35edf81d8e992194ab20d61281a835e0a15cde114a2cc208f8128
- languageName: node
- linkType: hard
-
"joplin@workspace:packages/app-cli":
version: 0.0.0-use.local
resolution: "joplin@workspace:packages/app-cli"