Merge "Show chronometer in expired timer notification" into ub-deskclock-fantasy
diff --git a/src/com/android/deskclock/data/StopwatchModel.java b/src/com/android/deskclock/data/StopwatchModel.java
index ecbd864..3589666 100644
--- a/src/com/android/deskclock/data/StopwatchModel.java
+++ b/src/com/android/deskclock/data/StopwatchModel.java
@@ -44,6 +44,7 @@
     private final NotificationManagerCompat mNotificationManager;
 
     /** Update stopwatch notification when locale changes. */
+    @SuppressWarnings("FieldCanBeLocal")
     private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
 
     /** The listeners to notify when the stopwatch or its laps change. */
diff --git a/src/com/android/deskclock/data/TimerModel.java b/src/com/android/deskclock/data/TimerModel.java
index b64ac39..745296b 100644
--- a/src/com/android/deskclock/data/TimerModel.java
+++ b/src/com/android/deskclock/data/TimerModel.java
@@ -31,9 +31,7 @@
 import android.media.RingtoneManager;
 import android.net.Uri;
 import android.support.annotation.StringRes;
-import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.NotificationManagerCompat;
-import android.text.TextUtils;
 import android.util.ArraySet;
 
 import com.android.deskclock.AlarmAlertWakeLock;
@@ -42,7 +40,6 @@
 import com.android.deskclock.Utils;
 import com.android.deskclock.events.Events;
 import com.android.deskclock.settings.SettingsActivity;
-import com.android.deskclock.timer.ExpiredTimersActivity;
 import com.android.deskclock.timer.TimerKlaxon;
 import com.android.deskclock.timer.TimerService;
 
@@ -75,12 +72,14 @@
     private final NotificationManagerCompat mNotificationManager;
 
     /** Update timer notification when locale changes. */
+    @SuppressWarnings("FieldCanBeLocal")
     private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
 
     /**
      * Retain a hard reference to the shared preference observer to prevent it from being garbage
      * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail.
      */
+    @SuppressWarnings("FieldCanBeLocal")
     private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
 
     /** The listeners to notify when a timer is added, updated or removed. */
@@ -662,68 +661,8 @@
             return;
         }
 
-        // Generate some descriptive text, a title, and an action name based on the timer count.
-        final int timerId;
-        final String contentText;
-        final String contentTitle;
-        final String resetActionTitle;
-        if (expired.size() > 1) {
-            timerId = -1;
-            contentText = mContext.getString(R.string.timer_multi_times_up, expired.size());
-            contentTitle = mContext.getString(R.string.timer_notification_label);
-            resetActionTitle = mContext.getString(R.string.timer_stop_all);
-        } else {
-            final Timer timer = expired.get(0);
-            timerId = timer.getId();
-            resetActionTitle = mContext.getString(R.string.timer_stop);
-            contentText = mContext.getString(R.string.timer_times_up);
-
-            final String label = timer.getLabel();
-            if (TextUtils.isEmpty(label)) {
-                contentTitle = mContext.getString(R.string.timer_notification_label);
-            } else {
-                contentTitle = label;
-            }
-        }
-
-        // Content intent shows the timer full screen when clicked.
-        final Intent content = new Intent(mContext, ExpiredTimersActivity.class);
-        final PendingIntent pendingContent = Utils.pendingActivityIntent(mContext, content);
-
-        // Full screen intent has flags so it is different than the content intent.
-        final Intent fullScreen = new Intent(mContext, ExpiredTimersActivity.class)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
-        final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(mContext, fullScreen);
-
-        // First action intent is either reset single timer or reset all timers.
-        final Intent reset = TimerService.createResetExpiredTimersIntent(mContext);
-        final PendingIntent pendingReset = Utils.pendingServiceIntent(mContext, reset);
-
-        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
-                .setWhen(0)
-                .setOngoing(true)
-                .setLocalOnly(true)
-                .setAutoCancel(false)
-                .setContentText(contentText)
-                .setContentTitle(contentTitle)
-                .setContentIntent(pendingContent)
-                .setSmallIcon(R.drawable.stat_notify_timer)
-                .setFullScreenIntent(pendingFullScreen, true)
-                .setPriority(NotificationCompat.PRIORITY_MAX)
-                .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
-                .addAction(R.drawable.ic_stop_24dp, resetActionTitle, pendingReset);
-
-        // Add a second action if only a single timer is expired.
-        if (expired.size() == 1) {
-            // Second action intent adds a minute to a single timer.
-            final Intent addMinute = TimerService.createAddMinuteTimerIntent(mContext, timerId);
-            final PendingIntent pendingAddMinute = Utils.pendingServiceIntent(mContext, addMinute);
-            final String addMinuteTitle = mContext.getString(R.string.timer_plus_1_min);
-            builder.addAction(R.drawable.ic_add_24dp, addMinuteTitle, pendingAddMinute);
-        }
-
-        // Update the notification.
-        final Notification notification = builder.build();
+        // Otherwise build and post a foreground notification reflecting the latest expired timers.
+        final Notification notification = getNotificationBuilder().buildHeadsUp(mContext, expired);
         final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
         mService.startForeground(notificationId, notification);
     }
