1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Android: Fixes non-working alarms

Also imported react-native-alarm-notificatio into the project
This commit is contained in:
Laurent Cozic 2023-01-20 17:33:19 +00:00
parent c9831833c4
commit 138bc8144b
34 changed files with 3245 additions and 43 deletions

View File

@ -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"
]
}
]

View File

@ -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,

View File

@ -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

View File

@ -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,
};

View File

@ -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",

View File

@ -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;
}

View File

@ -0,0 +1 @@
*.pbxproj -text

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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
}
}

View File

@ -0,0 +1,38 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.emekalites.react.alarm.notification">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application>
<receiver
android:name="com.emekalites.react.alarm.notification.AlarmReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="ACTION_DISMISS" />
<action android:name="ACTION_SNOOZE" />
</intent-filter>
</receiver>
<receiver
android:name="com.emekalites.react.alarm.notification.AlarmDismissReceiver"
android:enabled="true"
android:exported="true" />
<receiver
android:name="com.emekalites.react.alarm.notification.AlarmBootReceiver"
android:directBootAware="true"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -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<AlarmModel> 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);
}
}

View File

@ -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<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
return Collections.<NativeModule>singletonList(new ANModule(reactContext));
}
// Deprecated RN 0.47
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@ -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<AlarmModel> 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);
}
}
}
}

View File

@ -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<AlarmModel> getAlarmList(int isActive) {
String selectQuery = "SELECT * FROM " + TABLE_NAME;
if (isActive == 1) {
selectQuery += " WHERE " + COL_ACTIVE + " = " + isActive;
}
SQLiteDatabase db = this.getWritableDatabase();
ArrayList<AlarmModel> 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<AlarmModel> getAlarmList() {
return getAlarmList(0);
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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 <T> TypeAdapter<T> create(final Gson gson, TypeToken<T> typeToken) {
if (!Bundle.class.isAssignableFrom(typeToken.getRawType())) {
return null;
}
return (TypeAdapter<T>) new TypeAdapter<Bundle>() {
@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);
}
}

View File

@ -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<AlarmModel> 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;
}
}
}
}
}

View File

@ -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<AlarmModel> 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<AlarmModel> 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<AlarmModel> getAlarms() {
return alarmDB.getAlarmList(1);
}
WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException {
WritableMap map = new WritableNativeMap();
Iterator<String> 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;
}
}

View File

@ -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";
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF707070"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.93 6,11v5l-2,2v1h16v-1l-2,-2zM14.5,9.8l-2.8,3.4h2.8L14.5,15h-5v-1.8l2.8,-3.4L9.5,9.8L9.5,8h5v1.8z"/>
</vector>

View File

@ -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));
}
}

View File

@ -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;

View File

@ -0,0 +1,9 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <UserNotifications/UserNotifications.h>
@interface RnAlarmNotification : RCTEventEmitter <RCTBridgeModule>
+ (void)didReceiveNotificationResponse:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0));
+ (void)didReceiveNotification:(UNNotification *)notification API_AVAILABLE(ios(10.0));
@end

View File

@ -0,0 +1,794 @@
#import "RnAlarmNotification.h"
#import <UserNotifications/UserNotifications.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTUtils.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVFoundation.h>
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<NSString *> *)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<AVAudioPlayerDelegate>)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<UNNotificationRequest *> * _Nonnull requests) {
NSLog(@"count%lu",(unsigned long)requests.count);
NSMutableArray<NSDictionary *> *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

View File

@ -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 = "<group>"; };
8D0B4EFC24DC22B900A18E45 /* RnAlarmNotification.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RnAlarmNotification.h; sourceTree = "<group>"; };
/* 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 = "<group>";
};
58B511D21A9E6C8500147676 = {
isa = PBXGroup;
children = (
134814211AA4EA7D00B7C361 /* Products */,
8D0B4EFC24DC22B900A18E45 /* RnAlarmNotification.h */,
8D0B4EFB24DC22B900A18E45 /* RnAlarmNotification.m */,
);
sourceTree = "<group>";
};
/* 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:RnAlarmNotification.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -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"
}
}

View File

@ -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

View File

@ -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);

View File

@ -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"