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