ZenModePanel.java revision bb4a702e6fe44cb026097db13492f8345b38ee97
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.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.content.Context;
22import android.content.Intent;
23import android.content.SharedPreferences;
24import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
25import android.content.res.Resources;
26import android.net.Uri;
27import android.os.Handler;
28import android.os.Looper;
29import android.os.Message;
30import android.provider.Settings;
31import android.provider.Settings.Global;
32import android.service.notification.Condition;
33import android.service.notification.ZenModeConfig;
34import android.text.TextUtils;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.util.MathUtils;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.animation.AnimationUtils;
41import android.view.animation.Interpolator;
42import android.widget.CompoundButton;
43import android.widget.CompoundButton.OnCheckedChangeListener;
44import android.widget.ImageView;
45import android.widget.LinearLayout;
46import android.widget.RadioButton;
47import android.widget.TextView;
48
49import com.android.systemui.R;
50import com.android.systemui.statusbar.policy.ZenModeController;
51
52import java.util.Arrays;
53import java.util.Objects;
54
55public class ZenModePanel extends LinearLayout {
56    private static final String TAG = "ZenModePanel";
57    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
58
59    private static final int SECONDS_MS = 1000;
60    private static final int MINUTES_MS = 60 * SECONDS_MS;
61
62    private static final int[] MINUTE_BUCKETS = DEBUG
63            ? new int[] { 0, 1, 2, 5, 15, 30, 45, 60, 120, 180, 240, 480 }
64            : ZenModeConfig.MINUTE_BUCKETS;
65    private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
66    private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
67    private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
68    private static final int FOREVER_CONDITION_INDEX = 0;
69    private static final int TIME_CONDITION_INDEX = 1;
70    private static final int FIRST_CONDITION_INDEX = 2;
71    private static final float SILENT_HINT_PULSE_SCALE = 1.1f;
72    private static final long SELECT_DEFAULT_DELAY = 300;
73
74    public static final Intent ZEN_SETTINGS = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS);
75
76    private final Context mContext;
77    private final LayoutInflater mInflater;
78    private final H mHandler = new H();
79    private final Prefs mPrefs;
80    private final Interpolator mFastOutSlowInInterpolator;
81    private final int mSubheadWarningColor;
82    private final int mSubheadColor;
83
84    private String mTag = TAG + "/" + Integer.toHexString(System.identityHashCode(this));
85
86    private SegmentedButtons mZenButtons;
87    private View mZenSubhead;
88    private TextView mZenSubheadCollapsed;
89    private TextView mZenSubheadExpanded;
90    private View mMoreSettings;
91    private LinearLayout mZenConditions;
92
93    private Callback mCallback;
94    private ZenModeController mController;
95    private boolean mRequestingConditions;
96    private Condition mExitCondition;
97    private String mExitConditionText;
98    private int mBucketIndex = -1;
99    private boolean mExpanded;
100    private boolean mHidden = false;
101    private int mSessionZen;
102    private int mAttachedZen;
103    private Condition mSessionExitCondition;
104    private Condition[] mConditions;
105    private Condition mTimeCondition;
106
107    public ZenModePanel(Context context, AttributeSet attrs) {
108        super(context, attrs);
109        mContext = context;
110        mPrefs = new Prefs();
111        mInflater = LayoutInflater.from(mContext.getApplicationContext());
112        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext,
113                android.R.interpolator.fast_out_slow_in);
114        final Resources res = mContext.getResources();
115        mSubheadWarningColor = res.getColor(R.color.system_warning_color);
116        mSubheadColor = res.getColor(R.color.qs_subhead);
117        if (DEBUG) Log.d(mTag, "new ZenModePanel");
118    }
119
120    @Override
121    protected void onFinishInflate() {
122        super.onFinishInflate();
123
124        mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons);
125        mZenButtons.addButton(R.string.interruption_level_none, R.drawable.ic_zen_none,
126                Global.ZEN_MODE_NO_INTERRUPTIONS);
127        mZenButtons.addButton(R.string.interruption_level_priority, R.drawable.ic_zen_important,
128                Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
129        mZenButtons.addButton(R.string.interruption_level_all, R.drawable.ic_zen_all,
130                Global.ZEN_MODE_OFF);
131        mZenButtons.setCallback(mZenButtonsCallback);
132
133        mZenSubhead = findViewById(R.id.zen_subhead);
134
135        mZenSubheadCollapsed = (TextView) findViewById(R.id.zen_subhead_collapsed);
136        mZenSubheadCollapsed.setOnClickListener(new View.OnClickListener() {
137            @Override
138            public void onClick(View v) {
139                setExpanded(true);
140            }
141        });
142        Interaction.register(mZenSubheadCollapsed, mInteractionCallback);
143
144        mZenSubheadExpanded = (TextView) findViewById(R.id.zen_subhead_expanded);
145        Interaction.register(mZenSubheadExpanded, mInteractionCallback);
146
147        mMoreSettings = findViewById(R.id.zen_more_settings);
148        mMoreSettings.setOnClickListener(new View.OnClickListener() {
149            @Override
150            public void onClick(View v) {
151                fireMoreSettings();
152            }
153        });
154        Interaction.register(mMoreSettings, mInteractionCallback);
155
156        mZenConditions = (LinearLayout) findViewById(R.id.zen_conditions);
157    }
158
159    @Override
160    protected void onAttachedToWindow() {
161        super.onAttachedToWindow();
162        if (DEBUG) Log.d(mTag, "onAttachedToWindow");
163        mAttachedZen = getSelectedZen(-1);
164        mSessionZen = mAttachedZen;
165        mSessionExitCondition = copy(mExitCondition);
166        refreshExitConditionText();
167        updateWidgets();
168    }
169
170    @Override
171    protected void onDetachedFromWindow() {
172        super.onDetachedFromWindow();
173        if (DEBUG) Log.d(mTag, "onDetachedFromWindow");
174        checkForAttachedZenChange();
175        mAttachedZen = -1;
176        mSessionZen = -1;
177        mSessionExitCondition = null;
178        setExpanded(false);
179    }
180
181    public void setHidden(boolean hidden) {
182        if (mHidden == hidden) return;
183        mHidden = hidden;
184        updateWidgets();
185    }
186
187    private void checkForAttachedZenChange() {
188        final int selectedZen = getSelectedZen(-1);
189        if (DEBUG) Log.d(mTag, "selectedZen=" + selectedZen);
190        if (selectedZen != mAttachedZen) {
191            if (DEBUG) Log.d(mTag, "attachedZen: " + mAttachedZen + " -> " + selectedZen);
192            if (selectedZen == Global.ZEN_MODE_NO_INTERRUPTIONS) {
193                mPrefs.trackNoneSelected();
194            }
195        }
196    }
197
198    private void setExpanded(boolean expanded) {
199        if (expanded == mExpanded) return;
200        mExpanded = expanded;
201        updateWidgets();
202        setRequestingConditions(mExpanded);
203        fireExpanded();
204    }
205
206    /** Start or stop requesting relevant zen mode exit conditions */
207    private void setRequestingConditions(boolean requesting) {
208        if (mRequestingConditions == requesting) return;
209        if (DEBUG) Log.d(mTag, "setRequestingConditions " + requesting);
210        mRequestingConditions = requesting;
211        if (mController != null) {
212            mController.requestConditions(mRequestingConditions);
213        }
214        if (mRequestingConditions) {
215            mTimeCondition = parseExistingTimeCondition(mExitCondition);
216            if (mTimeCondition != null) {
217                mBucketIndex = -1;
218            } else {
219                mBucketIndex = DEFAULT_BUCKET_INDEX;
220                mTimeCondition = ZenModeConfig.toTimeCondition(mContext,
221                        MINUTE_BUCKETS[mBucketIndex]);
222            }
223            if (DEBUG) Log.d(mTag, "Initial bucket index: " + mBucketIndex);
224            mConditions = null; // reset conditions
225            handleUpdateConditions();
226        } else {
227            mZenConditions.removeAllViews();
228        }
229    }
230
231    public void init(ZenModeController controller) {
232        mController = controller;
233        setExitCondition(mController.getExitCondition());
234        refreshExitConditionText();
235        mSessionZen = getSelectedZen(-1);
236        handleUpdateZen(mController.getZen());
237        if (DEBUG) Log.d(mTag, "init mExitCondition=" + mExitCondition);
238        mZenConditions.removeAllViews();
239        mController.addCallback(mZenCallback);
240    }
241
242    public void updateLocale() {
243        mZenButtons.updateLocale();
244    }
245
246    private void setExitCondition(Condition exitCondition) {
247        if (sameConditionId(mExitCondition, exitCondition)) return;
248        mExitCondition = exitCondition;
249        refreshExitConditionText();
250        updateWidgets();
251    }
252
253    private static Uri getConditionId(Condition condition) {
254        return condition != null ? condition.id : null;
255    }
256
257    private static boolean sameConditionId(Condition lhs, Condition rhs) {
258        return lhs == null ? rhs == null : rhs != null && lhs.id.equals(rhs.id);
259    }
260
261    private static Condition copy(Condition condition) {
262        return condition == null ? null : condition.copy();
263    }
264
265    private void refreshExitConditionText() {
266        final String forever = mContext.getString(com.android.internal.R.string.zen_mode_forever);
267        if (mExitCondition == null) {
268            mExitConditionText = forever;
269        } else if (ZenModeConfig.isValidCountdownConditionId(mExitCondition.id)) {
270            final Condition condition = parseExistingTimeCondition(mExitCondition);
271            mExitConditionText = condition != null ? condition.summary : forever;
272        } else {
273            mExitConditionText = mExitCondition.summary;
274        }
275    }
276
277    public void setCallback(Callback callback) {
278        mCallback = callback;
279    }
280
281    public void showSilentHint() {
282        if (DEBUG) Log.d(mTag, "showSilentHint");
283        if (mZenButtons == null || mZenButtons.getChildCount() == 0) return;
284        final View noneButton = mZenButtons.getChildAt(0);
285        if (noneButton.getScaleX() != 1) return;  // already running
286        noneButton.animate().cancel();
287        noneButton.animate().scaleX(SILENT_HINT_PULSE_SCALE).scaleY(SILENT_HINT_PULSE_SCALE)
288                .setInterpolator(mFastOutSlowInInterpolator)
289                .setListener(new AnimatorListenerAdapter() {
290                    @Override
291                    public void onAnimationEnd(Animator animation) {
292                        noneButton.animate().scaleX(1).scaleY(1).setListener(null);
293                    }
294                });
295    }
296
297    private void handleUpdateZen(int zen) {
298        if (mSessionZen != -1 && mSessionZen != zen) {
299            setExpanded(zen != Global.ZEN_MODE_OFF);
300            mSessionZen = zen;
301        }
302        mZenButtons.setSelectedValue(zen);
303        updateWidgets();
304    }
305
306    private int getSelectedZen(int defValue) {
307        final Object zen = mZenButtons.getSelectedValue();
308        return zen != null ? (Integer) zen : defValue;
309    }
310
311    private void updateWidgets() {
312        final int zen = getSelectedZen(Global.ZEN_MODE_OFF);
313        final boolean zenOff = zen == Global.ZEN_MODE_OFF;
314        final boolean zenImportant = zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
315        final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
316        final boolean expanded = !mHidden && mExpanded;
317
318        mZenButtons.setVisibility(mHidden ? GONE : VISIBLE);
319        mZenSubhead.setVisibility(!mHidden && !zenOff ? VISIBLE : GONE);
320        mZenSubheadExpanded.setVisibility(expanded ? VISIBLE : GONE);
321        mZenSubheadCollapsed.setVisibility(!expanded ? VISIBLE : GONE);
322        mMoreSettings.setVisibility(zenImportant && expanded ? VISIBLE : GONE);
323        mZenConditions.setVisibility(!zenOff && expanded ? VISIBLE : GONE);
324
325        if (zenNone) {
326            mZenSubheadExpanded.setText(R.string.zen_no_interruptions_with_warning);
327            mZenSubheadCollapsed.setText(mExitConditionText);
328        } else if (zenImportant) {
329            mZenSubheadExpanded.setText(R.string.zen_important_interruptions);
330            mZenSubheadCollapsed.setText(mExitConditionText);
331        }
332        mZenSubheadExpanded.setTextColor(zenNone && mPrefs.isNoneDangerous()
333                ? mSubheadWarningColor : mSubheadColor);
334    }
335
336    private Condition parseExistingTimeCondition(Condition condition) {
337        if (condition == null) return null;
338        final long time = ZenModeConfig.tryParseCountdownConditionId(condition.id);
339        if (time == 0) return null;
340        final long now = System.currentTimeMillis();
341        final long span = time - now;
342        if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null;
343        return ZenModeConfig.toTimeCondition(mContext,
344                time, Math.round(span / (float) MINUTES_MS), now);
345    }
346
347    private void handleUpdateConditions(Condition[] conditions) {
348        mConditions = conditions;
349        handleUpdateConditions();
350    }
351
352    private void handleUpdateConditions() {
353        final int conditionCount = mConditions == null ? 0 : mConditions.length;
354        if (DEBUG) Log.d(mTag, "handleUpdateConditions conditionCount=" + conditionCount);
355        for (int i = mZenConditions.getChildCount() - 1; i >= FIRST_CONDITION_INDEX; i--) {
356            mZenConditions.removeViewAt(i);
357        }
358        // forever
359        bind(null, mZenConditions.getChildAt(FOREVER_CONDITION_INDEX));
360        // countdown
361        bind(mTimeCondition, mZenConditions.getChildAt(TIME_CONDITION_INDEX));
362        // provider conditions
363        boolean foundDowntime = false;
364        for (int i = 0; i < conditionCount; i++) {
365            bind(mConditions[i], mZenConditions.getChildAt(FIRST_CONDITION_INDEX + i));
366            foundDowntime |= isDowntime(mConditions[i]);
367        }
368        // ensure downtime exists, if active
369        if (isDowntime(mSessionExitCondition) && !foundDowntime) {
370            bind(mSessionExitCondition, null);
371        }
372        // ensure something is selected, after waiting for providers to respond
373        mHandler.removeMessages(H.SELECT_DEFAULT);
374        mHandler.sendEmptyMessageDelayed(H.SELECT_DEFAULT, SELECT_DEFAULT_DELAY);
375    }
376
377    private static boolean isDowntime(Condition c) {
378        return ZenModeConfig.isValidDowntimeConditionId(getConditionId(c));
379    }
380
381    private ConditionTag getConditionTagAt(int index) {
382        return (ConditionTag) mZenConditions.getChildAt(index).getTag();
383    }
384
385    private void handleSelectDefault() {
386        if (!mExpanded) return;
387        // are we left without anything selected?  if so, set a default
388        for (int i = 0; i < mZenConditions.getChildCount(); i++) {
389            if (getConditionTagAt(i).rb.isChecked()) {
390                if (DEBUG) Log.d(mTag, "Not selecting a default, checked="
391                        + getConditionTagAt(i).condition);
392                return;
393            }
394        }
395        if (DEBUG) Log.d(mTag, "Selecting a default");
396        final int favoriteIndex = mPrefs.getMinuteIndex();
397        if (favoriteIndex == -1) {
398            getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true);
399        } else {
400            mTimeCondition = ZenModeConfig.toTimeCondition(mContext, MINUTE_BUCKETS[favoriteIndex]);
401            mBucketIndex = favoriteIndex;
402            bind(mTimeCondition, mZenConditions.getChildAt(TIME_CONDITION_INDEX));
403            getConditionTagAt(TIME_CONDITION_INDEX).rb.setChecked(true);
404        }
405    }
406
407    private void handleExitConditionChanged(Condition exitCondition) {
408        setExitCondition(exitCondition);
409        if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitCondition);
410        final int N = mZenConditions.getChildCount();
411        for (int i = 0; i < N; i++) {
412            final ConditionTag tag = getConditionTagAt(i);
413            tag.rb.setChecked(sameConditionId(tag.condition, mExitCondition));
414        }
415    }
416
417    private void bind(final Condition condition, View convertView) {
418        final boolean enabled = condition == null || condition.state == Condition.STATE_TRUE;
419        final View row;
420        if (convertView == null) {
421            row = mInflater.inflate(R.layout.zen_mode_condition, this, false);
422            if (DEBUG) Log.d(mTag, "Adding new condition view for: " + condition);
423            mZenConditions.addView(row);
424        } else {
425            row = convertView;
426        }
427        final ConditionTag tag =
428                row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag();
429        row.setTag(tag);
430        if (tag.rb == null) {
431            tag.rb = (RadioButton) row.findViewById(android.R.id.checkbox);
432        }
433        tag.condition = condition;
434        tag.rb.setEnabled(enabled);
435        if (mSessionExitCondition != null
436                && sameConditionId(mSessionExitCondition, tag.condition)) {
437            tag.rb.setChecked(true);
438        }
439        tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
440            @Override
441            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
442                if (mExpanded && isChecked) {
443                    if (DEBUG) Log.d(mTag, "onCheckedChanged " + tag.condition);
444                    final int N = mZenConditions.getChildCount();
445                    for (int i = 0; i < N; i++) {
446                        ConditionTag childTag = getConditionTagAt(i);
447                        if (childTag == tag) continue;
448                        childTag.rb.setChecked(false);
449                    }
450                    select(tag.condition);
451                    announceConditionSelection(tag);
452                }
453            }
454        });
455
456        if (tag.lines == null) {
457            tag.lines = row.findViewById(android.R.id.content);
458        }
459        if (tag.line1 == null) {
460            tag.line1 = (TextView) row.findViewById(android.R.id.text1);
461        }
462        if (tag.line2 == null) {
463            tag.line2 = (TextView) row.findViewById(android.R.id.text2);
464        }
465        final String line1, line2;
466        if (condition == null) {
467            line1 = mContext.getString(com.android.internal.R.string.zen_mode_forever);
468            line2 = null;
469        } else {
470            line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1 : condition.summary;
471            line2 = condition.line2;
472        }
473        tag.line1.setText(line1);
474        if (TextUtils.isEmpty(line2)) {
475            tag.line2.setVisibility(GONE);
476        } else {
477            tag.line2.setVisibility(VISIBLE);
478            tag.line2.setText(line2);
479        }
480        tag.lines.setEnabled(enabled);
481        tag.lines.setAlpha(enabled ? 1 : .4f);
482
483        final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
484        button1.setOnClickListener(new OnClickListener() {
485            @Override
486            public void onClick(View v) {
487                onClickTimeButton(row, tag, false /*down*/);
488            }
489        });
490
491        final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
492        button2.setOnClickListener(new OnClickListener() {
493            @Override
494            public void onClick(View v) {
495                onClickTimeButton(row, tag, true /*up*/);
496            }
497        });
498        tag.lines.setOnClickListener(new OnClickListener() {
499            @Override
500            public void onClick(View v) {
501                tag.rb.setChecked(true);
502            }
503        });
504
505        final long time = ZenModeConfig.tryParseCountdownConditionId(getConditionId(tag.condition));
506        if (time > 0) {
507            if (mBucketIndex > -1) {
508                button1.setEnabled(mBucketIndex > 0);
509                button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
510            } else {
511                final long span = time - System.currentTimeMillis();
512                button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
513                final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
514                        MAX_BUCKET_MINUTES);
515                button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
516            }
517
518            button1.setAlpha(button1.isEnabled() ? 1f : .5f);
519            button2.setAlpha(button2.isEnabled() ? 1f : .5f);
520        } else {
521            button1.setVisibility(View.GONE);
522            button2.setVisibility(View.GONE);
523        }
524        // wire up interaction callbacks for newly-added condition rows
525        if (convertView == null) {
526            Interaction.register(tag.rb, mInteractionCallback);
527            Interaction.register(tag.lines, mInteractionCallback);
528            Interaction.register(button1, mInteractionCallback);
529            Interaction.register(button2, mInteractionCallback);
530        }
531    }
532
533    private void announceConditionSelection(ConditionTag tag) {
534        final int zen = getSelectedZen(Global.ZEN_MODE_OFF);
535        String modeText;
536        switch(zen) {
537            case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
538                modeText = mContext.getString(R.string.zen_important_interruptions);
539                break;
540            case Global.ZEN_MODE_NO_INTERRUPTIONS:
541                modeText = mContext.getString(R.string.zen_no_interruptions);
542                break;
543             default:
544                return;
545        }
546        announceForAccessibility(mContext.getString(R.string.zen_mode_and_condition, modeText,
547                tag.line1.getText()));
548    }
549
550    private void onClickTimeButton(View row, ConditionTag tag, boolean up) {
551        Condition newCondition = null;
552        final int N = MINUTE_BUCKETS.length;
553        if (mBucketIndex == -1) {
554            // not on a known index, search for the next or prev bucket by time
555            final Uri conditionId = getConditionId(tag.condition);
556            final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
557            final long now = System.currentTimeMillis();
558            for (int i = 0; i < N; i++) {
559                int j = up ? i : N - 1 - i;
560                final int bucketMinutes = MINUTE_BUCKETS[j];
561                final long bucketTime = now + bucketMinutes * MINUTES_MS;
562                if (up && bucketTime > time || !up && bucketTime < time) {
563                    mBucketIndex = j;
564                    newCondition = ZenModeConfig.toTimeCondition(mContext,
565                            bucketTime, bucketMinutes, now);
566                    break;
567                }
568            }
569            if (newCondition == null) {
570                mBucketIndex = DEFAULT_BUCKET_INDEX;
571                newCondition = ZenModeConfig.toTimeCondition(mContext,
572                        MINUTE_BUCKETS[mBucketIndex]);
573            }
574        } else {
575            // on a known index, simply increment or decrement
576            mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
577            newCondition = ZenModeConfig.toTimeCondition(mContext,
578                    MINUTE_BUCKETS[mBucketIndex]);
579        }
580        mTimeCondition = newCondition;
581        bind(mTimeCondition, row);
582        tag.rb.setChecked(true);
583        select(mTimeCondition);
584        announceConditionSelection(tag);
585    }
586
587    private void select(Condition condition) {
588        if (DEBUG) Log.d(mTag, "select " + condition);
589        if (mController != null) {
590            mController.setExitCondition(condition);
591        }
592        setExitCondition(condition);
593        if (condition == null) {
594            mPrefs.setMinuteIndex(-1);
595        } else if (ZenModeConfig.isValidCountdownConditionId(condition.id) && mBucketIndex != -1) {
596            mPrefs.setMinuteIndex(mBucketIndex);
597        }
598        mSessionExitCondition = copy(condition);
599    }
600
601    private void fireMoreSettings() {
602        if (mCallback != null) {
603            mCallback.onMoreSettings();
604        }
605    }
606
607    private void fireInteraction() {
608        if (mCallback != null) {
609            mCallback.onInteraction();
610        }
611    }
612
613    private void fireExpanded() {
614        if (mCallback != null) {
615            mCallback.onExpanded(mExpanded);
616        }
617    }
618
619    private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() {
620        @Override
621        public void onZenChanged(int zen) {
622            mHandler.obtainMessage(H.UPDATE_ZEN, zen, 0).sendToTarget();
623        }
624        @Override
625        public void onConditionsChanged(Condition[] conditions) {
626            mHandler.obtainMessage(H.UPDATE_CONDITIONS, conditions).sendToTarget();
627        }
628
629        @Override
630        public void onExitConditionChanged(Condition exitCondition) {
631            mHandler.obtainMessage(H.EXIT_CONDITION_CHANGED, exitCondition).sendToTarget();
632        }
633    };
634
635    private final class H extends Handler {
636        private static final int UPDATE_CONDITIONS = 1;
637        private static final int EXIT_CONDITION_CHANGED = 2;
638        private static final int UPDATE_ZEN = 3;
639        private static final int SELECT_DEFAULT = 4;
640
641        private H() {
642            super(Looper.getMainLooper());
643        }
644
645        @Override
646        public void handleMessage(Message msg) {
647            if (msg.what == UPDATE_CONDITIONS) {
648                handleUpdateConditions((Condition[]) msg.obj);
649            } else if (msg.what == EXIT_CONDITION_CHANGED) {
650                handleExitConditionChanged((Condition) msg.obj);
651            } else if (msg.what == UPDATE_ZEN) {
652                handleUpdateZen(msg.arg1);
653            } else if (msg.what == SELECT_DEFAULT) {
654                handleSelectDefault();
655            }
656        }
657    }
658
659    public interface Callback {
660        void onMoreSettings();
661        void onInteraction();
662        void onExpanded(boolean expanded);
663    }
664
665    // used as the view tag on condition rows
666    private static class ConditionTag {
667        RadioButton rb;
668        View lines;
669        TextView line1;
670        TextView line2;
671        Condition condition;
672    }
673
674    private final class Prefs implements OnSharedPreferenceChangeListener {
675        private static final String KEY_MINUTE_INDEX = "minuteIndex";
676        private static final String KEY_NONE_SELECTED = "noneSelected";
677
678        private final int mNoneDangerousThreshold;
679
680        private int mMinuteIndex;
681        private int mNoneSelected;
682
683        private Prefs() {
684            mNoneDangerousThreshold = mContext.getResources()
685                    .getInteger(R.integer.zen_mode_alarm_warning_threshold);
686            prefs().registerOnSharedPreferenceChangeListener(this);
687            updateMinuteIndex();
688            updateNoneSelected();
689        }
690
691        public boolean isNoneDangerous() {
692            return mNoneSelected < mNoneDangerousThreshold;
693        }
694
695        public void trackNoneSelected() {
696            mNoneSelected = clampNoneSelected(mNoneSelected + 1);
697            if (DEBUG) Log.d(mTag, "Setting none selected: " + mNoneSelected + " threshold="
698                    + mNoneDangerousThreshold);
699            prefs().edit().putInt(KEY_NONE_SELECTED, mNoneSelected).apply();
700        }
701
702        public int getMinuteIndex() {
703            return mMinuteIndex;
704        }
705
706        public void setMinuteIndex(int minuteIndex) {
707            minuteIndex = clampIndex(minuteIndex);
708            if (minuteIndex == mMinuteIndex) return;
709            mMinuteIndex = clampIndex(minuteIndex);
710            if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex);
711            prefs().edit().putInt(KEY_MINUTE_INDEX, mMinuteIndex).apply();
712        }
713
714        @Override
715        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
716            updateMinuteIndex();
717            updateNoneSelected();
718        }
719
720        private SharedPreferences prefs() {
721            return mContext.getSharedPreferences(mContext.getPackageName(), 0);
722        }
723
724        private void updateMinuteIndex() {
725            mMinuteIndex = clampIndex(prefs().getInt(KEY_MINUTE_INDEX, DEFAULT_BUCKET_INDEX));
726            if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex);
727        }
728
729        private int clampIndex(int index) {
730            return MathUtils.constrain(index, -1, MINUTE_BUCKETS.length - 1);
731        }
732
733        private void updateNoneSelected() {
734            mNoneSelected = clampNoneSelected(prefs().getInt(KEY_NONE_SELECTED, 0));
735            if (DEBUG) Log.d(mTag, "None selected: " + mNoneSelected);
736        }
737
738        private int clampNoneSelected(int noneSelected) {
739            return MathUtils.constrain(noneSelected, 0, Integer.MAX_VALUE);
740        }
741    }
742
743    private final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() {
744        @Override
745        public void onSelected(Object value) {
746            if (value != null && mZenButtons.isShown()) {
747                if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + value);
748                mController.setZen((Integer) value);
749            }
750        }
751
752        @Override
753        public void onInteraction() {
754            fireInteraction();
755        }
756    };
757
758    private final Interaction.Callback mInteractionCallback = new Interaction.Callback() {
759        @Override
760        public void onInteraction() {
761            fireInteraction();
762        }
763    };
764}
765