@@ -786,5 +725,12 @@
          * @return a notification reporting the state of the {@code unexpiredTimers}
          */
         Notification build(Context context, List<Timer> unexpiredTimers);
+
+        /**
+         * @param context a context to use for fetching resources
+         * @param expiredTimers all expired timers
+         * @return a heads-up notification reporting the state of the {@code expiredTimers}
+         */
+        Notification buildHeadsUp(Context context, List<Timer> expiredTimers);
     }
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerNotificationBuilderN.java b/src/com/android/deskclock/data/TimerNotificationBuilderN.java
index b8b52d9..50a7e08 100644
--- a/src/com/android/deskclock/data/TimerNotificationBuilderN.java
+++ b/src/com/android/deskclock/data/TimerNotificationBuilderN.java
@@ -31,6 +31,7 @@
 import com.android.deskclock.HandleDeskClockApiCalls;
 import com.android.deskclock.R;
 import com.android.deskclock.Utils;
+import com.android.deskclock.timer.ExpiredTimersActivity;
 import com.android.deskclock.timer.TimerService;
 
 import java.util.ArrayList;
@@ -51,29 +52,24 @@
 
         // Compute some values required below.
         final boolean running = timer.isRunning();
-        final String pname = context.getPackageName();
         final Resources res = context.getResources();
 
-        // The in-app timer display rounds *up* to the next second. To mirror that behavior in the
-        // notification's Chronometer, pad in an extra second.
-        final long remainingTime = timer.getRemainingTime() + SECOND_IN_MILLIS;
-
-        // Chronometer will reach 0:00 at remainingTime milliseconds in the future.
-        final long base = SystemClock.elapsedRealtime() + remainingTime;
-
+        final long base = getChronometerBase(timer);
+        final String pname = context.getPackageName();
         final RemoteViews content = new RemoteViews(pname, R.layout.chronometer_notif_content);
         content.setChronometerCountDown(R.id.chronometer, true);
         content.setChronometer(R.id.chronometer, base, null, running);
 
         final List<Notification.Action> actions = new ArrayList<>(2);
 
