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