diff --git a/.github/stale.yml b/.github/stale.yml index 30fcc1a..922de73 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Number of days of inactivity before an issue becomes stale -daysUntilStale: 80 +daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 +daysUntilClose: 14 # Issues with these labels will never be considered stale exemptLabels: - pinned diff --git a/.gitignore b/.gitignore index c5464d9..b3c2121 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ project.xcworkspace build/ .idea .gradle +gradle/ +gradle* local.properties *.iml diff --git a/README.md b/README.md index 396ade6..3a09d71 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,9 @@ In your `AndroidManifest.xml` | **`loop_sound`** | Play sound continuously until you decide to stop it. `[boolean]` | `false` | | **`message`** | **Required:** Add Notification message. | `"My Notification Message"` | | **`play_sound`** | Play alarm notification sound. `[boolean]` | `true` | -| **`repeat_interval`** | Interval set to repeat alarm if schedule_type is "repeat". `[number]` in minutes | `1` | | **`schedule_type`** | **Required:** Type of alarm schedule. `"once"` (to show alarm once) or `"repeat"` (to display alarm after set repeat_interval) | `"once"` | +| **`repeat_interval`** | Interval set to repeat alarm if schedule_type is "repeat". `[minutely, hourly, daily, weekly]` | `"hourly"` | +| **`interval_value`** | Set interval_value if repeat_interval is minutely and hourly. `[number]` | `1` | | **`small_icon`** | **Required:** Set the small icon resource, which will be used to represent the notification in the status bar. eg `"ic_launcher"`. PS: make sure to add the file in your mipmap folders `[project_root]/android/app/src/main/res/mipmap*` | `"ic_launcher"` | | **`snooze_interval`** | Interval set to show alarm after snooze button is tapped. `[number]` in minutes | `1` | | **`sound_name`** | Set audio file to play when alarm is triggered. example `"sound_name.mp3"` or `"sound_name"`. PS: make sure to add the file in your res/raw folder `[project_root]/android/app/src/main/res/raw` | _None_ | @@ -128,6 +129,8 @@ In your `AndroidManifest.xml` | **`vibrate`** | Set vibration when alarm is triggered. `[boolean]` | `true` | | **`vibration`** | Set number of milliseconds to vibrate. `[number]` | `100` | | **`use_big_text (Android only)`** | Set notification message style as big text. `[boolean]` | `false` | +| **`volume`** | Set volume. `[number between 0.0 and 1.0]` | `0.5` | +| **`bypass_dnd (Android only)`** | Sets whether or not notifications posted to this channel can interrupt the user | `false` | ## Usage @@ -162,7 +165,7 @@ class App extends Component { ReactNativeAN.deleteAlarm(alarm.id); //Delete Repeating Alarm - ReactNativeAN.deleteRepeatingAlarm(); + ReactNativeAN.deleteRepeatingAlarm(alarm.id); //Stop Alarm ReactNativeAN.stopAlarmSound(); diff --git a/android/build.gradle b/android/build.gradle index 7dbd52d..76a290f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -74,6 +74,7 @@ dependencies { //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules implementation 'com.google.code.gson:gson:2.8.6' + implementation 'androidx.appcompat:appcompat:1.1.0' } def configureReactNativePom(def pom) { diff --git a/android/src/main/java/com/emekalites/react/alarm/notification/ANModule.java b/android/src/main/java/com/emekalites/react/alarm/notification/ANModule.java index b9be284..f2fbeea 100644 --- a/android/src/main/java/com/emekalites/react/alarm/notification/ANModule.java +++ b/android/src/main/java/com/emekalites/react/alarm/notification/ANModule.java @@ -2,7 +2,6 @@ import android.app.Application; import android.os.Bundle; -import android.util.Log; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -68,7 +67,7 @@ public void scheduleAlarm(ReadableMap details, Promise promise) throws ParseExce Bundle data = bundle.getBundle("data"); alarm.setData(bundle2string(data)); - alarm.setInterval((int)bundle.getDouble("repeat_interval", 1.0)); + 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")); @@ -85,6 +84,9 @@ public void scheduleAlarm(ReadableMap details, Promise promise) throws ParseExce 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); @@ -149,12 +151,10 @@ public void sendNotification(ReadableMap details) throws ParseException { Bundle data = bundle.getBundle("data"); alarm.setData(bundle2string(data)); - alarm.setInterval((int)bundle.getDouble("repeat_interval", 1)); 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)); alarm.setSoundName(bundle.getString("sound_name")); @@ -166,8 +166,9 @@ public void sendNotification(ReadableMap details) throws ParseException { alarm.setHasButton(bundle.getBoolean("has_button", false)); alarm.setVibration((int)bundle.getDouble("vibration", 100)); alarm.setUseBigText(bundle.getBoolean("use_big_text", false)); + alarm.setVolume(bundle.getDouble("volume", 0.5)); + alarm.setBypassDnd(bundle.getBoolean("bypass_dnd", false)); - SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.ENGLISH); Calendar calendar = new GregorianCalendar(); alarmUtil.setAlarmFromCalendar(alarm, calendar); diff --git a/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModel.java b/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModel.java index 42e66a0..519a4cc 100644 --- a/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModel.java +++ b/android/src/main/java/com/emekalites/react/alarm/notification/AlarmModel.java @@ -28,13 +28,16 @@ public class AlarmModel implements Serializable { private String soundNames; // separate sounds with comma eg (sound1.mp3,sound2.mp3) private String color = "red"; private String scheduleType = "once"; // once or repeat - private int interval = 1; // in minutes + private String interval = "hourly"; // hourly, daily, weekly + private int intervalValue = 1; private int snoozeInterval = 1; // in minutes private String tag; private String data; private boolean loopSound = false; private boolean useBigText = false; private boolean hasButton = false; + private double volume = 0.5; + private boolean bypassDnd = false; private int active = 1; // 1 = yes, 0 = no @@ -214,14 +217,22 @@ public void setScheduleType(String scheduleType) { this.scheduleType = scheduleType; } - public int getInterval() { + public String getInterval() { return interval; } - public void setInterval(int 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; } @@ -278,6 +289,26 @@ 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; + } + @Override public String toString() { return "AlarmModel{" + @@ -304,12 +335,15 @@ public String toString() { ", 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 + '}'; } diff --git a/android/src/main/java/com/emekalites/react/alarm/notification/AlarmUtil.java b/android/src/main/java/com/emekalites/react/alarm/notification/AlarmUtil.java index 8c48819..da551d8 100644 --- a/android/src/main/java/com/emekalites/react/alarm/notification/AlarmUtil.java +++ b/android/src/main/java/com/emekalites/react/alarm/notification/AlarmUtil.java @@ -18,6 +18,7 @@ import android.media.MediaPlayer; import android.os.Build; import android.os.Bundle; +import android.os.VibrationEffect; import android.os.Vibrator; import android.util.Log; import android.widget.Toast; @@ -44,7 +45,7 @@ class AlarmUtil { private Context mContext; private AudioInterface audioInterface; - private static final long DEFAULT_VIBRATION = 100; + static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250}; AlarmUtil(Application context) { mContext = context; @@ -78,9 +79,12 @@ private NotificationManager getNotificationManager() { return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); } - private void playAlarmSound(String name, String names, boolean shouldLoop) { + private void playAlarmSound(String name, String names, boolean shouldLoop, double volume) { + float number = (float) volume; + MediaPlayer mediaPlayer = audioInterface.getSingletonMedia(name, names); mediaPlayer.setLooping(shouldLoop); + mediaPlayer.setVolume(number, number); mediaPlayer.start(); mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @@ -148,7 +152,9 @@ void setAlarm(AlarmModel alarm) { alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent); } } else if (scheduleType.equals("repeat")) { - alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarm.getInterval() * 1000, alarmIntent); + long interval = this.getInterval(alarm.getInterval(), alarm.getIntervalValue()); + + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), interval, alarmIntent); } else { Log.d(TAG, "Schedule type should either be once or repeat"); return; @@ -193,12 +199,35 @@ void snoozeAlarm(AlarmModel alarm) { alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent); } } else if (scheduleType.equals("repeat")) { - alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarm.getInterval() * 1000, alarmIntent); + long interval = this.getInterval(alarm.getInterval(), alarm.getIntervalValue()); + + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), interval, alarmIntent); } else { Log.d(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 = getAlarmDB().getAlarm(id); @@ -220,7 +249,7 @@ void deleteAlarm(int id) { void deleteRepeatingAlarm(int id) { try { AlarmModel alarm = getAlarmDB().getAlarm(id); - + String scheduleType = alarm.getScheduleType(); if (scheduleType.equals("repeat")) { this.stopAlarm(alarm); @@ -301,6 +330,7 @@ private PendingIntent createOnDismissedIntent(Context context, int notificationI void sendNotification(AlarmModel alarm) { try { Class intentClass = getMainActivityClass(); + if (intentClass == null) { Log.e(TAG, "No activity class found for the notification"); return; @@ -308,7 +338,7 @@ void sendNotification(AlarmModel alarm) { boolean playSound = alarm.isPlaySound(); if (playSound) { - this.playAlarmSound(alarm.getSoundName(), alarm.getSoundNames(), alarm.isLoopSound()); + this.playAlarmSound(alarm.getSoundName(), alarm.getSoundNames(), alarm.isLoopSound(), alarm.getVolume()); } NotificationManager mNotificationManager = getNotificationManager(); @@ -368,27 +398,53 @@ void sendNotification(AlarmModel alarm) { .setContentTitle(title) .setContentText(message) .setTicker(alarm.getTicker()) - .setDefaults(NotificationCompat.DEFAULT_ALL) .setPriority(NotificationCompat.PRIORITY_MAX) .setAutoCancel(alarm.isAutoCancel()) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_ALARM) .setSound(null) - .setVibrate(null) .setDeleteIntent(createOnDismissedIntent(mContext, alarm.getId())); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - String color = alarm.getColor(); + long vibration = (long) alarm.getVibration(); + + long[] vibrationPattern = vibration == 0 ? DEFAULT_VIBRATE_PATTERN : new long[]{0, vibration, 1000, vibration}; - NotificationChannel mChannel = new NotificationChannel(channelID, "alarmnotif", NotificationManager.IMPORTANCE_HIGH); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel mChannel = new NotificationChannel(channelID, "Alarm Notify", NotificationManager.IMPORTANCE_HIGH); mChannel.enableLights(true); - mChannel.enableVibration(alarm.isVibrate()); + + String color = alarm.getColor(); if (color != null && !color.equals("")) { mChannel.setLightColor(Color.parseColor(color)); } - mChannel.setVibrationPattern(new long[]{1000, 2000}); + + if(!mChannel.canBypassDnd()){ + mChannel.setBypassDnd(alarm.isBypassDnd()); + } + + mChannel.setVibrationPattern(null); + + // play vibration + if (alarm.isVibrate()) { + Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + if (vibrator.hasVibrator()) { + vibrator.vibrate(VibrationEffect.createWaveform(vibrationPattern, 0)); + } + } + 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); @@ -416,7 +472,7 @@ void sendNotification(AlarmModel alarm) { //large icon String largeIcon = alarm.getLargeIcon(); - if (largeIcon != null && !largeIcon.equals("") && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + 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) { @@ -424,25 +480,6 @@ void sendNotification(AlarmModel alarm) { } } - //color - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - String color = alarm.getColor(); - if (color != null && !color.equals("")) { - mBuilder.setColor(Color.parseColor(color)); - } - } - - //vibrate - boolean vibrate = alarm.isVibrate(); - if (vibrate) { - long vibration = (long) alarm.getVibration(); - if (vibration == 0) - vibration = DEFAULT_VIBRATION; - Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); - assert vibrator != null; - vibrator.vibrate(vibration); - } - // set tag and push notification Notification notification = mBuilder.build(); diff --git a/example/App.js b/example/App.js index 58149fb..2cb1f01 100644 --- a/example/App.js +++ b/example/App.js @@ -28,11 +28,12 @@ const repeatAlarmNotifData = { message: 'Stand up', vibrate: true, play_sound: true, - schedule_type: 'repeat', channel: 'wakeup', data: {content: 'my notification id is 22'}, loop_sound: true, - repeat_interval: 1, // repeat after 1 minute + schedule_type: 'repeat', + repeat_interval: 'minutely', + interval_value: 5, // repeat after 5 minutes }; class App extends Component { diff --git a/index.js b/index.js index 93a53f7..376abcd 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,20 @@ ReactNativeAN.scheduleAlarm = async (details) => { throw new Error('failed to schedule alarm because fire date is missing'); } + 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'); + } + return await RNAlarmNotification.scheduleAlarm(details); };