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