diff --git a/Geoclock/src/main/java/maurizi/geoclock/GeoAlarm.java b/Geoclock/src/main/java/maurizi/geoclock/GeoAlarm.java index 9916ccf..7d22d4c 100644 --- a/Geoclock/src/main/java/maurizi/geoclock/GeoAlarm.java +++ b/Geoclock/src/main/java/maurizi/geoclock/GeoAlarm.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; import android.support.annotation.Nullable; import com.google.android.gms.location.Geofence; @@ -20,6 +21,7 @@ import org.threeten.bp.LocalDateTime; import org.threeten.bp.LocalTime; import org.threeten.bp.ZoneId; +import org.threeten.bp.ZonedDateTime; import java.util.ArrayList; import java.util.Calendar; @@ -86,7 +88,7 @@ private boolean isAlarmForToday(LocalDateTime now) { /** * @return A Date object for just before the alarm is due to go off */ - public long getAlarmManagerTime(LocalDateTime now) { + public ZonedDateTime getAlarmManagerTime(LocalDateTime now) { final LocalTime alarmTime = getAlarmTime(); final LocalDateTime alarmDateTime = days == null || days.isEmpty() @@ -95,7 +97,7 @@ public long getAlarmManagerTime(LocalDateTime now) { : now.toLocalDate().plusDays(1)) : alarmTime.atDate(getSoonestDayForRepeatingAlarm(now)); - return alarmDateTime.atZone(ZoneId.systemDefault()).toEpochSecond(); + return alarmDateTime.atZone(ZoneId.systemDefault()); } private LocalDate getSoonestDayForRepeatingAlarm(LocalDateTime now) { @@ -122,7 +124,7 @@ private LocalDate getSoonestDayForRepeatingAlarm(LocalDateTime now) { return now.toLocalDate().with(next(nextDayForAlarm)); } - static Collection getGeoAlarms(Context context) { + public static Collection getGeoAlarms(Context context) { SharedPreferences prefs = getSharedAlarmPreferences(context); return ImmutableList.builder() .addAll(filter(transform(prefs.getAll().values(), GeoAlarm::parse), @@ -130,20 +132,26 @@ static Collection getGeoAlarms(Context context) { .build(); } - static Function getGeoAlarmForGeofenceFn(Context context) { - SharedPreferences prefs = getSharedAlarmPreferences(context); + public static Function getGeoAlarmForGeofenceFn(Context context) { + final SharedPreferences prefs = getSharedAlarmPreferences(context); return geofence -> parse(prefs.getString(geofence.getRequestId(), null)); } - static void replace(Context context, GeoAlarm oldAlarm, GeoAlarm newAlarm) { + public static void add(Context context, GeoAlarm newAlarm) { + replace(context, null, newAlarm); + } + + public static void replace(Context context, GeoAlarm oldAlarm, GeoAlarm newAlarm) { SharedPreferences prefs = getSharedAlarmPreferences(context); - prefs.edit() - .remove(oldAlarm.geofenceId) - .putString(newAlarm.geofenceId, gson.toJson(newAlarm, GeoAlarm.class)) - .commit(); + Editor editor = prefs.edit(); + if (oldAlarm != null && prefs.contains(oldAlarm.geofenceId)) { + editor.remove(oldAlarm.geofenceId); + } + editor.putString(newAlarm.geofenceId, gson.toJson(newAlarm, GeoAlarm.class)) + .commit(); } - static void remove(Context context, GeoAlarm oldAlarm) { + public static void remove(Context context, GeoAlarm oldAlarm) { SharedPreferences prefs = getSharedAlarmPreferences(context); prefs.edit().remove(oldAlarm.geofenceId).commit(); } diff --git a/Geoclock/src/main/java/maurizi/geoclock/GeofenceReceiver.java b/Geoclock/src/main/java/maurizi/geoclock/GeofenceReceiver.java index 4182e40..defd19f 100644 --- a/Geoclock/src/main/java/maurizi/geoclock/GeofenceReceiver.java +++ b/Geoclock/src/main/java/maurizi/geoclock/GeofenceReceiver.java @@ -5,36 +5,56 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import com.google.android.gms.location.Geofence; import com.google.android.gms.location.LocationClient; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.common.collect.Sets; +import com.google.gson.Gson; + +import org.threeten.bp.LocalDateTime; +import org.threeten.bp.ZonedDateTime; +import org.threeten.bp.format.DateTimeFormatter; +import org.threeten.bp.format.FormatStyle; import java.util.List; +import java.util.Set; +import static com.google.common.collect.Collections2.transform; import static maurizi.geoclock.GeoAlarm.getGeoAlarmForGeofenceFn; public class GeofenceReceiver extends AbstractGeoReceiver { + private static final Gson gson = new Gson(); private static final int NOTIFICATION_ID = 42; + private static final String ACTIVE_ALARM_PREFS = "active_alarm_prefs"; + + private interface SetOp { + Set apply(Set a, Set b); + } + @Override public void onConnected(Bundle bundle) { - int transition = LocationClient.getGeofenceTransition(this.intent); - List affectedGeofences = LocationClient.getTriggeringGeofences(intent); - List affectedAlarms = Lists.transform(affectedGeofences, getGeoAlarmForGeofenceFn(context)); + final int transition = LocationClient.getGeofenceTransition(this.intent); + + final List affectedGeofences = LocationClient.getTriggeringGeofences(intent); + final ImmutableSet affectedAlarms = ImmutableSet.copyOf(Lists.transform(affectedGeofences, + getGeoAlarmForGeofenceFn(context))); /* TODO: Need to keep track of which notifications are being shown currently * When you leave a GeoFence, you may still have some alarms left due to overlapping geofences * So we need to know if (current alarms - removedAlarms) is empty before removing the notifications */ - final NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if ((transition == Geofence.GEOFENCE_TRANSITION_ENTER)) { - setNotification(notificationManager); + ImmutableSet currentAlarms = changeActiveAlarms(affectedAlarms, Sets::union); // TODO: Use Alarm Manager to set alarms, using GeoAlarm.getAlarmManagerTime // final AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); @@ -43,13 +63,27 @@ public void onConnected(Bundle bundle) { // } } else if (transition == Geofence.GEOFENCE_TRANSITION_EXIT) { - notificationManager.cancelAll(); + ImmutableSet currentAlarms = changeActiveAlarms(affectedAlarms, Sets::difference); // TODO: Reset by iterating through geofences?? It's unclear // TODO: Remove AlarmMAnager alarms for geofences we are leaving } } - private void setNotification(final NotificationManager manager) { + private void setNotification(final ImmutableSet activeAlarms) { + final NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (activeAlarms.isEmpty()) { + notificationManager.cancelAll(); + return; + } + + final LocalDateTime now = LocalDateTime.now(); + final GeoAlarm nextAlarm = Ordering.from(ZonedDateTime.timeLineOrder()) + .onResultOf((GeoAlarm alarm) -> alarm.getAlarmManagerTime(now)) + .min(activeAlarms); + final ZonedDateTime nextAlarmTime = nextAlarm.getAlarmManagerTime(now); + final String alarmFormattedTime = nextAlarmTime.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)); + // Create an content intent that comes with a back stack // This makes hitting back from the activity go to the home screen TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); @@ -58,24 +92,36 @@ private void setNotification(final NotificationManager manager) { PendingIntent notificationPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); - // TODO: Fill in // TODO: Add a cancel button // TODO: Make clicking the notification open the GeoAlarmFragment Notification notification = new NotificationCompat .Builder(context) .setSmallIcon(R.drawable.ic_launcher) .setOngoing(true) - .setContentTitle("Alarm") - .setContentText("Next alarm goes off in ") + .setContentTitle("Geo Alarm") + .setContentText(String.format("Alarm at %s for %s", alarmFormattedTime, nextAlarm.name)) .setContentIntent(notificationPendingIntent) .build(); // Issue the notification - manager.notify(NOTIFICATION_ID, notification); + notificationManager.notify(NOTIFICATION_ID, notification); } public static PendingIntent getPendingIntent(Context context) { Intent intent = new Intent(context, GeofenceReceiver.class); return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } + + private ImmutableSet changeActiveAlarms(ImmutableSet triggerAlarms, SetOp op) { + final SharedPreferences activeAlarmsPrefs = context.getSharedPreferences(ACTIVE_ALARM_PREFS, Context.MODE_PRIVATE); + + final String savedActiveAlarmsJson = activeAlarmsPrefs.getString(ACTIVE_ALARM_PREFS, gson.toJson(new GeoAlarm[] {})); + final Set savedAlarms = ImmutableSet.copyOf(gson.fromJson(savedActiveAlarmsJson, GeoAlarm[].class)); + + final ImmutableSet currentAlarms = ImmutableSet.copyOf(op.apply(savedAlarms, triggerAlarms)); + activeAlarmsPrefs.edit().putString(ACTIVE_ALARM_PREFS, gson.toJson(currentAlarms.toArray())).apply(); + setNotification(currentAlarms); + + return currentAlarms; + } } diff --git a/Geoclock/src/test/java/maurizi/geoclock/test/GeoAlarmTest.java b/Geoclock/src/test/java/maurizi/geoclock/test/GeoAlarmTest.java index 53bbc92..8c175bb 100644 --- a/Geoclock/src/test/java/maurizi/geoclock/test/GeoAlarmTest.java +++ b/Geoclock/src/test/java/maurizi/geoclock/test/GeoAlarmTest.java @@ -11,6 +11,7 @@ import org.threeten.bp.LocalDate; import org.threeten.bp.LocalDateTime; import org.threeten.bp.ZoneId; +import org.threeten.bp.ZonedDateTime; import maurizi.geoclock.GeoAlarm; import maurizi.geoclock.MapActivity; @@ -22,7 +23,7 @@ @SuppressWarnings("ConstantConditions") public class GeoAlarmTest { - private static final GeoAlarm testAlarm = GeoAlarm.builder() + static final GeoAlarm testAlarm = GeoAlarm.builder() .name("test") .location(new LatLng(0,0)) .radius(1000) @@ -33,17 +34,16 @@ private void assertAlarmManager(int alarmHour, int alarmDay, LocalDateTime curre assertAlarmManager(alarmHour, alarmDay, currentTime, expectedTime, new DayOfWeek[] {}, message); } - private void assertAlarmManager(int alarmHour, int alarmMinutes, LocalDateTime currentTime, LocalDateTime expectedTime, DayOfWeek[] days, String message) { + private void assertAlarmManager(int alarmHour, int alarmMinutes, LocalDateTime currentTime, LocalDateTime expectedLocalTime, DayOfWeek[] days, String message) { final GeoAlarm timedTestAlarm = testAlarm.withDays(ImmutableSet.copyOf(days)) .withHour(alarmHour) .withMinute(alarmMinutes); - final long expectedMillis = expectedTime.atZone(ZoneId.systemDefault()) - .toEpochSecond(); + final ZonedDateTime expectedTime = expectedLocalTime.atZone(ZoneId.systemDefault()); - final long actualMillis = timedTestAlarm.getAlarmManagerTime(currentTime); + final ZonedDateTime actualTime = timedTestAlarm.getAlarmManagerTime(currentTime); - assertEquals(message, expectedMillis, actualMillis); + assertEquals(message, expectedTime, actualTime); } @Test public void testAlarmManagerTime() { diff --git a/Geoclock/src/test/java/maurizi/geoclock/test/GeofenceReceiverTest.java b/Geoclock/src/test/java/maurizi/geoclock/test/GeofenceReceiverTest.java index de2f374..a218c6e 100644 --- a/Geoclock/src/test/java/maurizi/geoclock/test/GeofenceReceiverTest.java +++ b/Geoclock/src/test/java/maurizi/geoclock/test/GeofenceReceiverTest.java @@ -19,20 +19,31 @@ import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; import org.robolectric.shadows.ShadowApplication.Wrapper; +import org.robolectric.shadows.ShadowNotification; +import org.robolectric.shadows.ShadowNotificationManager; import java.util.List; +import maurizi.geoclock.GeoAlarm; import maurizi.geoclock.GeofenceReceiver; import maurizi.geoclock.test.shadows.ShadowLocationClient; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.robolectric.Robolectric.shadowOf; @RunWith(RobolectricTestRunner.class) @Config(emulateSdk = 18, shadows = {ShadowLocationClient.class}) public class GeofenceReceiverTest { + static final Geofence mockGeofence = mock(Geofence.class); + + @Before + public void setUp() { + when(mockGeofence.getRequestId()).thenReturn(GeoAlarmTest.testAlarm.geofenceId); + } + @Test public void testBroadcastReceiverRegistered() { List registeredReceivers = Robolectric.getShadowApplication().getRegisteredReceivers(); @@ -82,13 +93,22 @@ public void testIntentHandling() { @Test public void testNotificationIsAdded() { - NotificationManager notificationManager = (NotificationManager) Robolectric.application.getSystemService(Context.NOTIFICATION_SERVICE); - ShadowLocationClient.setGeofences(ImmutableList.of(mock(Geofence.class))); + final NotificationManager notificationManager = (NotificationManager) Robolectric.application.getSystemService(Context.NOTIFICATION_SERVICE); + + final GeoAlarm testAlarm = GeoAlarmTest.testAlarm.withHour(5).withMinute(0).withName("Place"); + GeoAlarm.add(Robolectric.application, testAlarm); + + final GeoAlarm laterTestAlarm = testAlarm.withHour(6).withName("Later").withGeofenceId("Different"); + GeoAlarm.add(Robolectric.application, laterTestAlarm); + ShadowLocationClient.setGeofences(ImmutableList.of(mockGeofence)); - GeofenceReceiver receiver = setupGeofenceReceiver(); + final GeofenceReceiver receiver = setupGeofenceReceiver(); receiver.onConnected(new Bundle()); - assertEquals(1, shadowOf(notificationManager).size()); + final ShadowNotificationManager notificationManagerShadow = shadowOf(notificationManager); + assertEquals(1, notificationManagerShadow.size()); + final ShadowNotification notificationShadow = shadowOf(notificationManagerShadow.getAllNotifications().get(0)); + assertEquals("Alarm at 5:00 AM for Place", notificationShadow.getContentText()); } private GeofenceReceiver setupGeofenceReceiver() {