Home | History | Annotate | Download | only in volume
      1 /**
      2  * Copyright (C) 2014 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.systemui.volume;
     18 
     19 import android.animation.LayoutTransition;
     20 import android.animation.LayoutTransition.TransitionListener;
     21 import android.app.ActivityManager;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.SharedPreferences;
     25 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
     26 import android.content.res.Configuration;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.os.Handler;
     30 import android.os.Looper;
     31 import android.os.Message;
     32 import android.provider.Settings;
     33 import android.provider.Settings.Global;
     34 import android.service.notification.Condition;
     35 import android.service.notification.ZenModeConfig;
     36 import android.service.notification.ZenModeConfig.ZenRule;
     37 import android.text.TextUtils;
     38 import android.text.format.DateFormat;
     39 import android.text.format.DateUtils;
     40 import android.util.ArraySet;
     41 import android.util.AttributeSet;
     42 import android.util.Log;
     43 import android.util.MathUtils;
     44 import android.util.Slog;
     45 import android.view.LayoutInflater;
     46 import android.view.View;
     47 import android.view.ViewGroup;
     48 import android.widget.CompoundButton;
     49 import android.widget.CompoundButton.OnCheckedChangeListener;
     50 import android.widget.FrameLayout;
     51 import android.widget.ImageView;
     52 import android.widget.LinearLayout;
     53 import android.widget.RadioButton;
     54 import android.widget.RadioGroup;
     55 import android.widget.TextView;
     56 
     57 import com.android.internal.annotations.VisibleForTesting;
     58 import com.android.internal.logging.MetricsLogger;
     59 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     60 import com.android.systemui.Prefs;
     61 import com.android.systemui.R;
     62 import com.android.systemui.statusbar.policy.ZenModeController;
     63 
     64 import java.io.FileDescriptor;
     65 import java.io.PrintWriter;
     66 import java.util.Arrays;
     67 import java.util.Calendar;
     68 import java.util.GregorianCalendar;
     69 import java.util.Locale;
     70 import java.util.Objects;
     71 
     72 public class ZenModePanel extends FrameLayout {
     73     private static final String TAG = "ZenModePanel";
     74     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     75 
     76     public static final int STATE_MODIFY = 0;
     77     public static final int STATE_AUTO_RULE = 1;
     78     public static final int STATE_OFF = 2;
     79 
     80     private static final int SECONDS_MS = 1000;
     81     private static final int MINUTES_MS = 60 * SECONDS_MS;
     82 
     83     private static final int[] MINUTE_BUCKETS = ZenModeConfig.MINUTE_BUCKETS;
     84     private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
     85     private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
     86     private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
     87     private static final int FOREVER_CONDITION_INDEX = 0;
     88     private static final int COUNTDOWN_CONDITION_INDEX = 1;
     89     private static final int COUNTDOWN_ALARM_CONDITION_INDEX = 2;
     90     private static final int COUNTDOWN_CONDITION_COUNT = 2;
     91 
     92     public static final Intent ZEN_SETTINGS
     93             = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS);
     94     public static final Intent ZEN_PRIORITY_SETTINGS
     95             = new Intent(Settings.ACTION_ZEN_MODE_PRIORITY_SETTINGS);
     96 
     97     private static final long TRANSITION_DURATION = 300;
     98 
     99     private final Context mContext;
    100     protected final LayoutInflater mInflater;
    101     private final H mHandler = new H();
    102     private final ZenPrefs mPrefs;
    103     private final TransitionHelper mTransitionHelper = new TransitionHelper();
    104     private final Uri mForeverId;
    105     private final ConfigurableTexts mConfigurableTexts;
    106 
    107     private String mTag = TAG + "/" + Integer.toHexString(System.identityHashCode(this));
    108 
    109     protected SegmentedButtons mZenButtons;
    110     private View mZenIntroduction;
    111     private TextView mZenIntroductionMessage;
    112     private View mZenIntroductionConfirm;
    113     private TextView mZenIntroductionCustomize;
    114     protected LinearLayout mZenConditions;
    115     private TextView mZenAlarmWarning;
    116     private RadioGroup mZenRadioGroup;
    117     private LinearLayout mZenRadioGroupContent;
    118 
    119     private Callback mCallback;
    120     private ZenModeController mController;
    121     private Condition mExitCondition;
    122     private int mBucketIndex = -1;
    123     private boolean mExpanded;
    124     private boolean mHidden;
    125     private int mSessionZen;
    126     private int mAttachedZen;
    127     private boolean mAttached;
    128     private Condition mSessionExitCondition;
    129     private boolean mVoiceCapable;
    130 
    131     protected int mZenModeConditionLayoutId;
    132     protected int mZenModeButtonLayoutId;
    133     private View mEmpty;
    134     private TextView mEmptyText;
    135     private ImageView mEmptyIcon;
    136     private View mAutoRule;
    137     private TextView mAutoTitle;
    138     private int mState = STATE_MODIFY;
    139     private ViewGroup mEdit;
    140 
    141     public ZenModePanel(Context context, AttributeSet attrs) {
    142         super(context, attrs);
    143         mContext = context;
    144         mPrefs = new ZenPrefs();
    145         mInflater = LayoutInflater.from(mContext);
    146         mForeverId = Condition.newId(mContext).appendPath("forever").build();
    147         mConfigurableTexts = new ConfigurableTexts(mContext);
    148         mVoiceCapable = Util.isVoiceCapable(mContext);
    149         mZenModeConditionLayoutId = R.layout.zen_mode_condition;
    150         mZenModeButtonLayoutId = R.layout.zen_mode_button;
    151         if (DEBUG) Log.d(mTag, "new ZenModePanel");
    152     }
    153 
    154     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    155         pw.println("ZenModePanel state:");
    156         pw.print("  mAttached="); pw.println(mAttached);
    157         pw.print("  mHidden="); pw.println(mHidden);
    158         pw.print("  mExpanded="); pw.println(mExpanded);
    159         pw.print("  mSessionZen="); pw.println(mSessionZen);
    160         pw.print("  mAttachedZen="); pw.println(mAttachedZen);
    161         pw.print("  mConfirmedPriorityIntroduction=");
    162         pw.println(mPrefs.mConfirmedPriorityIntroduction);
    163         pw.print("  mConfirmedSilenceIntroduction=");
    164         pw.println(mPrefs.mConfirmedSilenceIntroduction);
    165         pw.print("  mVoiceCapable="); pw.println(mVoiceCapable);
    166         mTransitionHelper.dump(fd, pw, args);
    167     }
    168 
    169     protected void createZenButtons() {
    170         mZenButtons = findViewById(R.id.zen_buttons);
    171         mZenButtons.addButton(R.string.interruption_level_none_twoline,
    172                 R.string.interruption_level_none_with_warning,
    173                 Global.ZEN_MODE_NO_INTERRUPTIONS);
    174         mZenButtons.addButton(R.string.interruption_level_alarms_twoline,
    175                 R.string.interruption_level_alarms,
    176                 Global.ZEN_MODE_ALARMS);
    177         mZenButtons.addButton(R.string.interruption_level_priority_twoline,
    178                 R.string.interruption_level_priority,
    179                 Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
    180         mZenButtons.setCallback(mZenButtonsCallback);
    181     }
    182 
    183     @Override
    184     protected void onFinishInflate() {
    185         super.onFinishInflate();
    186         createZenButtons();
    187         mZenIntroduction = findViewById(R.id.zen_introduction);
    188         mZenIntroductionMessage = findViewById(R.id.zen_introduction_message);
    189         mZenIntroductionConfirm = findViewById(R.id.zen_introduction_confirm);
    190         mZenIntroductionConfirm.setOnClickListener(v -> confirmZenIntroduction());
    191         mZenIntroductionCustomize = findViewById(R.id.zen_introduction_customize);
    192         mZenIntroductionCustomize.setOnClickListener(v -> {
    193             confirmZenIntroduction();
    194             if (mCallback != null) {
    195                 mCallback.onPrioritySettings();
    196             }
    197         });
    198         mConfigurableTexts.add(mZenIntroductionCustomize, R.string.zen_priority_customize_button);
    199 
    200         mZenConditions = findViewById(R.id.zen_conditions);
    201         mZenAlarmWarning = findViewById(R.id.zen_alarm_warning);
    202         mZenRadioGroup = findViewById(R.id.zen_radio_buttons);
    203         mZenRadioGroupContent = findViewById(R.id.zen_radio_buttons_content);
    204 
    205         mEdit = findViewById(R.id.edit_container);
    206 
    207         mEmpty = findViewById(android.R.id.empty);
    208         mEmpty.setVisibility(INVISIBLE);
    209         mEmptyText = mEmpty.findViewById(android.R.id.title);
    210         mEmptyIcon = mEmpty.findViewById(android.R.id.icon);
    211 
    212         mAutoRule = findViewById(R.id.auto_rule);
    213         mAutoTitle = mAutoRule.findViewById(android.R.id.title);
    214         mAutoRule.setVisibility(INVISIBLE);
    215     }
    216 
    217     public void setEmptyState(int icon, int text) {
    218         mEmptyIcon.post(() -> {
    219             mEmptyIcon.setImageResource(icon);
    220             mEmptyText.setText(text);
    221         });
    222     }
    223 
    224     public void setAutoText(CharSequence text) {
    225         mAutoTitle.post(() -> mAutoTitle.setText(text));
    226     }
    227 
    228     public void setState(int state) {
    229         if (mState == state) return;
    230         transitionFrom(getView(mState), getView(state));
    231         mState = state;
    232     }
    233 
    234     private void transitionFrom(View from, View to) {
    235         from.post(() -> {
    236             // TODO: Better transitions
    237             to.setAlpha(0);
    238             to.setVisibility(VISIBLE);
    239             to.bringToFront();
    240             to.animate().cancel();
    241             to.animate().alpha(1)
    242                     .setDuration(TRANSITION_DURATION)
    243                     .withEndAction(() -> from.setVisibility(INVISIBLE))
    244                     .start();
    245         });
    246     }
    247 
    248     private View getView(int state) {
    249         switch (state) {
    250             case STATE_AUTO_RULE:
    251                 return mAutoRule;
    252             case STATE_OFF:
    253                 return mEmpty;
    254             default:
    255                 return mEdit;
    256         }
    257     }
    258 
    259     @Override
    260     protected void onConfigurationChanged(Configuration newConfig) {
    261         super.onConfigurationChanged(newConfig);
    262         mConfigurableTexts.update();
    263         if (mZenButtons != null) {
    264             mZenButtons.update();
    265         }
    266     }
    267 
    268     private void confirmZenIntroduction() {
    269         final String prefKey = prefKeyForConfirmation(getSelectedZen(Global.ZEN_MODE_OFF));
    270         if (prefKey == null) return;
    271         if (DEBUG) Log.d(TAG, "confirmZenIntroduction " + prefKey);
    272         Prefs.putBoolean(mContext, prefKey, true);
    273         mHandler.sendEmptyMessage(H.UPDATE_WIDGETS);
    274     }
    275 
    276     private static String prefKeyForConfirmation(int zen) {
    277         switch (zen) {
    278             case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
    279                 return Prefs.Key.DND_CONFIRMED_PRIORITY_INTRODUCTION;
    280             case Global.ZEN_MODE_NO_INTERRUPTIONS:
    281                 return Prefs.Key.DND_CONFIRMED_SILENCE_INTRODUCTION;
    282             case Global.ZEN_MODE_ALARMS:
    283                 return Prefs.Key.DND_CONFIRMED_ALARM_INTRODUCTION;
    284             default:
    285                 return null;
    286         }
    287     }
    288 
    289     private void onAttach() {
    290         setExpanded(true);
    291         mAttachedZen = mController.getZen();
    292         ZenRule manualRule = mController.getManualRule();
    293         mExitCondition = manualRule != null ? manualRule.condition : null;
    294         if (DEBUG) Log.d(mTag, "onAttach " + mAttachedZen + " " + manualRule);
    295         handleUpdateManualRule(manualRule);
    296         mZenButtons.setSelectedValue(mAttachedZen, false);
    297         mSessionZen = mAttachedZen;
    298         mTransitionHelper.clear();
    299         mController.addCallback(mZenCallback);
    300         setSessionExitCondition(copy(mExitCondition));
    301         updateWidgets();
    302         setAttached(true);
    303     }
    304 
    305     private void onDetach() {
    306         if (DEBUG) Log.d(mTag, "onDetach");
    307         setExpanded(false);
    308         checkForAttachedZenChange();
    309         setAttached(false);
    310         mAttachedZen = -1;
    311         mSessionZen = -1;
    312         mController.removeCallback(mZenCallback);
    313         setSessionExitCondition(null);
    314         mTransitionHelper.clear();
    315     }
    316 
    317     @VisibleForTesting
    318     void setAttached(boolean attached) {
    319         mAttached = attached;
    320     }
    321 
    322     @Override
    323     public void onVisibilityAggregated(boolean isVisible) {
    324         super.onVisibilityAggregated(isVisible);
    325         if (isVisible == mAttached) return;
    326         if (isVisible) {
    327             onAttach();
    328         } else {
    329             onDetach();
    330         }
    331     }
    332 
    333     private void setSessionExitCondition(Condition condition) {
    334         if (Objects.equals(condition, mSessionExitCondition)) return;
    335         if (DEBUG) Log.d(mTag, "mSessionExitCondition=" + getConditionId(condition));
    336         mSessionExitCondition = condition;
    337     }
    338 
    339     public void setHidden(boolean hidden) {
    340         if (mHidden == hidden) return;
    341         if (DEBUG) Log.d(mTag, "hidden=" + hidden);
    342         mHidden = hidden;
    343         updateWidgets();
    344     }
    345 
    346     private void checkForAttachedZenChange() {
    347         final int selectedZen = getSelectedZen(-1);
    348         if (DEBUG) Log.d(mTag, "selectedZen=" + selectedZen);
    349         if (selectedZen != mAttachedZen) {
    350             if (DEBUG) Log.d(mTag, "attachedZen: " + mAttachedZen + " -> " + selectedZen);
    351             if (selectedZen == Global.ZEN_MODE_NO_INTERRUPTIONS) {
    352                 mPrefs.trackNoneSelected();
    353             }
    354         }
    355     }
    356 
    357     private void setExpanded(boolean expanded) {
    358         if (expanded == mExpanded) return;
    359         if (DEBUG) Log.d(mTag, "setExpanded " + expanded);
    360         mExpanded = expanded;
    361         updateWidgets();
    362         fireExpanded();
    363     }
    364 
    365     protected void addZenConditions(int count) {
    366         for (int i = 0; i < count; i++) {
    367             final View rb = mInflater.inflate(mZenModeButtonLayoutId, mEdit, false);
    368             rb.setId(i);
    369             mZenRadioGroup.addView(rb);
    370             final View rbc = mInflater.inflate(mZenModeConditionLayoutId, mEdit, false);
    371             rbc.setId(i + count);
    372             mZenRadioGroupContent.addView(rbc);
    373         }
    374     }
    375 
    376     public void init(ZenModeController controller) {
    377         mController = controller;
    378         final int minConditions = 1 /*forever*/ + COUNTDOWN_CONDITION_COUNT;
    379         addZenConditions(minConditions);
    380         mSessionZen = getSelectedZen(-1);
    381         handleUpdateManualRule(mController.getManualRule());
    382         if (DEBUG) Log.d(mTag, "init mExitCondition=" + mExitCondition);
    383         hideAllConditions();
    384     }
    385 
    386     private void setExitCondition(Condition exitCondition) {
    387         if (Objects.equals(mExitCondition, exitCondition)) return;
    388         mExitCondition = exitCondition;
    389         if (DEBUG) Log.d(mTag, "mExitCondition=" + getConditionId(mExitCondition));
    390         updateWidgets();
    391     }
    392 
    393     private static Uri getConditionId(Condition condition) {
    394         return condition != null ? condition.id : null;
    395     }
    396 
    397     private Uri getRealConditionId(Condition condition) {
    398         return isForever(condition) ? null : getConditionId(condition);
    399     }
    400 
    401     private static Condition copy(Condition condition) {
    402         return condition == null ? null : condition.copy();
    403     }
    404 
    405     public void setCallback(Callback callback) {
    406         mCallback = callback;
    407     }
    408 
    409     @VisibleForTesting
    410     void handleUpdateManualRule(ZenRule rule) {
    411         final int zen = rule != null ? rule.zenMode : Global.ZEN_MODE_OFF;
    412         handleUpdateZen(zen);
    413         final Condition c = rule == null ? null
    414                 : rule.condition != null ? rule.condition
    415                 : createCondition(rule.conditionId);
    416         handleUpdateConditions(c);
    417         setExitCondition(c);
    418     }
    419 
    420     private Condition createCondition(Uri conditionId) {
    421         if (ZenModeConfig.isValidCountdownToAlarmConditionId(conditionId)) {
    422             long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
    423             Condition c = ZenModeConfig.toNextAlarmCondition(
    424                     mContext, time, ActivityManager.getCurrentUser());
    425             return c;
    426         } else if (ZenModeConfig.isValidCountdownConditionId(conditionId)) {
    427             long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
    428             int mins = (int) ((time - System.currentTimeMillis() + DateUtils.MINUTE_IN_MILLIS / 2)
    429                     / DateUtils.MINUTE_IN_MILLIS);
    430             Condition c = ZenModeConfig.toTimeCondition(mContext, time, mins,
    431                     ActivityManager.getCurrentUser(), false);
    432             return c;
    433         }
    434         // If there is a manual rule, but it has no condition listed then it is forever.
    435         return forever();
    436     }
    437 
    438     private void handleUpdateZen(int zen) {
    439         if (mSessionZen != -1 && mSessionZen != zen) {
    440             mSessionZen = zen;
    441         }
    442         mZenButtons.setSelectedValue(zen, false /* fromClick */);
    443         updateWidgets();
    444     }
    445 
    446     @VisibleForTesting
    447     int getSelectedZen(int defValue) {
    448         final Object zen = mZenButtons.getSelectedValue();
    449         return zen != null ? (Integer) zen : defValue;
    450     }
    451 
    452     private void updateWidgets() {
    453         if (mTransitionHelper.isTransitioning()) {
    454             mTransitionHelper.pendingUpdateWidgets();
    455             return;
    456         }
    457         final int zen = getSelectedZen(Global.ZEN_MODE_OFF);
    458         final boolean zenImportant = zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
    459         final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
    460         final boolean zenAlarm = zen == Global.ZEN_MODE_ALARMS;
    461         final boolean introduction = (zenImportant && !mPrefs.mConfirmedPriorityIntroduction
    462                 || zenNone && !mPrefs.mConfirmedSilenceIntroduction
    463                 || zenAlarm && !mPrefs.mConfirmedAlarmIntroduction);
    464 
    465         mZenButtons.setVisibility(mHidden ? GONE : VISIBLE);
    466         mZenIntroduction.setVisibility(introduction ? VISIBLE : GONE);
    467         if (introduction) {
    468             int message = zenImportant
    469                     ? R.string.zen_priority_introduction
    470                     : zenAlarm
    471                             ? R.string.zen_alarms_introduction
    472                             : mVoiceCapable
    473                                     ? R.string.zen_silence_introduction_voice
    474                                     : R.string.zen_silence_introduction;
    475             mConfigurableTexts.add(mZenIntroductionMessage, message);
    476             mConfigurableTexts.update();
    477             mZenIntroductionCustomize.setVisibility(zenImportant ? VISIBLE : GONE);
    478         }
    479         final String warning = computeAlarmWarningText(zenNone);
    480         mZenAlarmWarning.setVisibility(warning != null ? VISIBLE : GONE);
    481         mZenAlarmWarning.setText(warning);
    482     }
    483 
    484     private String computeAlarmWarningText(boolean zenNone) {
    485         if (!zenNone) {
    486             return null;
    487         }
    488         final long now = System.currentTimeMillis();
    489         final long nextAlarm = mController.getNextAlarm();
    490         if (nextAlarm < now) {
    491             return null;
    492         }
    493         int warningRes = 0;
    494         if (mSessionExitCondition == null || isForever(mSessionExitCondition)) {
    495             warningRes = R.string.zen_alarm_warning_indef;
    496         } else {
    497             final long time = ZenModeConfig.tryParseCountdownConditionId(mSessionExitCondition.id);
    498             if (time > now && nextAlarm < time) {
    499                 warningRes = R.string.zen_alarm_warning;
    500             }
    501         }
    502         if (warningRes == 0) {
    503             return null;
    504         }
    505         final boolean soon = (nextAlarm - now) < 24 * 60 * 60 * 1000;
    506         final boolean is24 = DateFormat.is24HourFormat(mContext, ActivityManager.getCurrentUser());
    507         final String skeleton = soon ? (is24 ? "Hm" : "hma") : (is24 ? "EEEHm" : "EEEhma");
    508         final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
    509         final CharSequence formattedTime = DateFormat.format(pattern, nextAlarm);
    510         final int templateRes = soon ? R.string.alarm_template : R.string.alarm_template_far;
    511         final String template = getResources().getString(templateRes, formattedTime);
    512         return getResources().getString(warningRes, template);
    513     }
    514 
    515     @VisibleForTesting
    516     void handleUpdateConditions(Condition c) {
    517         if (mTransitionHelper.isTransitioning()) {
    518             return;
    519         }
    520         // forever
    521         bind(forever(), mZenRadioGroupContent.getChildAt(FOREVER_CONDITION_INDEX),
    522                 FOREVER_CONDITION_INDEX);
    523         if (c == null) {
    524             bindGenericCountdown();
    525             bindNextAlarm(getTimeUntilNextAlarmCondition());
    526         } else if (isForever(c)) {
    527 
    528             getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true);
    529             bindGenericCountdown();
    530             bindNextAlarm(getTimeUntilNextAlarmCondition());
    531         } else {
    532             if (isAlarm(c)) {
    533                 bindGenericCountdown();
    534                 bindNextAlarm(c);
    535                 getConditionTagAt(COUNTDOWN_ALARM_CONDITION_INDEX).rb.setChecked(true);
    536             } else if (isCountdown(c)) {
    537                 bindNextAlarm(getTimeUntilNextAlarmCondition());
    538                 bind(c, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
    539                         COUNTDOWN_CONDITION_INDEX);
    540                 getConditionTagAt(COUNTDOWN_CONDITION_INDEX).rb.setChecked(true);
    541             } else {
    542                 Slog.wtf(TAG, "Invalid manual condition: " + c);
    543             }
    544         }
    545         mZenConditions.setVisibility(mSessionZen != Global.ZEN_MODE_OFF ? View.VISIBLE : View.GONE);
    546     }
    547 
    548     private void bindGenericCountdown() {
    549         mBucketIndex = DEFAULT_BUCKET_INDEX;
    550         Condition countdown = ZenModeConfig.toTimeCondition(mContext,
    551                 MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
    552         // don't change the hour condition while the user is viewing the panel
    553         if (!mAttached || getConditionTagAt(COUNTDOWN_CONDITION_INDEX).condition == null) {
    554             bind(countdown, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
    555                     COUNTDOWN_CONDITION_INDEX);
    556         }
    557     }
    558 
    559     private void bindNextAlarm(Condition c) {
    560         View alarmContent = mZenRadioGroupContent.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX);
    561         ConditionTag tag = (ConditionTag) alarmContent.getTag();
    562         // Don't change the alarm condition while the user is viewing the panel
    563         if (c != null && (!mAttached || tag == null || tag.condition == null)) {
    564             bind(c, alarmContent, COUNTDOWN_ALARM_CONDITION_INDEX);
    565         }
    566 
    567         tag = (ConditionTag) alarmContent.getTag();
    568         boolean showAlarm = tag != null && tag.condition != null;
    569         mZenRadioGroup.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX).setVisibility(
    570                 showAlarm ? View.VISIBLE : View.INVISIBLE);
    571         alarmContent.setVisibility(showAlarm ? View.VISIBLE : View.INVISIBLE);
    572     }
    573 
    574     private Condition forever() {
    575         return new Condition(mForeverId, foreverSummary(mContext), "", "", 0 /*icon*/,
    576                 Condition.STATE_TRUE, 0 /*flags*/);
    577     }
    578 
    579     private static String foreverSummary(Context context) {
    580         return context.getString(com.android.internal.R.string.zen_mode_forever);
    581     }
    582 
    583     // Returns a time condition if the next alarm is within the next week.
    584     private Condition getTimeUntilNextAlarmCondition() {
    585         GregorianCalendar weekRange = new GregorianCalendar();
    586         setToMidnight(weekRange);
    587         weekRange.add(Calendar.DATE, 6);
    588         final long nextAlarmMs = mController.getNextAlarm();
    589         if (nextAlarmMs > 0) {
    590             GregorianCalendar nextAlarm = new GregorianCalendar();
    591             nextAlarm.setTimeInMillis(nextAlarmMs);
    592             setToMidnight(nextAlarm);
    593 
    594             if (weekRange.compareTo(nextAlarm) >= 0) {
    595                 return ZenModeConfig.toNextAlarmCondition(mContext, nextAlarmMs,
    596                         ActivityManager.getCurrentUser());
    597             }
    598         }
    599         return null;
    600     }
    601 
    602     private void setToMidnight(Calendar calendar) {
    603         calendar.set(Calendar.HOUR_OF_DAY, 0);
    604         calendar.set(Calendar.MINUTE, 0);
    605         calendar.set(Calendar.SECOND, 0);
    606         calendar.set(Calendar.MILLISECOND, 0);
    607     }
    608 
    609     @VisibleForTesting
    610     ConditionTag getConditionTagAt(int index) {
    611         return (ConditionTag) mZenRadioGroupContent.getChildAt(index).getTag();
    612     }
    613 
    614     @VisibleForTesting
    615     int getVisibleConditions() {
    616         int rt = 0;
    617         final int N = mZenRadioGroupContent.getChildCount();
    618         for (int i = 0; i < N; i++) {
    619             rt += mZenRadioGroupContent.getChildAt(i).getVisibility() == VISIBLE ? 1 : 0;
    620         }
    621         return rt;
    622     }
    623 
    624     private void hideAllConditions() {
    625         final int N = mZenRadioGroupContent.getChildCount();
    626         for (int i = 0; i < N; i++) {
    627             mZenRadioGroupContent.getChildAt(i).setVisibility(GONE);
    628         }
    629     }
    630 
    631     private static boolean isAlarm(Condition c) {
    632         return c != null && ZenModeConfig.isValidCountdownToAlarmConditionId(c.id);
    633     }
    634 
    635     private static boolean isCountdown(Condition c) {
    636         return c != null && ZenModeConfig.isValidCountdownConditionId(c.id);
    637     }
    638 
    639     private boolean isForever(Condition c) {
    640         return c != null && mForeverId.equals(c.id);
    641     }
    642 
    643     private void bind(final Condition condition, final View row, final int rowId) {
    644         if (condition == null) throw new IllegalArgumentException("condition must not be null");
    645         final boolean enabled = condition.state == Condition.STATE_TRUE;
    646         final ConditionTag tag =
    647                 row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag();
    648         row.setTag(tag);
    649         final boolean first = tag.rb == null;
    650         if (tag.rb == null) {
    651             tag.rb = (RadioButton) mZenRadioGroup.getChildAt(rowId);
    652         }
    653         tag.condition = condition;
    654         final Uri conditionId = getConditionId(tag.condition);
    655         if (DEBUG) Log.d(mTag, "bind i=" + mZenRadioGroupContent.indexOfChild(row) + " first="
    656                 + first + " condition=" + conditionId);
    657         tag.rb.setEnabled(enabled);
    658         tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    659             @Override
    660             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    661                 if (mExpanded && isChecked) {
    662                     tag.rb.setChecked(true);
    663                     if (DEBUG) Log.d(mTag, "onCheckedChanged " + conditionId);
    664                     MetricsLogger.action(mContext, MetricsEvent.QS_DND_CONDITION_SELECT);
    665                     select(tag.condition);
    666                     announceConditionSelection(tag);
    667                 }
    668             }
    669         });
    670 
    671         if (tag.lines == null) {
    672             tag.lines = row.findViewById(android.R.id.content);
    673         }
    674         if (tag.line1 == null) {
    675             tag.line1 = (TextView) row.findViewById(android.R.id.text1);
    676             mConfigurableTexts.add(tag.line1);
    677         }
    678         if (tag.line2 == null) {
    679             tag.line2 = (TextView) row.findViewById(android.R.id.text2);
    680             mConfigurableTexts.add(tag.line2);
    681         }
    682         final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1
    683                 : condition.summary;
    684         final String line2 = condition.line2;
    685         tag.line1.setText(line1);
    686         if (TextUtils.isEmpty(line2)) {
    687             tag.line2.setVisibility(GONE);
    688         } else {
    689             tag.line2.setVisibility(VISIBLE);
    690             tag.line2.setText(line2);
    691         }
    692         tag.lines.setEnabled(enabled);
    693         tag.lines.setAlpha(enabled ? 1 : .4f);
    694 
    695         final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
    696         button1.setOnClickListener(new OnClickListener() {
    697             @Override
    698             public void onClick(View v) {
    699                 onClickTimeButton(row, tag, false /*down*/, rowId);
    700             }
    701         });
    702 
    703         final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
    704         button2.setOnClickListener(new OnClickListener() {
    705             @Override
    706             public void onClick(View v) {
    707                 onClickTimeButton(row, tag, true /*up*/, rowId);
    708             }
    709         });
    710         tag.lines.setOnClickListener(new OnClickListener() {
    711             @Override
    712             public void onClick(View v) {
    713                 tag.rb.setChecked(true);
    714             }
    715         });
    716 
    717         final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
    718         if (rowId != COUNTDOWN_ALARM_CONDITION_INDEX && time > 0) {
    719             button1.setVisibility(VISIBLE);
    720             button2.setVisibility(VISIBLE);
    721             if (mBucketIndex > -1) {
    722                 button1.setEnabled(mBucketIndex > 0);
    723                 button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
    724             } else {
    725                 final long span = time - System.currentTimeMillis();
    726                 button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
    727                 final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
    728                         MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser());
    729                 button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
    730             }
    731 
    732             button1.setAlpha(button1.isEnabled() ? 1f : .5f);
    733             button2.setAlpha(button2.isEnabled() ? 1f : .5f);
    734         } else {
    735             button1.setVisibility(GONE);
    736             button2.setVisibility(GONE);
    737         }
    738         // wire up interaction callbacks for newly-added condition rows
    739         if (first) {
    740             Interaction.register(tag.rb, mInteractionCallback);
    741             Interaction.register(tag.lines, mInteractionCallback);
    742             Interaction.register(button1, mInteractionCallback);
    743             Interaction.register(button2, mInteractionCallback);
    744         }
    745         row.setVisibility(VISIBLE);
    746     }
    747 
    748     private void announceConditionSelection(ConditionTag tag) {
    749         final int zen = getSelectedZen(Global.ZEN_MODE_OFF);
    750         String modeText;
    751         switch(zen) {
    752             case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
    753                 modeText = mContext.getString(R.string.interruption_level_priority);
    754                 break;
    755             case Global.ZEN_MODE_NO_INTERRUPTIONS:
    756                 modeText = mContext.getString(R.string.interruption_level_none);
    757                 break;
    758             case Global.ZEN_MODE_ALARMS:
    759                 modeText = mContext.getString(R.string.interruption_level_alarms);
    760                 break;
    761             default:
    762                 return;
    763         }
    764         announceForAccessibility(mContext.getString(R.string.zen_mode_and_condition, modeText,
    765                 tag.line1.getText()));
    766     }
    767 
    768     private void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) {
    769         MetricsLogger.action(mContext, MetricsEvent.QS_DND_TIME, up);
    770         Condition newCondition = null;
    771         final int N = MINUTE_BUCKETS.length;
    772         if (mBucketIndex == -1) {
    773             // not on a known index, search for the next or prev bucket by time
    774             final Uri conditionId = getConditionId(tag.condition);
    775             final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
    776             final long now = System.currentTimeMillis();
    777             for (int i = 0; i < N; i++) {
    778                 int j = up ? i : N - 1 - i;
    779                 final int bucketMinutes = MINUTE_BUCKETS[j];
    780                 final long bucketTime = now + bucketMinutes * MINUTES_MS;
    781                 if (up && bucketTime > time || !up && bucketTime < time) {
    782                     mBucketIndex = j;
    783                     newCondition = ZenModeConfig.toTimeCondition(mContext,
    784                             bucketTime, bucketMinutes, ActivityManager.getCurrentUser(),
    785                             false /*shortVersion*/);
    786                     break;
    787                 }
    788             }
    789             if (newCondition == null) {
    790                 mBucketIndex = DEFAULT_BUCKET_INDEX;
    791                 newCondition = ZenModeConfig.toTimeCondition(mContext,
    792                         MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
    793             }
    794         } else {
    795             // on a known index, simply increment or decrement
    796             mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
    797             newCondition = ZenModeConfig.toTimeCondition(mContext,
    798                     MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
    799         }
    800         bind(newCondition, row, rowId);
    801         tag.rb.setChecked(true);
    802         select(newCondition);
    803         announceConditionSelection(tag);
    804     }
    805 
    806     private void select(final Condition condition) {
    807         if (DEBUG) Log.d(mTag, "select " + condition);
    808         if (mSessionZen == -1 || mSessionZen == Global.ZEN_MODE_OFF) {
    809             if (DEBUG) Log.d(mTag, "Ignoring condition selection outside of manual zen");
    810             return;
    811         }
    812         final Uri realConditionId = getRealConditionId(condition);
    813         if (mController != null) {
    814             AsyncTask.execute(new Runnable() {
    815                 @Override
    816                 public void run() {
    817                     mController.setZen(mSessionZen, realConditionId, TAG + ".selectCondition");
    818                 }
    819             });
    820         }
    821         setExitCondition(condition);
    822         if (realConditionId == null) {
    823             mPrefs.setMinuteIndex(-1);
    824         } else if ((isAlarm(condition) || isCountdown(condition)) && mBucketIndex != -1) {
    825             mPrefs.setMinuteIndex(mBucketIndex);
    826         }
    827         setSessionExitCondition(copy(condition));
    828     }
    829 
    830     private void fireInteraction() {
    831         if (mCallback != null) {
    832             mCallback.onInteraction();
    833         }
    834     }
    835 
    836     private void fireExpanded() {
    837         if (mCallback != null) {
    838             mCallback.onExpanded(mExpanded);
    839         }
    840     }
    841 
    842     private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() {
    843         @Override
    844         public void onManualRuleChanged(ZenRule rule) {
    845             mHandler.obtainMessage(H.MANUAL_RULE_CHANGED, rule).sendToTarget();
    846         }
    847     };
    848 
    849     private final class H extends Handler {
    850         private static final int MANUAL_RULE_CHANGED = 2;
    851         private static final int UPDATE_WIDGETS = 3;
    852 
    853         private H() {
    854             super(Looper.getMainLooper());
    855         }
    856 
    857         @Override
    858         public void handleMessage(Message msg) {
    859             switch (msg.what) {
    860                 case MANUAL_RULE_CHANGED: handleUpdateManualRule((ZenRule) msg.obj); break;
    861                 case UPDATE_WIDGETS: updateWidgets(); break;
    862             }
    863         }
    864     }
    865 
    866     public interface Callback {
    867         void onPrioritySettings();
    868         void onInteraction();
    869         void onExpanded(boolean expanded);
    870     }
    871 
    872     // used as the view tag on condition rows
    873     @VisibleForTesting
    874     static class ConditionTag {
    875         RadioButton rb;
    876         View lines;
    877         TextView line1;
    878         TextView line2;
    879         Condition condition;
    880     }
    881 
    882     private final class ZenPrefs implements OnSharedPreferenceChangeListener {
    883         private final int mNoneDangerousThreshold;
    884 
    885         private int mMinuteIndex;
    886         private int mNoneSelected;
    887         private boolean mConfirmedPriorityIntroduction;
    888         private boolean mConfirmedSilenceIntroduction;
    889         private boolean mConfirmedAlarmIntroduction;
    890 
    891         private ZenPrefs() {
    892             mNoneDangerousThreshold = mContext.getResources()
    893                     .getInteger(R.integer.zen_mode_alarm_warning_threshold);
    894             Prefs.registerListener(mContext, this);
    895             updateMinuteIndex();
    896             updateNoneSelected();
    897             updateConfirmedPriorityIntroduction();
    898             updateConfirmedSilenceIntroduction();
    899             updateConfirmedAlarmIntroduction();
    900         }
    901 
    902         public void trackNoneSelected() {
    903             mNoneSelected = clampNoneSelected(mNoneSelected + 1);
    904             if (DEBUG) Log.d(mTag, "Setting none selected: " + mNoneSelected + " threshold="
    905                     + mNoneDangerousThreshold);
    906             Prefs.putInt(mContext, Prefs.Key.DND_NONE_SELECTED, mNoneSelected);
    907         }
    908 
    909         public int getMinuteIndex() {
    910             return mMinuteIndex;
    911         }
    912 
    913         public void setMinuteIndex(int minuteIndex) {
    914             minuteIndex = clampIndex(minuteIndex);
    915             if (minuteIndex == mMinuteIndex) return;
    916             mMinuteIndex = clampIndex(minuteIndex);
    917             if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex);
    918             Prefs.putInt(mContext, Prefs.Key.DND_FAVORITE_BUCKET_INDEX, mMinuteIndex);
    919         }
    920 
    921         @Override
    922         public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    923             updateMinuteIndex();
    924             updateNoneSelected();
    925             updateConfirmedPriorityIntroduction();
    926             updateConfirmedSilenceIntroduction();
    927             updateConfirmedAlarmIntroduction();
    928         }
    929 
    930         private void updateMinuteIndex() {
    931             mMinuteIndex = clampIndex(Prefs.getInt(mContext,
    932                     Prefs.Key.DND_FAVORITE_BUCKET_INDEX, DEFAULT_BUCKET_INDEX));
    933             if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex);
    934         }
    935 
    936         private int clampIndex(int index) {
    937             return MathUtils.constrain(index, -1, MINUTE_BUCKETS.length - 1);
    938         }
    939 
    940         private void updateNoneSelected() {
    941             mNoneSelected = clampNoneSelected(Prefs.getInt(mContext,
    942                     Prefs.Key.DND_NONE_SELECTED, 0));
    943             if (DEBUG) Log.d(mTag, "None selected: " + mNoneSelected);
    944         }
    945 
    946         private int clampNoneSelected(int noneSelected) {
    947             return MathUtils.constrain(noneSelected, 0, Integer.MAX_VALUE);
    948         }
    949 
    950         private void updateConfirmedPriorityIntroduction() {
    951             final boolean confirmed =  Prefs.getBoolean(mContext,
    952                     Prefs.Key.DND_CONFIRMED_PRIORITY_INTRODUCTION, false);
    953             if (confirmed == mConfirmedPriorityIntroduction) return;
    954             mConfirmedPriorityIntroduction = confirmed;
    955             if (DEBUG) Log.d(mTag, "Confirmed priority introduction: "
    956                     + mConfirmedPriorityIntroduction);
    957         }
    958 
    959         private void updateConfirmedSilenceIntroduction() {
    960             final boolean confirmed =  Prefs.getBoolean(mContext,
    961                     Prefs.Key.DND_CONFIRMED_SILENCE_INTRODUCTION, false);
    962             if (confirmed == mConfirmedSilenceIntroduction) return;
    963             mConfirmedSilenceIntroduction = confirmed;
    964             if (DEBUG) Log.d(mTag, "Confirmed silence introduction: "
    965                     + mConfirmedSilenceIntroduction);
    966         }
    967 
    968         private void updateConfirmedAlarmIntroduction() {
    969             final boolean confirmed =  Prefs.getBoolean(mContext,
    970                     Prefs.Key.DND_CONFIRMED_ALARM_INTRODUCTION, false);
    971             if (confirmed == mConfirmedAlarmIntroduction) return;
    972             mConfirmedAlarmIntroduction = confirmed;
    973             if (DEBUG) Log.d(mTag, "Confirmed alarm introduction: "
    974                     + mConfirmedAlarmIntroduction);
    975         }
    976     }
    977 
    978     protected final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() {
    979         @Override
    980         public void onSelected(final Object value, boolean fromClick) {
    981             if (value != null && mZenButtons.isShown() && isAttachedToWindow()) {
    982                 final int zen = (Integer) value;
    983                 if (fromClick) {
    984                     MetricsLogger.action(mContext, MetricsEvent.QS_DND_ZEN_SELECT, zen);
    985                 }
    986                 if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + zen);
    987                 final Uri realConditionId = getRealConditionId(mSessionExitCondition);
    988                 AsyncTask.execute(new Runnable() {
    989                     @Override
    990                     public void run() {
    991                         mController.setZen(zen, realConditionId, TAG + ".selectZen");
    992                         if (zen != Global.ZEN_MODE_OFF) {
    993                             Prefs.putInt(mContext, Prefs.Key.DND_FAVORITE_ZEN, zen);
    994                         }
    995                     }
    996                 });
    997             }
    998         }
    999 
   1000         @Override
   1001         public void onInteraction() {
   1002             fireInteraction();
   1003         }
   1004     };
   1005 
   1006     private final Interaction.Callback mInteractionCallback = new Interaction.Callback() {
   1007         @Override
   1008         public void onInteraction() {
   1009             fireInteraction();
   1010         }
   1011     };
   1012 
   1013     private final class TransitionHelper implements TransitionListener, Runnable {
   1014         private final ArraySet<View> mTransitioningViews = new ArraySet<View>();
   1015 
   1016         private boolean mTransitioning;
   1017         private boolean mPendingUpdateWidgets;
   1018 
   1019         public void clear() {
   1020             mTransitioningViews.clear();
   1021             mPendingUpdateWidgets = false;
   1022         }
   1023 
   1024         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
   1025             pw.println("  TransitionHelper state:");
   1026             pw.print("    mPendingUpdateWidgets="); pw.println(mPendingUpdateWidgets);
   1027             pw.print("    mTransitioning="); pw.println(mTransitioning);
   1028             pw.print("    mTransitioningViews="); pw.println(mTransitioningViews);
   1029         }
   1030 
   1031         public void pendingUpdateWidgets() {
   1032             mPendingUpdateWidgets = true;
   1033         }
   1034 
   1035         public boolean isTransitioning() {
   1036             return !mTransitioningViews.isEmpty();
   1037         }
   1038 
   1039         @Override
   1040         public void startTransition(LayoutTransition transition,
   1041                 ViewGroup container, View view, int transitionType) {
   1042             mTransitioningViews.add(view);
   1043             updateTransitioning();
   1044         }
   1045 
   1046         @Override
   1047         public void endTransition(LayoutTransition transition,
   1048                 ViewGroup container, View view, int transitionType) {
   1049             mTransitioningViews.remove(view);
   1050             updateTransitioning();
   1051         }
   1052 
   1053         @Override
   1054         public void run() {
   1055             if (DEBUG) Log.d(mTag, "TransitionHelper run"
   1056                     + " mPendingUpdateWidgets=" + mPendingUpdateWidgets);
   1057             if (mPendingUpdateWidgets) {
   1058                 updateWidgets();
   1059             }
   1060             mPendingUpdateWidgets = false;
   1061         }
   1062 
   1063         private void updateTransitioning() {
   1064             final boolean transitioning = isTransitioning();
   1065             if (mTransitioning == transitioning) return;
   1066             mTransitioning = transitioning;
   1067             if (DEBUG) Log.d(mTag, "TransitionHelper mTransitioning=" + mTransitioning);
   1068             if (!mTransitioning) {
   1069                 if (mPendingUpdateWidgets) {
   1070                     mHandler.post(this);
   1071                 } else {
   1072                     mPendingUpdateWidgets = false;
   1073                 }
   1074             }
   1075         }
   1076     }
   1077 }
   1078