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