Skip to content

Commit

Permalink
Merge pull request #286 from backbonelabs/schedule-timezone
Browse files Browse the repository at this point in the history
Timezone Changes
  • Loading branch information
kevhuang authored Apr 5, 2017
2 parents 623fe4c + f62476e commit f2efeb1
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 62 deletions.
6 changes: 6 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver android:name=".TimeZoneReceiver"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.TIMEZONE_CHANGED"/>
</intent-filter>
</receiver>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,9 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;

import java.util.Calendar;
import java.util.GregorianCalendar;

import co.backbonelabs.backbone.util.Constants;
import timber.log.Timber;

import static android.content.Context.MODE_PRIVATE;

// This class is responsible to listen to the boot events, particularly here is when
// the boot process has been completed, so we can proceed to reschedule notifications when needed
public class BootReceiver extends BroadcastReceiver {
Expand All @@ -25,50 +15,7 @@ public void onReceive(Context context, Intent intent) {
// Check for any scheduled notifications and proceed with rescheduling
Timber.d("Boot Completed");

// Types of notification we need check
int[] notificationTypes = new int[] {
Constants.NOTIFICATION_TYPES.INACTIVITY_REMINDER,
Constants.NOTIFICATION_TYPES.DAILY_REMINDER,
Constants.NOTIFICATION_TYPES.SINGLE_REMINDER,
Constants.NOTIFICATION_TYPES.INFREQUENT_REMINDER
};

SharedPreferences preference = context.getSharedPreferences(Constants.NOTIFICATION_PREFERENCES, MODE_PRIVATE);

for (int type : notificationTypes) {
// Check if the notification of this type has been scheduled
if (preference.getBoolean(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_IS_SCHEDULED, type), false)) {
// Detected scheduled notification, check if we still need to reschedule
long fireTimestamp = preference.getLong(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_TIMESTAMP, type), 0);
long repeatInterval = preference.getLong(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_REPEAT_INTERVAL, type), 0);
boolean shouldRepeat = repeatInterval > 0;

WritableMap notificationParam = Arguments.createMap();
notificationParam.putInt(Constants.NOTIFICATION_PARAMETER_TYPE, type);

Calendar currentCalendar = GregorianCalendar.getInstance();

if (!shouldRepeat) {
// Non-repeated timers should only be rescheduled when the scheduled time is in the future
if (fireTimestamp >= currentCalendar.getTimeInMillis()) {
Timber.d("Reschedule Notification: %d", type);
notificationParam.putDouble(Constants.NOTIFICATION_PARAMETER_SCHEDULED_TIMESTAMP, fireTimestamp);
NotificationService.scheduleNotification(context, notificationParam);
}
}
else {
// If the previous scheduled timestamp is in the past,
// skip to the next timestamp
while (fireTimestamp < currentCalendar.getTimeInMillis()) {
fireTimestamp += repeatInterval;
}

Timber.d("Reschedule Notification: %d", type);
notificationParam.putDouble(Constants.NOTIFICATION_PARAMETER_SCHEDULED_TIMESTAMP, fireTimestamp);
NotificationService.scheduleNotification(context, notificationParam);
}
}
}
NotificationService.rescheduleNotification(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,24 @@ public void onReceive(Context context, Intent intent) {

// Reschedule the timer if it needs to be repeated
SharedPreferences preference = context.getSharedPreferences(Constants.NOTIFICATION_PREFERENCES, MODE_PRIVATE);
long fireTimestamp = preference.getLong(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_TIMESTAMP, type), 0);
int year = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_YEAR, type), 0);
int month = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_MONTH, type), 0);
int day = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_DAY, type), 0);
int hour = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_HOUR, type), 0);
int minute = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_MINUTE, type), 0);
int second = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_SECOND, type), 0);
long repeatInterval = preference.getLong(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_REPEAT_INTERVAL, type), 0);
boolean shouldRepeat = repeatInterval > 0;