+        final CharSequence stateText;
         if (count == 1) {
             if (running) {
                 // Single timer is running.
                 if (TextUtils.isEmpty(timer.getLabel())) {
-                    content.setTextViewText(R.id.state, res.getString(R.string.timer_notification_label));
+                    stateText = res.getString(R.string.timer_notification_label);
                 } else {
-                    content.setTextViewText(R.id.state, timer.getLabel());
+                    stateText = timer.getLabel();
                 }
 
                 // Left button: Pause
@@ -98,7 +94,7 @@
 
             } else {
                 // Single timer is paused.
-                content.setTextViewText(R.id.state, res.getString(R.string.timer_paused));
+                stateText = res.getString(R.string.timer_paused);
 
                 // Left button: Start
                 final Intent start = new Intent(context, TimerService.class)
@@ -123,10 +119,10 @@
         } else {
             if (running) {
                 // At least one timer is running.
-                content.setTextViewText(R.id.state, res.getString(R.string.timers_in_use, count));
+                stateText = res.getString(R.string.timers_in_use, count);
             } else {
                 // All timers are paused.
-                content.setTextViewText(R.id.state, res.getString(R.string.timers_stopped, count));
+                stateText = res.getString(R.string.timers_stopped, count);
             }
 
             final Intent reset = TimerService.createResetUnexpiredTimersIntent(context);
@@ -137,6 +133,8 @@
             actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
         }
 
+        content.setTextViewText(R.id.state, stateText);
+
         // Intent to load the app and show the timer when the notification is tapped.
         final Intent showApp = new Intent(context, HandleDeskClockApiCalls.class)
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -162,4 +160,91 @@
                 .setActions(actions.toArray(new Notification.Action[actions.size()]))
                 .build();
     }
+
+    @Override
+    public Notification buildHeadsUp(Context context, List<Timer> expired) {
+        final Timer timer = expired.get(0);
+
+        // First action intent is to reset all timers.
+        final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_stop_24dp);
+        final Intent reset = TimerService.createResetExpiredTimersIntent(context);
+        final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
+
+        // Generate some descriptive text, a title, and an action name based on the timer count.
+        final CharSequence stateText;
+        final int count = expired.size();
+        final List<Notification.Action> actions = new ArrayList<>(2);
+        if (count == 1) {
+            final String label = timer.getLabel();
+            if (TextUtils.isEmpty(label)) {
+                stateText = context.getString(R.string.timer_times_up);
+            } else {
+                stateText = label;
+            }
+
+            // Left button: Reset single timer
+            final CharSequence title1 = context.getString(R.string.timer_stop);
+            actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+
+            // Right button: Add minute
+            final Intent addTime = TimerService.createAddMinuteTimerIntent(context, timer.getId());
+            final PendingIntent intent2 = Utils.pendingServiceIntent(context, addTime);
+            final Icon icon2 = Icon.createWithResource(context, R.drawable.ic_add_24dp);
+            final CharSequence title2 = context.getString(R.string.timer_plus_1_min);
+            actions.add(new Notification.Action.Builder(icon2, title2, intent2).build());
+
+        } else {
+            stateText = context.getString(R.string.timer_multi_times_up, count);
+
+            // Left button: Reset all timers
+            final CharSequence title1 = context.getString(R.string.timer_stop_all);
+            actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+        }
+
+        final long base = getChronometerBase(timer);
+
+        final String pname = context.getPackageName();
+        final RemoteViews contentView = new RemoteViews(pname, R.layout.chronometer_notif_content);
+        contentView.setChronometerCountDown(R.id.chronometer, true);
+        contentView.setChronometer(R.id.chronometer, base, null, true);
+        contentView.setTextViewText(R.id.state, stateText);
+
+        // Content intent shows the timer full screen when clicked.
+        final Intent content = new Intent(context, ExpiredTimersActivity.class);
+        final PendingIntent contentIntent = Utils.pendingActivityIntent(context, content);
+
+        // Full screen intent has flags so it is different than the content intent.
+        final Intent fullScreen = new Intent(context, ExpiredTimersActivity.class)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
+        final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(context, fullScreen);
+
+        return new Notification.Builder(context)
+                .setOngoing(true)
+                .setLocalOnly(true)
+                .setShowWhen(false)
+                .setAutoCancel(false)
+                .setContentIntent(contentIntent)
+                .setCustomContentView(contentView)
+                .setPriority(Notification.PRIORITY_MAX)
+                .setDefaults(Notification.DEFAULT_LIGHTS)
+                .setSmallIcon(R.drawable.stat_notify_timer)
+                .setFullScreenIntent(pendingFullScreen, true)
+                .setStyle(new Notification.DecoratedCustomViewStyle())
+                .setActions(actions.toArray(new Notification.Action[actions.size()]))
+                .build();
+    }
+
+    /**
+     * @param timer the timer on which to base the chronometer display
+     * @return the time at which the chronometer will/did reach 0:00 in realtime
+     */
+    private static long getChronometerBase(Timer timer) {
+        // The in-app timer display rounds *up* to the next second for positive timer values. Mirror
+        // that behavior in the notification's Chronometer by padding in an extra second as needed.
+        final long remaining = timer.getRemainingTime();
+        final long adjustedRemaining = remaining < 0 ? remaining : remaining + SECOND_IN_MILLIS;
+
+        // Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now.
+        return SystemClock.elapsedRealtime() + adjustedRemaining;
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerNotificationBuilderPreN.java b/src/com/android/deskclock/data/TimerNotificationBuilderPreN.java
index 82ddfec..c645d99 100644
--- a/src/com/android/deskclock/data/TimerNotificationBuilderPreN.java
+++ b/src/com/android/deskclock/data/TimerNotificationBuilderPreN.java
@@ -31,6 +31,7 @@
 import com.android.deskclock.HandleDeskClockApiCalls;
 import com.android.deskclock.R;
 import com.android.deskclock.Utils;
+import com.android.deskclock.timer.ExpiredTimersActivity;
 import com.android.deskclock.timer.TimerService;
 
 import java.util.List;
@@ -166,6 +167,73 @@
         return builder.build();
     }
 
