From 138bc8144b44240fdf32906270161ad07198dee8 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Fri, 20 Jan 2023 17:33:19 +0000 Subject: [PATCH] Android: Fixes non-working alarms Also imported react-native-alarm-notificatio into the project --- .npmpackagejsonlintrc.json | 3 +- joplin.code-workspace | 3 + packages/app-mobile/ios/Podfile.lock | 8 +- packages/app-mobile/metro.config.js | 51 +- packages/app-mobile/package.json | 2 +- .../services/AlarmServiceDriver.android.ts | 4 +- .../.gitattributes | 1 + .../.gitignore | 44 + .../react-native-alarm-notification/LICENSE | 21 + .../react-native-alarm-notification/README.md | 5 + .../android/build.gradle | 145 ++++ .../android/src/main/AndroidManifest.xml | 38 + .../react/alarm/notification/ANModule.java | 198 +++++ .../react/alarm/notification/ANPackage.java | 31 + .../alarm/notification/AlarmBootReceiver.java | 34 + .../alarm/notification/AlarmDatabase.java | 153 ++++ .../notification/AlarmDismissReceiver.java | 28 + .../react/alarm/notification/AlarmModel.java | 444 ++++++++++ .../alarm/notification/AlarmModelCodec.java | 114 +++ .../alarm/notification/AlarmReceiver.java | 79 ++ .../react/alarm/notification/AlarmUtil.java | 476 +++++++++++ .../react/alarm/notification/Constants.java | 14 + .../src/main/res/drawable/ic_snooze.xml | 9 + .../alarm/notification/AlarmModelTest.java | 33 + .../react-native-alarm-notification/index.js | 174 ++++ .../ios/RnAlarmNotification.h | 9 + .../ios/RnAlarmNotification.m | 794 ++++++++++++++++++ .../project.pbxproj | 290 +++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../package.json | 20 + .../react-native-alarm-notification.podspec | 25 + packages/tools/setupNewRelease.ts | 5 +- yarn.lock | 18 +- 34 files changed, 3245 insertions(+), 43 deletions(-) create mode 100644 packages/react-native-alarm-notification/.gitattributes create mode 100644 packages/react-native-alarm-notification/.gitignore create mode 100644 packages/react-native-alarm-notification/LICENSE create mode 100644 packages/react-native-alarm-notification/README.md create mode 100644 packages/react-native-alarm-notification/android/build.gradle create mode 100644 packages/react-native-alarm-notification/android/src/main/AndroidManifest.xml create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/ANModule.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/ANPackage.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmBootReceiver.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmDatabase.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmDismissReceiver.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModel.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModelCodec.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmReceiver.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/AlarmUtil.java create mode 100644 packages/react-native-alarm-notification/android/src/main/java/com/emekalites/react/alarm/notification/Constants.java create mode 100644 packages/react-native-alarm-notification/android/src/main/res/drawable/ic_snooze.xml create mode 100644 packages/react-native-alarm-notification/android/src/test/java/com/emekalites/react/alarm/notification/AlarmModelTest.java create mode 100644 packages/react-native-alarm-notification/index.js create mode 100644 packages/react-native-alarm-notification/ios/RnAlarmNotification.h create mode 100644 packages/react-native-alarm-notification/ios/RnAlarmNotification.m create mode 100644 packages/react-native-alarm-notification/ios/RnAlarmNotification.xcodeproj/project.pbxproj create mode 100644 packages/react-native-alarm-notification/ios/RnAlarmNotification.xcworkspace/contents.xcworkspacedata create mode 100644 packages/react-native-alarm-notification/ios/RnAlarmNotification.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/react-native-alarm-notification/package.json create mode 100644 packages/react-native-alarm-notification/react-native-alarm-notification.podspec 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"