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