+    @Override
+    public Notification buildHeadsUp(Context context, List<Timer> expired) {
+        // Generate some descriptive text, a title, and an action name based on the timer count.
+        final int timerId;
+        final String contentText;
+        final String contentTitle;
+        final String resetActionTitle;
+        final int count = expired.size();
+
+        if (count == 1) {
+            final Timer timer = expired.get(0);
+            timerId = timer.getId();
+            resetActionTitle = context.getString(R.string.timer_stop);
+            contentText = context.getString(R.string.timer_times_up);
+
+            final String label = timer.getLabel();
+            if (TextUtils.isEmpty(label)) {
+                contentTitle = context.getString(R.string.timer_notification_label);
+            } else {
+                contentTitle = label;
+            }
+        } else {
+            timerId = -1;
+            contentText = context.getString(R.string.timer_multi_times_up, count);
+            contentTitle = context.getString(R.string.timer_notification_label);
+            resetActionTitle = context.getString(R.string.timer_stop_all);
+        }
+
+        // Content intent shows the expired timers full screen when clicked.
+        final Intent content = new Intent(context, ExpiredTimersActivity.class);
+        final PendingIntent pendingContent = Utils.pendingActivityIntent(context, content);
+
+        // Full screen intent has flags so it is different than the content intent.
+        final Intent fullScreen = new Intent(context, ExpiredTimersActivity.class)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
+        final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(context, fullScreen);
+
+        // Left button: Reset timer / Reset all timers
+        final Intent reset = TimerService.createResetExpiredTimersIntent(context);
+        final PendingIntent pendingReset = Utils.pendingServiceIntent(context, reset);
+
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
+                .setOngoing(true)
+                .setLocalOnly(true)
+                .setShowWhen(false)
+                .setAutoCancel(false)
+                .setContentText(contentText)
+                .setContentTitle(contentTitle)
+                .setContentIntent(pendingContent)
+                .setSmallIcon(R.drawable.stat_notify_timer)
+                .setFullScreenIntent(pendingFullScreen, true)
+                .setPriority(NotificationCompat.PRIORITY_MAX)
+                .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
+                .addAction(R.drawable.ic_stop_24dp, resetActionTitle, pendingReset);
+
+        // Add a second action if only a single timer is expired.
+        if (count == 1) {
+            // Right button: Add minute
+            final Intent addMinute = TimerService.createAddMinuteTimerIntent(context, timerId);
+            final PendingIntent pendingAddMinute = Utils.pendingServiceIntent(context, addMinute);
+            final String addMinuteTitle = context.getString(R.string.timer_plus_1_min);
+            builder.addAction(R.drawable.ic_add_24dp, addMinuteTitle, pendingAddMinute);
+        }
+
+        return builder.build();
+    }
+
     /**
      * Format "7 hours 52 minutes remaining"
      */