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