if (shouldRepeat) {
// If the previous scheduled timestamp is in the past,
// skip to the next timestamp
Calendar currentCalendar = GregorianCalendar.getInstance();
while (fireTimestamp < currentCalendar.getTimeInMillis()) {
Calendar fireCalendar = GregorianCalendar.getInstance();
fireCalendar.set(year, month, day, hour, minute, second);
long fireTimestamp = fireCalendar.getTimeInMillis();

while (fireTimestamp <= currentCalendar.getTimeInMillis()) {
fireTimestamp += repeatInterval;
}

Expand All @@ -54,5 +63,8 @@ public void onReceive(Context context, Intent intent) {
Timber.d("Repeat Notification: %d %d %d", type, repeatInterval, fireTimestamp);
NotificationService.scheduleNotification(context, notificationParam);
}
else {
NotificationService.unscheduleNotification(context, type);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@
import android.os.Build;
import android.support.v4.app.NotificationCompat;

import com.facebook.react.bridge.Arguments;
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.WritableMap;

import java.util.Calendar;
import java.util.GregorianCalendar;

import co.backbonelabs.backbone.util.Constants;
import timber.log.Timber;

import static android.content.Context.CONSUMER_IR_SERVICE;
import static android.content.Context.MODE_PRIVATE;

public class NotificationService extends ReactContextBaseJavaModule {
Expand Down Expand Up @@ -118,7 +119,7 @@ public static void scheduleNotification(Context context, ReadableMap notificatio
case Constants.NOTIFICATION_TYPES.SINGLE_REMINDER:
title = "It's time!";
text = "It's that time of the day again! Brace yourself!";
break;
break;
}

// Invalid notification type, exit the function
Expand Down Expand Up @@ -172,7 +173,7 @@ public static void scheduleNotification(Context context, ReadableMap notificatio
// Otherwise use the current time components
if (year != -1) {
nextFireCalendar.set(Calendar.YEAR, year);
nextFireCalendar.set(Calendar.MONTH, month);
nextFireCalendar.set(Calendar.MONTH, month - 1); // Month in the API is set from 0-11
nextFireCalendar.set(Calendar.DAY_OF_MONTH, day);
}

Expand Down Expand Up @@ -231,8 +232,19 @@ public static void scheduleNotification(Context context, ReadableMap notificatio
SharedPreferences preference = context.getSharedPreferences(Constants.NOTIFICATION_PREFERENCES, MODE_PRIVATE);
SharedPreferences.Editor editor = preference.edit();

// Store the exact date parts instead of timestamp to handle changes in timezone
// in order to have the notification fires at the exact same time as originally scheduled
// on the local time
Calendar nextDate = GregorianCalendar.getInstance();
nextDate.setTimeInMillis(fireTimestamp);

editor.putBoolean(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_IS_SCHEDULED, type), true);
editor.putLong(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_TIMESTAMP, type), fireTimestamp);
editor.putInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_YEAR, type), nextDate.get(Calendar.YEAR));
editor.putInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_MONTH, type), nextDate.get(Calendar.MONTH));
editor.putInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_DAY, type), nextDate.get(Calendar.DAY_OF_MONTH));
editor.putInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_HOUR, type), nextDate.get(Calendar.HOUR_OF_DAY));
editor.putInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_MINUTE, type), nextDate.get(Calendar.MINUTE));
editor.putInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_SECOND, type), nextDate.get(Calendar.SECOND));
editor.putLong(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_REPEAT_INTERVAL, type), repeatInterval);

editor.commit();
Expand Down Expand Up @@ -265,7 +277,12 @@ public static void unscheduleNotification(Context context, int type) {
SharedPreferences.Editor editor = preference.edit();

editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_IS_SCHEDULED, type));
editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_TIMESTAMP, type));
editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_YEAR, type));
editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_MONTH, type));
editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_DAY, type));
editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_HOUR, type));
editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_MINUTE, type));
editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_SECOND, type));
editor.remove(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_REPEAT_INTERVAL, type));

editor.commit();
Expand Down Expand Up @@ -303,6 +320,69 @@ public static void unscheduleNotification(Context context, int type) {
}
}

/**
* Reschedule notifications called by certain events including boot process and timezone changes
* @param context Current context to be used for various initializations
*/
public static void rescheduleNotification(Context context) {
// Types of notification we need check
int[] notificationTypes = new int[] {
Constants.NOTIFICATION_TYPES.INACTIVITY_REMINDER,
Constants.NOTIFICATION_TYPES.DAILY_REMINDER,
Constants.NOTIFICATION_TYPES.SINGLE_REMINDER,
Constants.NOTIFICATION_TYPES.INFREQUENT_REMINDER
};

SharedPreferences preference = context.getSharedPreferences(Constants.NOTIFICATION_PREFERENCES, MODE_PRIVATE);

for (int type : notificationTypes) {
// Check if the notification of this type has been scheduled
if (preference.getBoolean(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_IS_SCHEDULED, type), false)) {
// Detected scheduled notification, check if we still need to reschedule
int year = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_YEAR, type), 0);
int month = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_MONTH, type), 0);
int day = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_DAY, type), 0);
int hour = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_HOUR, type), 0);
int minute = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_MINUTE, type), 0);
int second = preference.getInt(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_SECOND, type), 0);
long repeatInterval = preference.getLong(String.format("%s%d", Constants.NOTIFICATION_PREFERENCE_FORMAT_REPEAT_INTERVAL, type), 0);
boolean shouldRepeat = repeatInterval > 0;

