Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2018 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.settingslib.notification;
     18 
     19 import android.app.ActivityManager;
     20 import android.app.AlarmManager;
     21 import android.app.AlertDialog;
     22 import android.app.Dialog;
     23 import android.app.NotificationManager;
     24 import android.content.Context;
     25 import android.content.DialogInterface;
     26 import android.net.Uri;
     27 import android.provider.Settings;
     28 import android.service.notification.Condition;
     29 import android.service.notification.ZenModeConfig;
     30 import android.text.TextUtils;
     31 import android.text.format.DateFormat;
     32 import android.util.Log;
     33 import android.util.Slog;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.widget.CompoundButton;
     37 import android.widget.ImageView;
     38 import android.widget.LinearLayout;
     39 import android.widget.RadioButton;
     40 import android.widget.RadioGroup;
     41 import android.widget.ScrollView;
     42 import android.widget.TextView;
     43 
     44 import com.android.internal.annotations.VisibleForTesting;
     45 import com.android.internal.logging.MetricsLogger;
     46 import com.android.internal.logging.nano.MetricsProto;
     47 import com.android.internal.policy.PhoneWindow;
     48 import com.android.settingslib.R;
     49 
     50 import java.util.Arrays;
     51 import java.util.Calendar;
     52 import java.util.GregorianCalendar;
     53 import java.util.Locale;
     54 import java.util.Objects;
     55 
     56 public class EnableZenModeDialog {
     57     private static final String TAG = "EnableZenModeDialog";
     58     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     59 
     60     private static final int[] MINUTE_BUCKETS = ZenModeConfig.MINUTE_BUCKETS;
     61     private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
     62     private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
     63     private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
     64 
     65     @VisibleForTesting
     66     protected static final int FOREVER_CONDITION_INDEX = 0;
     67     @VisibleForTesting
     68     protected static final int COUNTDOWN_CONDITION_INDEX = 1;
     69     @VisibleForTesting
     70     protected static final int COUNTDOWN_ALARM_CONDITION_INDEX = 2;
     71 
     72     private static final int SECONDS_MS = 1000;
     73     private static final int MINUTES_MS = 60 * SECONDS_MS;
     74 
     75     @VisibleForTesting
     76     protected Uri mForeverId;
     77     private int mBucketIndex = -1;
     78 
     79     @VisibleForTesting
     80     protected NotificationManager mNotificationManager;
     81     private AlarmManager mAlarmManager;
     82     private int mUserId;
     83     private boolean mAttached;
     84 
     85     @VisibleForTesting
     86     protected Context mContext;
     87     @VisibleForTesting
     88     protected TextView mZenAlarmWarning;
     89     @VisibleForTesting
     90     protected LinearLayout mZenRadioGroupContent;
     91 
     92     private RadioGroup mZenRadioGroup;
     93     private int MAX_MANUAL_DND_OPTIONS = 3;
     94 
     95     @VisibleForTesting
     96     protected LayoutInflater mLayoutInflater;
     97 
     98     public EnableZenModeDialog(Context context) {
     99         mContext = context;
    100     }
    101 
    102     public Dialog createDialog() {
    103         mNotificationManager = (NotificationManager) mContext.
    104                 getSystemService(Context.NOTIFICATION_SERVICE);
    105         mForeverId =  Condition.newId(mContext).appendPath("forever").build();
    106         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    107         mUserId = mContext.getUserId();
    108         mAttached = false;
    109 
    110         final AlertDialog.Builder builder = new AlertDialog.Builder(mContext)
    111                 .setTitle(R.string.zen_mode_settings_turn_on_dialog_title)
    112                 .setNegativeButton(R.string.cancel, null)
    113                 .setPositiveButton(R.string.zen_mode_enable_dialog_turn_on,
    114                         new DialogInterface.OnClickListener() {
    115                             @Override
    116                             public void onClick(DialogInterface dialog, int which) {
    117                                 int checkedId = mZenRadioGroup.getCheckedRadioButtonId();
    118                                 ConditionTag tag = getConditionTagAt(checkedId);
    119 
    120                                 if (isForever(tag.condition)) {
    121                                     MetricsLogger.action(mContext,
    122                                             MetricsProto.MetricsEvent.
    123                                                     NOTIFICATION_ZEN_MODE_TOGGLE_ON_FOREVER);
    124                                 } else if (isAlarm(tag.condition)) {
    125                                     MetricsLogger.action(mContext,
    126                                             MetricsProto.MetricsEvent.
    127                                                     NOTIFICATION_ZEN_MODE_TOGGLE_ON_ALARM);
    128                                 } else if (isCountdown(tag.condition)) {
    129                                     MetricsLogger.action(mContext,
    130                                             MetricsProto.MetricsEvent.
    131                                                     NOTIFICATION_ZEN_MODE_TOGGLE_ON_COUNTDOWN);
    132                                 } else {
    133                                     Slog.d(TAG, "Invalid manual condition: " + tag.condition);
    134                                 }
    135                                 // always triggers priority-only dnd with chosen condition
    136                                 mNotificationManager.setZenMode(
    137                                         Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
    138                                         getRealConditionId(tag.condition), TAG);
    139                             }
    140                         });
    141 
    142         View contentView = getContentView();
    143         bindConditions(forever());
    144         builder.setView(contentView);
    145         return builder.create();
    146     }
    147 
    148     private void hideAllConditions() {
    149         final int N = mZenRadioGroupContent.getChildCount();
    150         for (int i = 0; i < N; i++) {
    151             mZenRadioGroupContent.getChildAt(i).setVisibility(View.GONE);
    152         }
    153 
    154         mZenAlarmWarning.setVisibility(View.GONE);
    155     }
    156 
    157     protected View getContentView() {
    158         if (mLayoutInflater == null) {
    159             mLayoutInflater = new PhoneWindow(mContext).getLayoutInflater();
    160         }
    161         View contentView = mLayoutInflater.inflate(R.layout.zen_mode_turn_on_dialog_container,
    162                 null);
    163         ScrollView container = (ScrollView) contentView.findViewById(R.id.container);
    164 
    165         mZenRadioGroup = container.findViewById(R.id.zen_radio_buttons);
    166         mZenRadioGroupContent = container.findViewById(R.id.zen_radio_buttons_content);
    167         mZenAlarmWarning = container.findViewById(R.id.zen_alarm_warning);
    168 
    169         for (int i = 0; i < MAX_MANUAL_DND_OPTIONS; i++) {
    170             final View radioButton = mLayoutInflater.inflate(R.layout.zen_mode_radio_button,
    171                     mZenRadioGroup, false);
    172             mZenRadioGroup.addView(radioButton);
    173             radioButton.setId(i);
    174 
    175             final View radioButtonContent = mLayoutInflater.inflate(R.layout.zen_mode_condition,
    176                     mZenRadioGroupContent, false);
    177             radioButtonContent.setId(i + MAX_MANUAL_DND_OPTIONS);
    178             mZenRadioGroupContent.addView(radioButtonContent);
    179         }
    180 
    181         hideAllConditions();
    182         return contentView;
    183     }
    184 
    185     @VisibleForTesting
    186     protected void bind(final Condition condition, final View row, final int rowId) {
    187         if (condition == null) throw new IllegalArgumentException("condition must not be null");
    188 
    189         final boolean enabled = condition.state == Condition.STATE_TRUE;
    190         final ConditionTag tag = row.getTag() != null ? (ConditionTag) row.getTag() :
    191                 new ConditionTag();
    192         row.setTag(tag);
    193         final boolean first = tag.rb == null;
    194         if (tag.rb == null) {
    195             tag.rb = (RadioButton) mZenRadioGroup.getChildAt(rowId);
    196         }
    197         tag.condition = condition;
    198         final Uri conditionId = getConditionId(tag.condition);
    199         if (DEBUG) Log.d(TAG, "bind i=" + mZenRadioGroupContent.indexOfChild(row) + " first="
    200                 + first + " condition=" + conditionId);
    201         tag.rb.setEnabled(enabled);
    202         tag.rb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    203             @Override
    204             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    205                 if (isChecked) {
    206                     tag.rb.setChecked(true);
    207                     if (DEBUG) Log.d(TAG, "onCheckedChanged " + conditionId);
    208                     MetricsLogger.action(mContext,
    209                             MetricsProto.MetricsEvent.QS_DND_CONDITION_SELECT);
    210                     updateAlarmWarningText(tag.condition);
    211                 }
    212             }
    213         });
    214 
    215         updateUi(tag, row, condition, enabled, rowId, conditionId);
    216         row.setVisibility(View.VISIBLE);
    217     }
    218 
    219     @VisibleForTesting
    220     protected ConditionTag getConditionTagAt(int index) {
    221         return (ConditionTag) mZenRadioGroupContent.getChildAt(index).getTag();
    222     }
    223 
    224     @VisibleForTesting
    225     protected void bindConditions(Condition c) {
    226         // forever
    227         bind(forever(), mZenRadioGroupContent.getChildAt(FOREVER_CONDITION_INDEX),
    228                 FOREVER_CONDITION_INDEX);
    229         if (c == null) {
    230             bindGenericCountdown();
    231             bindNextAlarm(getTimeUntilNextAlarmCondition());
    232         } else if (isForever(c)) {
    233             getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true);
    234             bindGenericCountdown();
    235             bindNextAlarm(getTimeUntilNextAlarmCondition());
    236         } else {
    237             if (isAlarm(c)) {
    238                 bindGenericCountdown();
    239                 bindNextAlarm(c);
    240                 getConditionTagAt(COUNTDOWN_ALARM_CONDITION_INDEX).rb.setChecked(true);
    241             } else if (isCountdown(c)) {
    242                 bindNextAlarm(getTimeUntilNextAlarmCondition());
    243                 bind(c, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
    244                         COUNTDOWN_CONDITION_INDEX);
    245                 getConditionTagAt(COUNTDOWN_CONDITION_INDEX).rb.setChecked(true);
    246             } else {
    247                 Slog.d(TAG, "Invalid manual condition: " + c);
    248             }
    249         }
    250     }
    251 
    252     public static Uri getConditionId(Condition condition) {
    253         return condition != null ? condition.id : null;
    254     }
    255 
    256     public Condition forever() {
    257         Uri foreverId = Condition.newId(mContext).appendPath("forever").build();
    258         return new Condition(foreverId, foreverSummary(mContext), "", "", 0 /*icon*/,
    259                 Condition.STATE_TRUE, 0 /*flags*/);
    260     }
    261 
    262     public long getNextAlarm() {
    263         final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(mUserId);
    264         return info != null ? info.getTriggerTime() : 0;
    265     }
    266 
    267     @VisibleForTesting
    268     protected boolean isAlarm(Condition c) {
    269         return c != null && ZenModeConfig.isValidCountdownToAlarmConditionId(c.id);
    270     }
    271 
    272     @VisibleForTesting
    273     protected boolean isCountdown(Condition c) {
    274         return c != null && ZenModeConfig.isValidCountdownConditionId(c.id);
    275     }
    276 
    277     private boolean isForever(Condition c) {
    278         return c != null && mForeverId.equals(c.id);
    279     }
    280 
    281     private Uri getRealConditionId(Condition condition) {
    282         return isForever(condition) ? null : getConditionId(condition);
    283     }
    284 
    285     private String foreverSummary(Context context) {
    286         return context.getString(com.android.internal.R.string.zen_mode_forever);
    287     }
    288 
    289     private static void setToMidnight(Calendar calendar) {
    290         calendar.set(Calendar.HOUR_OF_DAY, 0);
    291         calendar.set(Calendar.MINUTE, 0);
    292         calendar.set(Calendar.SECOND, 0);
    293         calendar.set(Calendar.MILLISECOND, 0);
    294     }
    295 
    296     // Returns a time condition if the next alarm is within the next week.
    297     @VisibleForTesting
    298     protected Condition getTimeUntilNextAlarmCondition() {
    299         GregorianCalendar weekRange = new GregorianCalendar();
    300         setToMidnight(weekRange);
    301         weekRange.add(Calendar.DATE, 6);
    302         final long nextAlarmMs = getNextAlarm();
    303         if (nextAlarmMs > 0) {
    304             GregorianCalendar nextAlarm = new GregorianCalendar();
    305             nextAlarm.setTimeInMillis(nextAlarmMs);
    306             setToMidnight(nextAlarm);
    307 
    308             if (weekRange.compareTo(nextAlarm) >= 0) {
    309                 return ZenModeConfig.toNextAlarmCondition(mContext, nextAlarmMs,
    310                         ActivityManager.getCurrentUser());
    311             }
    312         }
    313         return null;
    314     }
    315 
    316     @VisibleForTesting
    317     protected void bindGenericCountdown() {
    318         mBucketIndex = DEFAULT_BUCKET_INDEX;
    319         Condition countdown = ZenModeConfig.toTimeCondition(mContext,
    320                 MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
    321         if (!mAttached || getConditionTagAt(COUNTDOWN_CONDITION_INDEX).condition == null) {
    322             bind(countdown, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
    323                     COUNTDOWN_CONDITION_INDEX);
    324         }
    325     }
    326 
    327     private void updateUi(ConditionTag tag, View row, Condition condition,
    328             boolean enabled, int rowId, Uri conditionId) {
    329         if (tag.lines == null) {
    330             tag.lines = row.findViewById(android.R.id.content);
    331             tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
    332         }
    333         if (tag.line1 == null) {
    334             tag.line1 = (TextView) row.findViewById(android.R.id.text1);
    335         }
    336 
    337         if (tag.line2 == null) {
    338             tag.line2 = (TextView) row.findViewById(android.R.id.text2);
    339         }
    340 
    341         final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1
    342                 : condition.summary;
    343         final String line2 = condition.line2;
    344         tag.line1.setText(line1);
    345         if (TextUtils.isEmpty(line2)) {
    346             tag.line2.setVisibility(View.GONE);
    347         } else {
    348             tag.line2.setVisibility(View.VISIBLE);
    349             tag.line2.setText(line2);
    350         }
    351         tag.lines.setEnabled(enabled);
    352         tag.lines.setAlpha(enabled ? 1 : .4f);
    353 
    354         tag.lines.setOnClickListener(new View.OnClickListener() {
    355             @Override
    356             public void onClick(View v) {
    357                 tag.rb.setChecked(true);
    358             }
    359         });
    360 
    361         // minus button
    362         final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
    363         button1.setOnClickListener(new View.OnClickListener() {
    364             @Override
    365             public void onClick(View v) {
    366                 onClickTimeButton(row, tag, false /*down*/, rowId);
    367             }
    368         });
    369 
    370         // plus button
    371         final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
    372         button2.setOnClickListener(new View.OnClickListener() {
    373             @Override
    374             public void onClick(View v) {
    375                 onClickTimeButton(row, tag, true /*up*/, rowId);
    376             }
    377         });
    378 
    379         final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
    380         if (rowId == COUNTDOWN_CONDITION_INDEX && time > 0) {
    381             button1.setVisibility(View.VISIBLE);
    382             button2.setVisibility(View.VISIBLE);
    383             if (mBucketIndex > -1) {
    384                 button1.setEnabled(mBucketIndex > 0);
    385                 button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
    386             } else {
    387                 final long span = time - System.currentTimeMillis();
    388                 button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
    389                 final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
    390                         MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser());
    391                 button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
    392             }
    393 
    394             button1.setAlpha(button1.isEnabled() ? 1f : .5f);
    395             button2.setAlpha(button2.isEnabled() ? 1f : .5f);
    396         } else {
    397             button1.setVisibility(View.GONE);
    398             button2.setVisibility(View.GONE);
    399         }
    400     }
    401 
    402     @VisibleForTesting
    403     protected void bindNextAlarm(Condition c) {
    404         View alarmContent = mZenRadioGroupContent.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX);
    405         ConditionTag tag = (ConditionTag) alarmContent.getTag();
    406 
    407         if (c != null && (!mAttached || tag == null || tag.condition == null)) {
    408             bind(c, alarmContent, COUNTDOWN_ALARM_CONDITION_INDEX);
    409         }
    410 
    411         // hide the alarm radio button if there isn't a "next alarm condition"
    412         tag = (ConditionTag) alarmContent.getTag();
    413         boolean showAlarm = tag != null && tag.condition != null;
    414         mZenRadioGroup.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX).setVisibility(
    415                 showAlarm ? View.VISIBLE : View.GONE);
    416         alarmContent.setVisibility(showAlarm ? View.VISIBLE : View.GONE);
    417     }
    418 
    419     private void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) {
    420         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.QS_DND_TIME, up);
    421         Condition newCondition = null;
    422         final int N = MINUTE_BUCKETS.length;
    423         if (mBucketIndex == -1) {
    424             // not on a known index, search for the next or prev bucket by time
    425             final Uri conditionId = getConditionId(tag.condition);
    426             final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
    427             final long now = System.currentTimeMillis();
    428             for (int i = 0; i < N; i++) {
    429                 int j = up ? i : N - 1 - i;
    430                 final int bucketMinutes = MINUTE_BUCKETS[j];
    431                 final long bucketTime = now + bucketMinutes * MINUTES_MS;
    432                 if (up && bucketTime > time || !up && bucketTime < time) {
    433                     mBucketIndex = j;
    434                     newCondition = ZenModeConfig.toTimeCondition(mContext,
    435                             bucketTime, bucketMinutes, ActivityManager.getCurrentUser(),
    436                             false /*shortVersion*/);
    437                     break;
    438                 }
    439             }
    440             if (newCondition == null) {
    441                 mBucketIndex = DEFAULT_BUCKET_INDEX;
    442                 newCondition = ZenModeConfig.toTimeCondition(mContext,
    443                         MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
    444             }
    445         } else {
    446             // on a known index, simply increment or decrement
    447             mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
    448             newCondition = ZenModeConfig.toTimeCondition(mContext,
    449                     MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
    450         }
    451         bind(newCondition, row, rowId);
    452         updateAlarmWarningText(tag.condition);
    453         tag.rb.setChecked(true);
    454     }
    455 
    456     private void updateAlarmWarningText(Condition condition) {
    457         String warningText = computeAlarmWarningText(condition);
    458         mZenAlarmWarning.setText(warningText);
    459         mZenAlarmWarning.setVisibility(warningText == null ? View.GONE : View.VISIBLE);
    460     }
    461 
    462     @VisibleForTesting
    463     protected String computeAlarmWarningText(Condition condition) {
    464         boolean allowAlarms = (mNotificationManager.getNotificationPolicy().priorityCategories
    465                 & NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS) != 0;
    466 
    467         // don't show alarm warning if alarms are allowed to bypass dnd
    468         if (allowAlarms) {
    469             return null;
    470         }
    471 
    472         final long now = System.currentTimeMillis();
    473         final long nextAlarm = getNextAlarm();
    474         if (nextAlarm < now) {
    475             return null;
    476         }
    477         int warningRes = 0;
    478         if (condition == null || isForever(condition)) {
    479             warningRes = R.string.zen_alarm_warning_indef;
    480         } else {
    481             final long time = ZenModeConfig.tryParseCountdownConditionId(condition.id);
    482             if (time > now && nextAlarm < time) {
    483                 warningRes = R.string.zen_alarm_warning;
    484             }
    485         }
    486         if (warningRes == 0) {
    487             return null;
    488         }
    489 
    490         return mContext.getResources().getString(warningRes, getTime(nextAlarm, now));
    491     }
    492 
    493     @VisibleForTesting
    494     protected String getTime(long nextAlarm, long now) {
    495         final boolean soon = (nextAlarm - now) < 24 * 60 * 60 * 1000;
    496         final boolean is24 = DateFormat.is24HourFormat(mContext, ActivityManager.getCurrentUser());
    497         final String skeleton = soon ? (is24 ? "Hm" : "hma") : (is24 ? "EEEHm" : "EEEhma");
    498         final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
    499         final CharSequence formattedTime = DateFormat.format(pattern, nextAlarm);
    500         final int templateRes = soon ? R.string.alarm_template : R.string.alarm_template_far;
    501         return mContext.getResources().getString(templateRes, formattedTime);
    502     }
    503 
    504     // used as the view tag on condition rows
    505     @VisibleForTesting
    506     protected static class ConditionTag {
    507         public RadioButton rb;
    508         public View lines;
    509         public TextView line1;
    510         public TextView line2;
    511         public Condition condition;
    512     }
    513 }
    514