Home | History | Annotate | Download | only in data
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.deskclock.data;
     18 
     19 import android.annotation.TargetApi;
     20 import android.app.AlarmManager;
     21 import android.app.Notification;
     22 import android.app.PendingIntent;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.res.Resources;
     26 import android.os.Build;
     27 import android.os.SystemClock;
     28 import android.support.annotation.DrawableRes;
     29 import android.support.v4.app.NotificationCompat;
     30 import android.support.v4.content.ContextCompat;
     31 import android.text.TextUtils;
     32 import android.widget.RemoteViews;
     33 
     34 import com.android.deskclock.AlarmUtils;
     35 import com.android.deskclock.R;
     36 import com.android.deskclock.Utils;
     37 import com.android.deskclock.events.Events;
     38 import com.android.deskclock.timer.ExpiredTimersActivity;
     39 import com.android.deskclock.timer.TimerService;
     40 
     41 import java.util.ArrayList;
     42 import java.util.List;
     43 
     44 import static android.support.v4.app.NotificationCompat.Action;
     45 import static android.support.v4.app.NotificationCompat.Builder;
     46 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
     47 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
     48 
     49 /**
     50  * Builds notifications to reflect the latest state of the timers.
     51  */
     52 class TimerNotificationBuilder {
     53 
     54     private static final int REQUEST_CODE_UPCOMING = 0;
     55     private static final int REQUEST_CODE_MISSING = 1;
     56 
     57     public Notification build(Context context, NotificationModel nm, List<Timer> unexpired) {
     58         final Timer timer = unexpired.get(0);
     59         final int count = unexpired.size();
     60 
     61         // Compute some values required below.
     62         final boolean running = timer.isRunning();
     63         final Resources res = context.getResources();
     64 
     65         final long base = getChronometerBase(timer);
     66         final String pname = context.getPackageName();
     67 
     68         final List<Action> actions = new ArrayList<>(2);
     69 
     70         final CharSequence stateText;
     71         if (count == 1) {
     72             if (running) {
     73                 // Single timer is running.
     74                 if (TextUtils.isEmpty(timer.getLabel())) {
     75                     stateText = res.getString(R.string.timer_notification_label);
     76                 } else {
     77                     stateText = timer.getLabel();
     78                 }
     79 
     80                 // Left button: Pause
     81                 final Intent pause = new Intent(context, TimerService.class)
     82                         .setAction(TimerService.ACTION_PAUSE_TIMER)
     83                         .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
     84 
     85                 @DrawableRes final int icon1 = R.drawable.ic_pause_24dp;
     86                 final CharSequence title1 = res.getText(R.string.timer_pause);
     87                 final PendingIntent intent1 = Utils.pendingServiceIntent(context, pause);
     88                 actions.add(new Action.Builder(icon1, title1, intent1).build());
     89 
     90                 // Right Button: +1 Minute
     91                 final Intent addMinute = new Intent(context, TimerService.class)
     92                         .setAction(TimerService.ACTION_ADD_MINUTE_TIMER)
     93                         .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
     94 
     95                 @DrawableRes final int icon2 = R.drawable.ic_add_24dp;
     96                 final CharSequence title2 = res.getText(R.string.timer_plus_1_min);
     97                 final PendingIntent intent2 = Utils.pendingServiceIntent(context, addMinute);
     98                 actions.add(new Action.Builder(icon2, title2, intent2).build());
     99 
    100             } else {
    101                 // Single timer is paused.
    102                 stateText = res.getString(R.string.timer_paused);
    103 
    104                 // Left button: Start
    105                 final Intent start = new Intent(context, TimerService.class)
    106                         .setAction(TimerService.ACTION_START_TIMER)
    107                         .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
    108 
    109                 @DrawableRes final int icon1 = R.drawable.ic_start_24dp;
    110                 final CharSequence title1 = res.getText(R.string.sw_resume_button);
    111                 final PendingIntent intent1 = Utils.pendingServiceIntent(context, start);
    112                 actions.add(new Action.Builder(icon1, title1, intent1).build());
    113 
    114                 // Right Button: Reset
    115                 final Intent reset = new Intent(context, TimerService.class)
    116                         .setAction(TimerService.ACTION_RESET_TIMER)
    117                         .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
    118 
    119                 @DrawableRes final int icon2 = R.drawable.ic_reset_24dp;
    120                 final CharSequence title2 = res.getText(R.string.sw_reset_button);
    121                 final PendingIntent intent2 = Utils.pendingServiceIntent(context, reset);
    122                 actions.add(new Action.Builder(icon2, title2, intent2).build());
    123             }
    124         } else {
    125             if (running) {
    126                 // At least one timer is running.
    127                 stateText = res.getString(R.string.timers_in_use, count);
    128             } else {
    129                 // All timers are paused.
    130                 stateText = res.getString(R.string.timers_stopped, count);
    131             }
    132 
    133             final Intent reset = TimerService.createResetUnexpiredTimersIntent(context);
    134 
    135             @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
    136             final CharSequence title1 = res.getText(R.string.timer_reset_all);
    137             final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
    138             actions.add(new Action.Builder(icon1, title1, intent1).build());
    139         }
    140 
    141         // Intent to load the app and show the timer when the notification is tapped.
    142         final Intent showApp = new Intent(context, TimerService.class)
    143                 .setAction(TimerService.ACTION_SHOW_TIMER)
    144                 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId())
    145                 .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification);
    146 
    147         final PendingIntent pendingShowApp =
    148                 PendingIntent.getService(context, REQUEST_CODE_UPCOMING, showApp,
    149                         PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    150 
    151         final Builder notification = new NotificationCompat.Builder(context)
    152                 .setOngoing(true)
    153                 .setLocalOnly(true)
    154                 .setShowWhen(false)
    155                 .setAutoCancel(false)
    156                 .setContentIntent(pendingShowApp)
    157                 .setPriority(Notification.PRIORITY_HIGH)
    158                 .setCategory(NotificationCompat.CATEGORY_ALARM)
    159                 .setSmallIcon(R.drawable.stat_notify_timer)
    160                 .setSortKey(nm.getTimerNotificationSortKey())
    161                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
    162                 .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
    163                 .setColor(ContextCompat.getColor(context, R.color.default_background));
    164 
    165         for (Action action : actions) {
    166             notification.addAction(action);
    167         }
    168 
    169         if (Utils.isNOrLater()) {
    170             notification.setCustomContentView(buildChronometer(pname, base, running, stateText))
    171                     .setGroup(nm.getTimerNotificationGroupKey());
    172         } else {
    173             final CharSequence contentTextPreN;
    174             if (count == 1) {
    175                 contentTextPreN = TimerStringFormatter.formatTimeRemaining(context,
    176                         timer.getRemainingTime(), false);
    177             } else if (running) {
    178                 final String timeRemaining = TimerStringFormatter.formatTimeRemaining(context,
    179                         timer.getRemainingTime(), false);
    180                 contentTextPreN = context.getString(R.string.next_timer_notif, timeRemaining);
    181             } else {
    182                 contentTextPreN = context.getString(R.string.all_timers_stopped_notif);
    183             }
    184 
    185             notification.setContentTitle(stateText).setContentText(contentTextPreN);
    186 
    187             final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    188             final Intent updateNotification = TimerService.createUpdateNotificationIntent(context);
    189             final long remainingTime = timer.getRemainingTime();
    190             if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) {
    191                 // Schedule a callback to update the time-sensitive information of the running timer
    192                 final PendingIntent pi =
    193                         PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification,
    194                                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    195 
    196                 final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS;
    197                 final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange;
    198                 TimerModel.schedulePendingIntent(am, triggerTime, pi);
    199             } else {
    200                 // Cancel the update notification callback.
    201                 final PendingIntent pi = PendingIntent.getService(context, 0, updateNotification,
    202                         PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
    203                 if (pi != null) {
    204                     am.cancel(pi);
    205                     pi.cancel();
    206                 }
    207             }
    208         }
    209 
    210         return notification.build();
    211     }
    212 
    213     Notification buildHeadsUp(Context context, List<Timer> expired) {
    214         final Timer timer = expired.get(0);
    215 
    216         // First action intent is to reset all timers.
    217         @DrawableRes final int icon1 = R.drawable.ic_stop_24dp;
    218         final Intent reset = TimerService.createResetExpiredTimersIntent(context);
    219         final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
    220 
    221         // Generate some descriptive text, a title, and an action name based on the timer count.
    222         final CharSequence stateText;
    223         final int count = expired.size();
    224         final List<Action> actions = new ArrayList<>(2);
    225         if (count == 1) {
    226             final String label = timer.getLabel();
    227             if (TextUtils.isEmpty(label)) {
    228                 stateText = context.getString(R.string.timer_times_up);
    229             } else {
    230                 stateText = label;
    231             }
    232 
    233             // Left button: Reset single timer
    234             final CharSequence title1 = context.getString(R.string.timer_stop);
    235             actions.add(new Action.Builder(icon1, title1, intent1).build());
    236 
    237             // Right button: Add minute
    238             final Intent addTime = TimerService.createAddMinuteTimerIntent(context, timer.getId());
    239             final PendingIntent intent2 = Utils.pendingServiceIntent(context, addTime);
    240             @DrawableRes final int icon2 = R.drawable.ic_add_24dp;
    241             final CharSequence title2 = context.getString(R.string.timer_plus_1_min);
    242             actions.add(new Action.Builder(icon2, title2, intent2).build());
    243         } else {
    244             stateText = context.getString(R.string.timer_multi_times_up, count);
    245 
    246             // Left button: Reset all timers
    247             final CharSequence title1 = context.getString(R.string.timer_stop_all);
    248             actions.add(new Action.Builder(icon1, title1, intent1).build());
    249         }
    250 
    251         final long base = getChronometerBase(timer);
    252 
    253         final String pname = context.getPackageName();
    254 
    255         // Content intent shows the timer full screen when clicked.
    256         final Intent content = new Intent(context, ExpiredTimersActivity.class);
    257         final PendingIntent contentIntent = Utils.pendingActivityIntent(context, content);
    258 
    259         // Full screen intent has flags so it is different than the content intent.
    260         final Intent fullScreen = new Intent(context, ExpiredTimersActivity.class)
    261                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
    262         final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(context, fullScreen);
    263 
    264         final Builder notification = new NotificationCompat.Builder(context)
    265                 .setOngoing(true)
    266                 .setLocalOnly(true)
    267                 .setShowWhen(false)
    268                 .setAutoCancel(false)
    269                 .setContentIntent(contentIntent)
    270                 .setPriority(Notification.PRIORITY_MAX)
    271                 .setDefaults(Notification.DEFAULT_LIGHTS)
    272                 .setSmallIcon(R.drawable.stat_notify_timer)
    273                 .setFullScreenIntent(pendingFullScreen, true)
    274                 .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
    275                 .setColor(ContextCompat.getColor(context, R.color.default_background));
    276 
    277         for (Action action : actions) {
    278             notification.addAction(action);
    279         }
    280 
    281         if (Utils.isNOrLater()) {
    282             notification.setCustomContentView(buildChronometer(pname, base, true, stateText));
    283         } else {
    284             final CharSequence contentTextPreN = count == 1
    285                     ? context.getString(R.string.timer_times_up)
    286                     : context.getString(R.string.timer_multi_times_up, count);
    287 
    288             notification.setContentTitle(stateText).setContentText(contentTextPreN);
    289         }
    290 
    291         return notification.build();
    292     }
    293 
    294     Notification buildMissed(Context context, NotificationModel nm,
    295             List<Timer> missedTimers) {
    296         final Timer timer = missedTimers.get(0);
    297         final int count = missedTimers.size();
    298 
    299         // Compute some values required below.
    300         final long base = getChronometerBase(timer);
    301         final String pname = context.getPackageName();
    302         final Resources res = context.getResources();
    303 
    304         final Action action;
    305 
    306         final CharSequence stateText;
    307         if (count == 1) {
    308             // Single timer is missed.
    309             if (TextUtils.isEmpty(timer.getLabel())) {
    310                 stateText = res.getString(R.string.missed_timer_notification_label);
    311             } else {
    312                 stateText = res.getString(R.string.missed_named_timer_notification_label,
    313                         timer.getLabel());
    314             }
    315 
    316             // Reset button
    317             final Intent reset = new Intent(context, TimerService.class)
    318                     .setAction(TimerService.ACTION_RESET_TIMER)
    319                     .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
    320 
    321             @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
    322             final CharSequence title1 = res.getText(R.string.timer_reset);
    323             final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
    324             action = new Action.Builder(icon1, title1, intent1).build();
    325         } else {
    326             // Multiple missed timers.
    327             stateText = res.getString(R.string.timer_multi_missed, count);
    328 
    329             final Intent reset = TimerService.createResetMissedTimersIntent(context);
    330 
    331             @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
    332             final CharSequence title1 = res.getText(R.string.timer_reset_all);
    333             final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
    334             action = new Action.Builder(icon1, title1, intent1).build();
    335         }
    336 
    337         // Intent to load the app and show the timer when the notification is tapped.
    338         final Intent showApp = new Intent(context, TimerService.class)
    339                 .setAction(TimerService.ACTION_SHOW_TIMER)
    340                 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId())
    341                 .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification);
    342 
    343         final PendingIntent pendingShowApp =
    344                 PendingIntent.getService(context, REQUEST_CODE_MISSING, showApp,
    345                         PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    346 
    347         final Builder notification = new NotificationCompat.Builder(context)
    348                 .setLocalOnly(true)
    349                 .setShowWhen(false)
    350                 .setAutoCancel(false)
    351                 .setContentIntent(pendingShowApp)
    352                 .setPriority(Notification.PRIORITY_HIGH)
    353                 .setCategory(NotificationCompat.CATEGORY_ALARM)
    354                 .setSmallIcon(R.drawable.stat_notify_timer)
    355                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
    356                 .setSortKey(nm.getTimerNotificationMissedSortKey())
    357                 .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
    358                 .addAction(action)
    359                 .setColor(ContextCompat.getColor(context, R.color.default_background));
    360 
    361         if (Utils.isNOrLater()) {
    362             notification.setCustomContentView(buildChronometer(pname, base, true, stateText))
    363                     .setGroup(nm.getTimerNotificationGroupKey());
    364         } else {
    365             final CharSequence contentText = AlarmUtils.getFormattedTime(context,
    366                     timer.getWallClockExpirationTime());
    367             notification.setContentText(contentText).setContentTitle(stateText);
    368         }
    369 
    370         return notification.build();
    371     }
    372 
    373     /**
    374      * @param timer the timer on which to base the chronometer display
    375      * @return the time at which the chronometer will/did reach 0:00 in realtime
    376      */
    377     private static long getChronometerBase(Timer timer) {
    378         // The in-app timer display rounds *up* to the next second for positive timer values. Mirror
    379         // that behavior in the notification's Chronometer by padding in an extra second as needed.
    380         final long remaining = timer.getRemainingTime();
    381         final long adjustedRemaining = remaining < 0 ? remaining : remaining + SECOND_IN_MILLIS;
    382 
    383         // Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now.
    384         return SystemClock.elapsedRealtime() + adjustedRemaining;
    385     }
    386 
    387     @TargetApi(Build.VERSION_CODES.N)
    388     private RemoteViews buildChronometer(String pname, long base, boolean running,
    389             CharSequence stateText) {
    390         final RemoteViews content = new RemoteViews(pname, R.layout.chronometer_notif_content);
    391         content.setChronometerCountDown(R.id.chronometer, true);
    392         content.setChronometer(R.id.chronometer, base, null, running);
    393         content.setTextViewText(R.id.state, stateText);
    394         return content;
    395     }
    396 }
    397