Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b1d0d4d0 authored by Sean Stout's avatar Sean Stout
Browse files

Timer no longer has Pre-N and N+ notification builders.

Bug: 30793112
Test: manual - Confirm notifications post correctly on Pre-N and N
Change-Id: I7921707a021e98396cd8b5e586d9b9aefecc212a
parent e34ee918
Loading
Loading
Loading
Loading
+6 −49
Original line number Diff line number Diff line
@@ -92,6 +92,9 @@ final class TimerModel {
    /** The listeners to notify when a timer is added, updated or removed. */
    private final List<TimerListener> mTimerListeners = new ArrayList<>();

    /** Delegate that builds platform-specific timer notifications. */
    private final TimerNotificationBuilder mNotificationBuilder = new TimerNotificationBuilder();

    /**
     * The ids of expired timers for which the ringer is ringing. Not all expired timers have their
     * ids in this collection. If a timer was already expired when the app was started its id will
@@ -115,9 +118,6 @@ final class TimerModel {
    /** A mutable copy of the missed timers. */
    private List<Timer> mMissedTimers;

    /** Delegate that builds platform-specific timer notifications. */
    private NotificationBuilder mNotificationBuilder;

    /**
     * The service that keeps this application in the foreground while a heads-up timer
     * notification is displayed. Marking the service as foreground prevents the operating system
@@ -737,7 +737,7 @@ final class TimerModel {

        // Otherwise build and post a notification reflecting the latest unexpired timers.
        final Notification notification =
                getNotificationBuilder().build(mContext, mNotificationModel, unexpired);
                mNotificationBuilder.build(mContext, mNotificationModel, unexpired);
        final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId();
        mNotificationManager.notify(notificationId, notification);

@@ -761,7 +761,7 @@ final class TimerModel {
            return;
        }

        final Notification notification = getNotificationBuilder().buildMissed(mContext,
        final Notification notification = mNotificationBuilder.buildMissed(mContext,
                mNotificationModel, missed);
        final int notificationId = mNotificationModel.getMissedTimerNotificationId();
        mNotificationManager.notify(notificationId, notification);
@@ -787,23 +787,11 @@ final class TimerModel {
        }

        // Otherwise build and post a foreground notification reflecting the latest expired timers.
        final Notification notification = getNotificationBuilder().buildHeadsUp(mContext, expired);
        final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired);
        final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
        mService.startForeground(notificationId, notification);
    }

    private NotificationBuilder getNotificationBuilder() {
        if (mNotificationBuilder == null) {
            if (Utils.isNOrLater()) {
                mNotificationBuilder = new TimerNotificationBuilderN();
            } else {
                mNotificationBuilder = new TimerNotificationBuilderPreN();
            }
        }

        return mNotificationBuilder;
    }

    /**
     * Update the stopwatch notification in response to a locale change.
     */
@@ -840,35 +828,4 @@ final class TimerModel {
            am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
        }
    }

    /**
     * An API for building platform-specific timer notifications.
     */
    public interface NotificationBuilder {

        int REQUEST_CODE_UPCOMING = 0;
        int REQUEST_CODE_MISSING = 1;

        /**
         * @param context a context to use for fetching resources
         * @param nm from which notification data are fetched
         * @param unexpiredTimers all running and paused timers
         * @return a notification reporting the state of the {@code unexpiredTimers}
         */
        Notification build(Context context, NotificationModel nm, 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);

        /**
         * @param context a context to use for fetching resources
         * @param missedTimers all missed timers
         * @return a heads-up notification reporting the state of the {@code missedTimers}
         */
        Notification buildMissed(Context context, NotificationModel nm, List<Timer> missedTimers);
    }
}
+137 −74
Original line number Diff line number Diff line
@@ -17,18 +17,21 @@
package com.android.deskclock.data;

import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.SystemClock;
import android.support.annotation.DrawableRes;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.NotificationCompat;
import android.text.TextUtils;
import android.widget.RemoteViews;

import com.android.deskclock.AlarmUtils;
import com.android.deskclock.R;
import com.android.deskclock.Utils;
import com.android.deskclock.events.Events;
@@ -38,15 +41,19 @@ import com.android.deskclock.timer.TimerService;
import java.util.ArrayList;
import java.util.List;

import static android.support.v4.app.NotificationCompat.Action;
import static android.support.v4.app.NotificationCompat.Builder;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;

/**
 * Builds N-style notification to reflect the latest state of the unexpired timers.
 * Builds notifications to reflect the latest state of the timers.
 */
@TargetApi(Build.VERSION_CODES.N)
class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
class TimerNotificationBuilder {

    private static final int REQUEST_CODE_UPCOMING = 0;
    private static final int REQUEST_CODE_MISSING = 1;

