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