WritableMap notificationParam = Arguments.createMap();
notificationParam.putInt(Constants.NOTIFICATION_PARAMETER_TYPE, type);

Calendar currentCalendar = GregorianCalendar.getInstance();
Calendar fireCalendar = GregorianCalendar.getInstance();
fireCalendar.set(year, month, day, hour, minute, second);
long fireTimestamp = fireCalendar.getTimeInMillis();

if (!shouldRepeat) {
// Non-repeated timers should only be rescheduled when
// the scheduled time is in the future, otherwise clean it up.
if (fireTimestamp >= currentCalendar.getTimeInMillis()) {
Timber.d("Reschedule Notification: %d", type);
notificationParam.putDouble(Constants.NOTIFICATION_PARAMETER_SCHEDULED_TIMESTAMP, fireTimestamp);
NotificationService.scheduleNotification(context, notificationParam);
}
else {
NotificationService.unscheduleNotification(context, type);
}
}
else {
// If the previous scheduled timestamp is in the past,
// skip to the next timestamp
while (fireTimestamp < currentCalendar.getTimeInMillis()) {
fireTimestamp += repeatInterval;
}

Timber.d("Reschedule Notification: %d", type);
notificationParam.putDouble(Constants.NOTIFICATION_PARAMETER_SCHEDULED_TIMESTAMP, fireTimestamp);
NotificationService.scheduleNotification(context, notificationParam);
}
}
}
}

/**
* Creates builder class for Notification objects that launches the MainActivity.
* A Notification can be created from the builder class to be passed to a NotificationManager.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package co.backbonelabs.backbone;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import timber.log.Timber;

// Listen to any changes to the current local timezone and reschedule notifications
public class TimeZoneReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
// Check for any scheduled notifications and proceed with rescheduling
Timber.d("Time Zone Changed");

NotificationService.rescheduleNotification(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ public interface IntCallBack {

public static final String NOTIFICATION_PREFERENCES = "co.backbonelabs.backbone.NOTIFICATION_PREFERENCES";
public static final String NOTIFICATION_PREFERENCE_FORMAT_IS_SCHEDULED = "notification-isScheduled-";
public static final String NOTIFICATION_PREFERENCE_FORMAT_TIMESTAMP = "notification-scheduledTimestamp-";
public static final String NOTIFICATION_PREFERENCE_FORMAT_YEAR = "notification-scheduledYear-";
public static final String NOTIFICATION_PREFERENCE_FORMAT_MONTH = "notification-scheduledMonth-";
public static final String NOTIFICATION_PREFERENCE_FORMAT_DAY = "notification-scheduledDay-";
public static final String NOTIFICATION_PREFERENCE_FORMAT_HOUR = "notification-scheduledHour-";
public static final String NOTIFICATION_PREFERENCE_FORMAT_MINUTE = "notification-scheduledMinute-";
public static final String NOTIFICATION_PREFERENCE_FORMAT_SECOND = "notification-scheduledSecond-";
public static final String NOTIFICATION_PREFERENCE_FORMAT_REPEAT_INTERVAL = "notification-repeatInterval-";

public static final String NOTIFICATION_PARAMETER_TYPE = "notificationType";
Expand Down
1 change: 1 addition & 0 deletions ios/backbone/NotificationService.m
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ - (id)init {

localNotification.alertTitle = NSLocalizedString(title, nil);
localNotification.alertBody = NSLocalizedString(text, nil);
localNotification.timeZone = [NSTimeZone defaultTimeZone];
localNotification.fireDate = [fireDate dateByAddingTimeInterval:initialDelay];
localNotification.userInfo = @{@"type" : @(type)};
localNotification.soundName = UILocalNotificationDefaultSoundName;
Expand Down

0 comments on commit f2efeb1

Please sign in to comment.