    @Override
    public Notification build(Context context, NotificationModel nm, List<Timer> unexpired) {
        final Timer timer = unexpired.get(0);
        final int count = unexpired.size();
@@ -57,11 +64,8 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {

        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 List<Action> actions = new ArrayList<>(2);

        final CharSequence stateText;
        if (count == 1) {
@@ -78,20 +82,20 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
                        .setAction(TimerService.ACTION_PAUSE_TIMER)
                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());

                final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_pause_24dp);
                @DrawableRes final int icon1 = R.drawable.ic_pause_24dp;
                final CharSequence title1 = res.getText(R.string.timer_pause);
                final PendingIntent intent1 = Utils.pendingServiceIntent(context, pause);
                actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
                actions.add(new Action.Builder(icon1, title1, intent1).build());

                // Right Button: +1 Minute
                final Intent addMinute = new Intent(context, TimerService.class)
                        .setAction(TimerService.ACTION_ADD_MINUTE_TIMER)
                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());

                final Icon icon2 = Icon.createWithResource(context, R.drawable.ic_add_24dp);
                @DrawableRes final int icon2 = R.drawable.ic_add_24dp;
                final CharSequence title2 = res.getText(R.string.timer_plus_1_min);
                final PendingIntent intent2 = Utils.pendingServiceIntent(context, addMinute);
                actions.add(new Notification.Action.Builder(icon2, title2, intent2).build());
                actions.add(new Action.Builder(icon2, title2, intent2).build());

            } else {
                // Single timer is paused.
@@ -102,20 +106,20 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
                        .setAction(TimerService.ACTION_START_TIMER)
                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());

                final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_start_24dp);
                @DrawableRes final int icon1 = R.drawable.ic_start_24dp;
                final CharSequence title1 = res.getText(R.string.sw_resume_button);
                final PendingIntent intent1 = Utils.pendingServiceIntent(context, start);
                actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
                actions.add(new Action.Builder(icon1, title1, intent1).build());

                // Right Button: Reset
                final Intent reset = new Intent(context, TimerService.class)
                        .setAction(TimerService.ACTION_RESET_TIMER)
                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());

                final Icon icon2 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
                @DrawableRes final int icon2 = R.drawable.ic_reset_24dp;
                final CharSequence title2 = res.getText(R.string.sw_reset_button);
                final PendingIntent intent2 = Utils.pendingServiceIntent(context, reset);
                actions.add(new Notification.Action.Builder(icon2, title2, intent2).build());
                actions.add(new Action.Builder(icon2, title2, intent2).build());
            }
        } else {
            if (running) {
@@ -128,14 +132,12 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {

            final Intent reset = TimerService.createResetUnexpiredTimersIntent(context);

            final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
            @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
            final CharSequence title1 = res.getText(R.string.timer_reset_all);
            final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
            actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
            actions.add(new 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, TimerService.class)
                .setAction(TimerService.ACTION_SHOW_TIMER)
@@ -146,38 +148,80 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
                PendingIntent.getService(context, REQUEST_CODE_UPCOMING, showApp,
                        PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);

        return new Notification.Builder(context)
        final Builder notification = new NotificationCompat.Builder(context)
                .setOngoing(true)
                .setLocalOnly(true)
                .setShowWhen(false)
                .setAutoCancel(false)
                .setCustomContentView(content)
                .setContentIntent(pendingShowApp)
                .setPriority(Notification.PRIORITY_HIGH)
                .setCategory(Notification.CATEGORY_ALARM)
                .setCategory(NotificationCompat.CATEGORY_ALARM)
                .setSmallIcon(R.drawable.stat_notify_timer)
                .setGroup(nm.getTimerNotificationGroupKey())
                .setSortKey(nm.getTimerNotificationSortKey())
                .setVisibility(Notification.VISIBILITY_PUBLIC)
                .setStyle(new Notification.DecoratedCustomViewStyle())
                .setActions(actions.toArray(new Notification.Action[actions.size()]))
                .setColor(ContextCompat.getColor(context, R.color.default_background))
                .build();
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
                .setColor(ContextCompat.getColor(context, R.color.default_background));

        for (Action action : actions) {
            notification.addAction(action);
        }

        if (Utils.isNOrLater()) {
            notification.setCustomContentView(buildChronometer(pname, base, running, stateText))
                    .setGroup(nm.getTimerNotificationGroupKey());
        } else {
            final CharSequence contentTextPreN;
            if (count == 1) {
                contentTextPreN = TimerStringFormatter.formatTimeRemaining(context,
                        timer.getRemainingTime(), false);
            } else if (running) {
                final String timeRemaining = TimerStringFormatter.formatTimeRemaining(context,
                        timer.getRemainingTime(), false);
                contentTextPreN = context.getString(R.string.next_timer_notif, timeRemaining);
            } else {
                contentTextPreN = context.getString(R.string.all_timers_stopped_notif);
            }

            notification.setContentTitle(stateText).setContentText(contentTextPreN);

            final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            final Intent updateNotification = TimerService.createUpdateNotificationIntent(context);
            final long remainingTime = timer.getRemainingTime();
            if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) {
                // Schedule a callback to update the time-sensitive information of the running timer
                final PendingIntent pi =
                        PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification,
                                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);

                final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS;
                final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange;
                TimerModel.schedulePendingIntent(am, triggerTime, pi);
            } else {
                // Cancel the update notification callback.
                final PendingIntent pi = PendingIntent.getService(context, 0, updateNotification,
                        PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
                if (pi != null) {
                    am.cancel(pi);
                    pi.cancel();
                }
            }
        }

    @Override
    public Notification buildHeadsUp(Context context, List<Timer> expired) {
        return notification.build();
    }

    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);
        @DrawableRes final int icon1 = 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);
        final List<Action> actions = new ArrayList<>(2);
        if (count == 1) {
            final String label = timer.getLabel();
            if (TextUtils.isEmpty(label)) {
@@ -188,30 +232,25 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {

            // Left button: Reset single timer
            final CharSequence title1 = context.getString(R.string.timer_stop);
            actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
            actions.add(new 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);
            @DrawableRes final int icon2 = 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());

            actions.add(new 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());
            actions.add(new 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);
@@ -222,39 +261,47 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
        final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(context, fullScreen);

        return new Notification.Builder(context)
        final Builder notification = new NotificationCompat.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()]))
                .setColor(ContextCompat.getColor(context, R.color.default_background))
                .build();
                .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
                .setColor(ContextCompat.getColor(context, R.color.default_background));

        for (Action action : actions) {
            notification.addAction(action);
        }

        if (Utils.isNOrLater()) {
            notification.setCustomContentView(buildChronometer(pname, base, true, stateText));
        } else {
            final CharSequence contentTextPreN = count == 1
                    ? context.getString(R.string.timer_times_up)
                    : context.getString(R.string.timer_multi_times_up, count);

            notification.setContentTitle(stateText).setContentText(contentTextPreN);
        }

        return notification.build();
    }

    @Override
    public Notification buildMissed(Context context, NotificationModel nm,
    Notification buildMissed(Context context, NotificationModel nm,
            List<Timer> missedTimers) {
        final Timer timer = missedTimers.get(0);
        final int count = missedTimers.size();

        // Compute some values required below.
        final long base = getChronometerBase(timer);
        final String pName = context.getPackageName();
        final String pname = context.getPackageName();
        final Resources res = context.getResources();

        final RemoteViews content = new RemoteViews(pName, R.layout.chronometer_notif_content);
        content.setChronometerCountDown(R.id.chronometer, true);
        content.setChronometer(R.id.chronometer, base, null, true);

        final List<Notification.Action> actions = new ArrayList<>(1);
        final Action action;

        final CharSequence stateText;
        if (count == 1) {
@@ -271,24 +318,22 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
                    .setAction(TimerService.ACTION_RESET_TIMER)
                    .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());

            final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
            @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
            final CharSequence title1 = res.getText(R.string.timer_reset);
            final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
            actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
            action = new Action.Builder(icon1, title1, intent1).build();
        } else {
            // Multiple missed timers.
            stateText = res.getString(R.string.timer_multi_missed, count);

            final Intent reset = TimerService.createResetMissedTimersIntent(context);

            final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
            @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
            final CharSequence title1 = res.getText(R.string.timer_reset_all);
            final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
            actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
            action = new 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, TimerService.class)
                .setAction(TimerService.ACTION_SHOW_TIMER)
@@ -299,22 +344,30 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
                PendingIntent.getService(context, REQUEST_CODE_MISSING, showApp,
                        PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);

        return new Notification.Builder(context)
        final Builder notification = new NotificationCompat.Builder(context)
                .setLocalOnly(true)
                .setShowWhen(false)
                .setAutoCancel(false)
                .setCustomContentView(content)
                .setContentIntent(pendingShowApp)
                .setPriority(Notification.PRIORITY_HIGH)
                .setCategory(Notification.CATEGORY_ALARM)
                .setCategory(NotificationCompat.CATEGORY_ALARM)
                .setSmallIcon(R.drawable.stat_notify_timer)
                .setGroup(nm.getTimerNotificationGroupKey())
                .setVisibility(Notification.VISIBILITY_PUBLIC)
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                .setSortKey(nm.getTimerNotificationMissedSortKey())
                .setStyle(new Notification.DecoratedCustomViewStyle())
                .setActions(actions.toArray(new Notification.Action[actions.size()]))
                .setColor(ContextCompat.getColor(context, R.color.default_background))
                .build();
                .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
                .addAction(action)
                .setColor(ContextCompat.getColor(context, R.color.default_background));

        if (Utils.isNOrLater()) {
            notification.setCustomContentView(buildChronometer(pname, base, true, stateText))
                    .setGroup(nm.getTimerNotificationGroupKey());
        } else {
            final CharSequence contentText = AlarmUtils.getFormattedTime(context,
                    timer.getWallClockExpirationTime());
            notification.setContentText(contentText).setContentTitle(stateText);
        }

        return notification.build();
    }

    /**
@@ -330,4 +383,14 @@ class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
        // Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now.
        return SystemClock.elapsedRealtime() + adjustedRemaining;
    }

    @TargetApi(Build.VERSION_CODES.N)
    private RemoteViews buildChronometer(String pname, long base, boolean running,
            CharSequence stateText) {
        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);
        content.setTextViewText(R.id.state, stateText);
        return content;
    }
}