Skip to content

Commit

Permalink
Merge pull request #11 from maurizi/better-notifications
Browse files Browse the repository at this point in the history
Improve notification text to include next alarm time
  • Loading branch information
maurizi committed Sep 28, 2014
2 parents 5fddcbd + cd7fe0f commit efab018
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 33 deletions.
30 changes: 19 additions & 11 deletions Geoclock/src/main/java/maurizi/geoclock/GeoAlarm.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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()
Expand All @@ -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) {
Expand All @@ -122,28 +124,34 @@ private LocalDate getSoonestDayForRepeatingAlarm(LocalDateTime now) {
return now.toLocalDate().with(next(nextDayForAlarm));
}

static Collection<GeoAlarm> getGeoAlarms(Context context) {
public static Collection<GeoAlarm> getGeoAlarms(Context context) {
SharedPreferences prefs = getSharedAlarmPreferences(context);
return ImmutableList.<GeoAlarm>builder()
.addAll(filter(transform(prefs.getAll().values(), GeoAlarm::parse),
(GeoAlarm geoAlarm) -> geoAlarm != null))
.build();
}

static Function<Geofence, GeoAlarm> getGeoAlarmForGeofenceFn(Context context) {
SharedPreferences prefs = getSharedAlarmPreferences(context);
public static Function<Geofence, GeoAlarm> 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();
}
Expand Down
70 changes: 58 additions & 12 deletions Geoclock/src/main/java/maurizi/geoclock/GeofenceReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Set<T> apply(Set<T> a, Set<T> b);
}

@Override
public void onConnected(Bundle bundle) {
int transition = LocationClient.getGeofenceTransition(this.intent);
List<Geofence> affectedGeofences = LocationClient.getTriggeringGeofences(intent);
List<GeoAlarm> affectedAlarms = Lists.transform(affectedGeofences, getGeoAlarmForGeofenceFn(context));
final int transition = LocationClient.getGeofenceTransition(this.intent);

final List<Geofence> affectedGeofences = LocationClient.getTriggeringGeofences(intent);
final ImmutableSet<GeoAlarm> 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<GeoAlarm> currentAlarms = changeActiveAlarms(affectedAlarms, Sets::union);

// TODO: Use Alarm Manager to set alarms, using GeoAlarm.getAlarmManagerTime
// final AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Expand All @@ -43,13 +63,27 @@ public void onConnected(Bundle bundle) {
// }

} else if (transition == Geofence.GEOFENCE_TRANSITION_EXIT) {
notificationManager.cancelAll();
ImmutableSet<GeoAlarm> 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<GeoAlarm> 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);
Expand All @@ -58,24 +92,36 @@ private void setNotification(final NotificationManager manager) {

PendingIntent notificationPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

// TODO: Fill in <ENTER TIME HERE>
// 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 <ENTER TIME HERE>")
.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<GeoAlarm> changeActiveAlarms(ImmutableSet<GeoAlarm> triggerAlarms, SetOp<GeoAlarm> 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<GeoAlarm> savedAlarms = ImmutableSet.copyOf(gson.fromJson(savedActiveAlarmsJson, GeoAlarm[].class));

final ImmutableSet<GeoAlarm> currentAlarms = ImmutableSet.copyOf(op.apply(savedAlarms, triggerAlarms));
activeAlarmsPrefs.edit().putString(ACTIVE_ALARM_PREFS, gson.toJson(currentAlarms.toArray())).apply();
setNotification(currentAlarms);

return currentAlarms;
}
}
12 changes: 6 additions & 6 deletions Geoclock/src/test/java/maurizi/geoclock/test/GeoAlarmTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Wrapper> registeredReceivers = Robolectric.getShadowApplication().getRegisteredReceivers();
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit efab018

Please sign in to comment.