ZenModePanel.java revision 969265af3131f7213cab50c08e09700ed6af4a88
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.net.Uri;
26import android.os.Handler;
27import android.os.Looper;
28import android.os.Message;
29import android.provider.Settings;
30import android.provider.Settings.Global;
31import android.service.notification.Condition;
32import android.service.notification.ZenModeConfig;
33import android.text.format.DateFormat;
34import android.text.format.Time;
35import android.util.AttributeSet;
36import android.util.Log;
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.text.SimpleDateFormat;
52import java.util.Arrays;
53import java.util.Date;
54import java.util.Locale;
55import java.util.Objects;
56
57public class ZenModePanel extends LinearLayout {
58    private static final String TAG = "ZenModePanel";
59    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
60
61    private static final int SECONDS_MS = 1000;
62    private static final int MINUTES_MS = 60 * SECONDS_MS;
63
64    private static final int[] MINUTE_BUCKETS = DEBUG
65            ? new int[] { 0, 1, 2, 5, 15, 30, 45, 60, 120, 180, 240, 480 }
66            : new int[] { 15, 30, 45, 60, 120, 180, 240, 480 };
67    private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
68    private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
69    private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
70    private static final int FOREVER_CONDITION_INDEX = 0;
71    private static final int TIME_CONDITION_INDEX = 1;
72    private static final int FIRST_CONDITION_INDEX = 2;
73    private static final float SILENT_HINT_PULSE_SCALE = 1.1f;
74    private static final int ZERO_VALUE_MS = 20 * SECONDS_MS;
75
76    public static final Intent ZEN_SETTINGS = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS);
77
78    private final Context mContext;
79    private final LayoutInflater mInflater;
80    private final H mHandler = new H();
81    private final Favorites mFavorites;
82    private final Interpolator mFastOutSlowInInterpolator;
83    private final int mTextColor;
84    private final int mAccentColor;
85
86    private char mLogTag = '?';
87    private String mTag;
88
89    private SegmentedButtons mZenButtons;
90    private View mZenSubhead;
91    private TextView mZenSubheadCollapsed;
92    private TextView mZenSubheadExpanded;
93    private View mMoreSettings;
94    private LinearLayout mZenConditions;
95    private TextView mAlarmWarning;
96
97    private Callback mCallback;
98    private ZenModeController mController;
99    private boolean mRequestingConditions;
100    private Uri mExitConditionId;
101    private int mBucketIndex = -1;
102    private boolean mExpanded;
103    private int mSessionZen;
104    private Uri mSessionExitConditionId;
105    private String mExitConditionText;
106    private long mNextAlarm;
107
108    public ZenModePanel(Context context, AttributeSet attrs) {
109        super(context, attrs);
110        mContext = context;
111        mFavorites = new Favorites();
112        mInflater = LayoutInflater.from(mContext.getApplicationContext());
113        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext,
114                android.R.interpolator.fast_out_slow_in);
115        mTextColor = mContext.getResources().getColor(R.color.qs_text);
116        mAccentColor = mContext.getResources().getColor(R.color.system_accent_color);
117        updateTag();
118        if (DEBUG) Log.d(mTag, "new ZenModePanel");
119    }
120
121    private void updateTag() {
122        mTag = TAG + "/" + mLogTag + "/" + Integer.toHexString(System.identityHashCode(this));
123    }
124
125    @Override
126    protected void onFinishInflate() {
127        super.onFinishInflate();
128
129        mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons);
130        mZenButtons.addButton(R.string.interruption_level_none, Global.ZEN_MODE_NO_INTERRUPTIONS);
131        mZenButtons.addButton(R.string.interruption_level_priority,
132                Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
133        mZenButtons.addButton(R.string.interruption_level_all, Global.ZEN_MODE_OFF);
134        mZenButtons.setCallback(mZenButtonsCallback);
135
136        mZenSubhead = findViewById(R.id.zen_subhead);
137
138        mZenSubheadCollapsed = (TextView) findViewById(R.id.zen_subhead_collapsed);
139        mZenSubheadCollapsed.setOnClickListener(new View.OnClickListener() {
140            @Override
141            public void onClick(View v) {
142                setExpanded(true);
143                fireInteraction();
144            }
145        });
146
147        mZenSubheadExpanded = (TextView) findViewById(R.id.zen_subhead_expanded);
148
149        mMoreSettings = findViewById(R.id.zen_more_settings);
150        mMoreSettings.setOnClickListener(new View.OnClickListener() {
151            @Override
152            public void onClick(View v) {
153                fireMoreSettings();
154                fireInteraction();
155            }
156        });
157
158        mZenConditions = (LinearLayout) findViewById(R.id.zen_conditions);
159        mAlarmWarning = (TextView) findViewById(R.id.zen_alarm_warning);
160    }
161
162    @Override
163    protected void onAttachedToWindow() {
164        super.onAttachedToWindow();
165        if (DEBUG) Log.d(mTag, "onAttachedToWindow");
166        mSessionZen = getSelectedZen(-1);
167        mSessionExitConditionId = mExitConditionId;
168        refreshExitConditionText();
169        refreshNextAlarm();
170        updateWidgets();
171    }
172
173    @Override
174    protected void onDetachedFromWindow() {
175        super.onDetachedFromWindow();
176        if (DEBUG) Log.d(mTag, "onDetachedFromWindow");
177        mSessionZen = -1;
178        mSessionExitConditionId = null;
179        setExpanded(false);
180    }
181
182    private void setExpanded(boolean expanded) {
183        if (expanded == mExpanded) return;
184        mExpanded = expanded;
185        updateWidgets();
186        setRequestingConditions(mExpanded);
187        fireExpanded();
188    }
189
190    /** Start or stop requesting relevant zen mode exit conditions */
191    private void setRequestingConditions(boolean requesting) {
192        if (mRequestingConditions == requesting) return;
193        if (DEBUG) Log.d(mTag, "setRequestingConditions " + requesting);
194        mRequestingConditions = requesting;
195        if (mController != null) {
196            mController.requestConditions(mRequestingConditions);
197        }
198        if (mRequestingConditions) {
199            Condition timeCondition = parseExistingTimeCondition(mExitConditionId);
200            if (timeCondition != null) {
201                mBucketIndex = -1;
202            } else {
203                mBucketIndex = DEFAULT_BUCKET_INDEX;
204                timeCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]);
205            }
206            if (DEBUG) Log.d(mTag, "Initial bucket index: " + mBucketIndex);
207            handleUpdateConditions(new Condition[0]);  // ensures forever exists
208            bind(timeCondition, mZenConditions.getChildAt(TIME_CONDITION_INDEX));
209            checkForDefault();
210        } else {
211            mZenConditions.removeAllViews();
212        }
213    }
214
215    public void init(ZenModeController controller, char logTag) {
216        mController = controller;
217        mLogTag = logTag;
218        updateTag();
219        setExitConditionId(mController.getExitConditionId());
220        refreshExitConditionText();
221        mSessionZen = getSelectedZen(-1);
222        handleUpdateZen(mController.getZen());
223        if (DEBUG) Log.d(mTag, "init mExitConditionId=" + mExitConditionId);
224        mZenConditions.removeAllViews();
225        mController.addCallback(mZenCallback);
226    }
227
228    private void setExitConditionId(Uri exitConditionId) {
229        if (Objects.equals(mExitConditionId, exitConditionId)) return;
230        mExitConditionId = exitConditionId;
231        refreshExitConditionText();
232        updateWidgets();
233    }
234
235    private void refreshExitConditionText() {
236        final String forever = mContext.getString(R.string.zen_mode_forever);
237        if (mExitConditionId == null) {
238            mExitConditionText = forever;
239        } else if (ZenModeConfig.isValidCountdownConditionId(mExitConditionId)) {
240            final Condition condition = parseExistingTimeCondition(mExitConditionId);
241            mExitConditionText = condition != null ? condition.summary : forever;
242        } else {
243            mExitConditionText = "(until condition ends)";  // TODO persist current description
244        }
245    }
246
247    public void setCallback(Callback callback) {
248        mCallback = callback;
249    }
250
251    public void showSilentHint() {
252        if (DEBUG) Log.d(mTag, "showSilentHint");
253        if (mZenButtons == null || mZenButtons.getChildCount() == 0) return;
254        final View noneButton = mZenButtons.getChildAt(0);
255        if (noneButton.getScaleX() != 1) return;  // already running
256        noneButton.animate().cancel();
257        noneButton.animate().scaleX(SILENT_HINT_PULSE_SCALE).scaleY(SILENT_HINT_PULSE_SCALE)
258                .setInterpolator(mFastOutSlowInInterpolator)
259                .setListener(new AnimatorListenerAdapter() {
260                    @Override
261                    public void onAnimationEnd(Animator animation) {
262                        noneButton.animate().scaleX(1).scaleY(1).setListener(null);
263                    }
264                });
265    }
266
267    private void handleUpdateZen(int zen) {
268        if (mSessionZen != -1 && mSessionZen != zen) {
269            setExpanded(zen != Global.ZEN_MODE_OFF);
270            mSessionZen = zen;
271        }
272        mZenButtons.setSelectedValue(zen);
273        updateWidgets();
274    }
275
276    private int getSelectedZen(int defValue) {
277        final Object zen = mZenButtons.getSelectedValue();
278        return zen != null ? (Integer) zen : defValue;
279    }
280
281    private void updateWidgets() {
282        final int zen = getSelectedZen(Global.ZEN_MODE_OFF);
283        final boolean zenOff = zen == Global.ZEN_MODE_OFF;
284        final boolean zenImportant = zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
285        final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
286        final boolean foreverSelected = mExitConditionId == null;
287        final boolean hasNextAlarm = mNextAlarm != 0;
288
289        mZenSubhead.setVisibility(!zenOff && (mExpanded || !foreverSelected) ? VISIBLE : GONE);
290        mZenSubheadExpanded.setVisibility(mExpanded ? VISIBLE : GONE);
291        mZenSubheadCollapsed.setVisibility(!mExpanded ? VISIBLE : GONE);
292        mMoreSettings.setVisibility(zenImportant && mExpanded ? VISIBLE : GONE);
293        mZenConditions.setVisibility(!zenOff && mExpanded ? VISIBLE : GONE);
294        mAlarmWarning.setVisibility(zenNone && mExpanded && hasNextAlarm ? VISIBLE : GONE);
295
296        if (zenNone && mExpanded && hasNextAlarm) {
297            final long exitTime = ZenModeConfig.tryParseCountdownConditionId(mExitConditionId);
298            final long now = System.currentTimeMillis();
299            final boolean alarmToday = time(mNextAlarm).yearDay == time(now).yearDay;
300            final String skeleton = (alarmToday ? "" : "E")
301                    + (DateFormat.is24HourFormat(mContext) ? "Hm" : "hma");
302            final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
303            final String alarm = new SimpleDateFormat(pattern).format(new Date(mNextAlarm));
304            final boolean isWarning = exitTime > 0 && mNextAlarm > now && mNextAlarm < exitTime;
305            if (isWarning) {
306                mAlarmWarning.setText(mContext.getString(R.string.zen_alarm_warning, alarm));
307                mAlarmWarning.setTextColor(mAccentColor);
308            } else {
309                mAlarmWarning.setText(mContext.getString(alarmToday
310                        ? R.string.zen_alarm_information_time
311                        : R.string.zen_alarm_information_day_time, alarm));
312                mAlarmWarning.setTextColor(mTextColor);
313            }
314        }
315
316        if (zenNone) {
317            mZenSubheadExpanded.setText(R.string.zen_no_interruptions_with_warning);
318            mZenSubheadCollapsed.setText(mExitConditionText);
319        } else if (zenImportant) {
320            mZenSubheadExpanded.setText(R.string.zen_important_interruptions);
321            mZenSubheadCollapsed.setText(mExitConditionText);
322        }
323    }
324
325    private static Time time(long millis) {
326        final Time t = new Time();
327        t.set(millis);
328        return t;
329    }
330
331    private Condition parseExistingTimeCondition(Uri conditionId) {
332        final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
333        if (time == 0) return null;
334        final long span = time - System.currentTimeMillis();
335        if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null;
336        return timeCondition(time, Math.round(span / (float)MINUTES_MS));
337    }
338
339    private Condition newTimeCondition(int minutesFromNow) {
340        final long now = System.currentTimeMillis();
341        final long millis = minutesFromNow == 0 ? ZERO_VALUE_MS : minutesFromNow * MINUTES_MS;
342        return timeCondition(now + millis, minutesFromNow);
343    }
344
345    private Condition timeCondition(long time, int minutes) {
346        final int num = minutes < 60 ? minutes : Math.round(minutes / 60f);
347        final int resId = minutes < 60
348                ? R.plurals.zen_mode_duration_minutes
349                : R.plurals.zen_mode_duration_hours;
350        final String caption = mContext.getResources().getQuantityString(resId, num, num);
351        final Uri id = ZenModeConfig.toCountdownConditionId(time);
352        return new Condition(id, caption, "", "", 0, Condition.STATE_TRUE,
353                Condition.FLAG_RELEVANT_NOW);
354    }
355
356    private void refreshNextAlarm() {
357        mNextAlarm = mController.getNextAlarm();
358    }
359
360    private void handleNextAlarmChanged() {
361        refreshNextAlarm();
362        updateWidgets();
363    }
364
365    private void handleUpdateConditions(Condition[] conditions) {
366        final int newCount = conditions == null ? 0 : conditions.length;
367        if (DEBUG) Log.d(mTag, "handleUpdateConditions newCount=" + newCount);
368        for (int i = mZenConditions.getChildCount(); i >= newCount + FIRST_CONDITION_INDEX; i--) {
369            mZenConditions.removeViewAt(i);
370        }
371        bind(null, mZenConditions.getChildAt(FOREVER_CONDITION_INDEX));
372        for (int i = 0; i < newCount; i++) {
373            bind(conditions[i], mZenConditions.getChildAt(FIRST_CONDITION_INDEX + i));
374        }
375    }
376
377    private ConditionTag getConditionTagAt(int index) {
378        return (ConditionTag) mZenConditions.getChildAt(index).getTag();
379    }
380
381    private void checkForDefault() {
382        // are we left without anything selected?  if so, set a default
383        for (int i = 0; i < mZenConditions.getChildCount(); i++) {
384            if (getConditionTagAt(i).rb.isChecked()) {
385                if (DEBUG) Log.d(mTag, "Not selecting a default, checked="
386                        + getConditionTagAt(i).conditionId);
387                return;
388            }
389        }
390        if (DEBUG) Log.d(mTag, "Selecting a default");
391        final int favoriteIndex = mFavorites.getMinuteIndex();
392        if (favoriteIndex == -1) {
393            getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true);
394        } else {
395            final Condition c = newTimeCondition(MINUTE_BUCKETS[favoriteIndex]);
396            mBucketIndex = favoriteIndex;
397            bind(c, mZenConditions.getChildAt(TIME_CONDITION_INDEX));
398            getConditionTagAt(TIME_CONDITION_INDEX).rb.setChecked(true);
399        }
400    }
401
402    private void handleExitConditionChanged(Uri exitCondition) {
403        setExitConditionId(exitCondition);
404        if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitConditionId);
405        final int N = mZenConditions.getChildCount();
406        for (int i = 0; i < N; i++) {
407            final ConditionTag tag = getConditionTagAt(i);
408            tag.rb.setChecked(Objects.equals(tag.conditionId, exitCondition));
409        }
410    }
411
412    private void bind(final Condition condition, View convertView) {
413        final boolean enabled = condition == null || condition.state == Condition.STATE_TRUE;
414        final View row;
415        if (convertView == null) {
416            row = mInflater.inflate(R.layout.zen_mode_condition, this, false);
417            if (DEBUG) Log.d(mTag, "Adding new condition view for: " + condition);
418            mZenConditions.addView(row);
419        } else {
420            row = convertView;
421        }
422        final ConditionTag tag =
423                row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag();
424        row.setTag(tag);
425        if (tag.rb == null) {
426            tag.rb = (RadioButton) row.findViewById(android.R.id.checkbox);
427        }
428        tag.conditionId = condition != null ? condition.id : null;
429        tag.rb.setEnabled(enabled);
430        if (mSessionExitConditionId != null && mSessionExitConditionId.equals(tag.conditionId)) {
431            tag.rb.setChecked(true);
432        }
433        tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
434            @Override
435            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
436                if (mExpanded && isChecked) {
437                    if (DEBUG) Log.d(mTag, "onCheckedChanged " + tag.conditionId);
438                    final int N = mZenConditions.getChildCount();
439                    for (int i = 0; i < N; i++) {
440                        ConditionTag childTag = getConditionTagAt(i);
441                        if (childTag == tag) continue;
442                        childTag.rb.setChecked(false);
443                    }
444                    select(tag.conditionId);
445                    fireInteraction();
446                }
447            }
448        });
449        final TextView title = (TextView) row.findViewById(android.R.id.title);
450        if (condition == null) {
451            title.setText(R.string.zen_mode_forever);
452        } else {
453            title.setText(condition.summary);
454        }
455        title.setEnabled(enabled);
456        title.setAlpha(enabled ? 1 : .4f);
457        final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
458        button1.setOnClickListener(new OnClickListener() {
459            @Override
460            public void onClick(View v) {
461                onClickTimeButton(row, tag, false /*down*/);
462            }
463        });
464
465        final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
466        button2.setOnClickListener(new OnClickListener() {
467            @Override
468            public void onClick(View v) {
469                onClickTimeButton(row, tag, true /*up*/);
470            }
471        });
472        title.setOnClickListener(new OnClickListener() {
473            @Override
474            public void onClick(View v) {
475                tag.rb.setChecked(true);
476                fireInteraction();
477            }
478        });
479
480        final long time = ZenModeConfig.tryParseCountdownConditionId(tag.conditionId);
481        if (time > 0) {
482            if (mBucketIndex > -1) {
483                button1.setEnabled(mBucketIndex > 0);
484                button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
485            } else {
486                final long span = time - System.currentTimeMillis();
487                button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
488                final Condition maxCondition = newTimeCondition(MAX_BUCKET_MINUTES);
489                button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
490            }
491
492            button1.setAlpha(button1.isEnabled() ? 1f : .5f);
493            button2.setAlpha(button2.isEnabled() ? 1f : .5f);
494        } else {
495            button1.setVisibility(View.GONE);
496            button2.setVisibility(View.GONE);
497        }
498    }
499
500    private void onClickTimeButton(View row, ConditionTag tag, boolean up) {
501        Condition newCondition = null;
502        final int N = MINUTE_BUCKETS.length;
503        if (mBucketIndex == -1) {
504            // not on a known index, search for the next or prev bucket by time
505            final long time = ZenModeConfig.tryParseCountdownConditionId(tag.conditionId);
506            final long now = System.currentTimeMillis();
507            for (int i = 0; i < N; i++) {
508                int j = up ? i : N - 1 - i;
509                final int bucketMinutes = MINUTE_BUCKETS[j];
510                final long bucketTime = now + bucketMinutes * MINUTES_MS;
511                if (up && bucketTime > time || !up && bucketTime < time) {
512                    mBucketIndex = j;
513                    newCondition = timeCondition(bucketTime, bucketMinutes);
514                    break;
515                }
516            }
517            if (newCondition == null) {
518                mBucketIndex = DEFAULT_BUCKET_INDEX;
519                newCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]);
520            }
521        } else {
522            // on a known index, simply increment or decrement
523            mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
524            newCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]);
525        }
526        bind(newCondition, row);
527        tag.rb.setChecked(true);
528        select(newCondition.id);
529        fireInteraction();
530    }
531
532    private void select(Uri conditionId) {
533        if (DEBUG) Log.d(mTag, "select " + conditionId);
534        if (mController != null) {
535            mController.setExitConditionId(conditionId);
536        }
537        setExitConditionId(conditionId);
538        if (conditionId == null) {
539            mFavorites.setMinuteIndex(-1);
540        } else if (ZenModeConfig.isValidCountdownConditionId(conditionId) && mBucketIndex != -1) {
541            mFavorites.setMinuteIndex(mBucketIndex);
542        }
543        mSessionExitConditionId = conditionId;
544    }
545
546    private void fireMoreSettings() {
547        if (mCallback != null) {
548            mCallback.onMoreSettings();
549        }
550    }
551
552    private void fireInteraction() {
553        if (mCallback != null) {
554            mCallback.onInteraction();
555        }
556    }
557
558    private void fireExpanded() {
559        if (mCallback != null) {
560            mCallback.onExpanded(mExpanded);
561        }
562    }
563
564    private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() {
565        @Override
566        public void onZenChanged(int zen) {
567            mHandler.obtainMessage(H.UPDATE_ZEN, zen, 0).sendToTarget();
568        }
569        @Override
570        public void onConditionsChanged(Condition[] conditions) {
571            mHandler.obtainMessage(H.UPDATE_CONDITIONS, conditions).sendToTarget();
572        }
573
574        @Override
575        public void onExitConditionChanged(Uri exitConditionId) {
576            mHandler.obtainMessage(H.EXIT_CONDITION_CHANGED, exitConditionId).sendToTarget();
577        }
578
579        @Override
580        public void onNextAlarmChanged() {
581            mHandler.sendEmptyMessage(H.NEXT_ALARM_CHANGED);
582        }
583    };
584
585    private final class H extends Handler {
586        private static final int UPDATE_CONDITIONS = 1;
587        private static final int EXIT_CONDITION_CHANGED = 2;
588        private static final int UPDATE_ZEN = 3;
589        private static final int NEXT_ALARM_CHANGED = 4;
590
591        private H() {
592            super(Looper.getMainLooper());
593        }
594
595        @Override
596        public void handleMessage(Message msg) {
597            if (msg.what == UPDATE_CONDITIONS) {
598                handleUpdateConditions((Condition[]) msg.obj);
599                checkForDefault();
600            } else if (msg.what == EXIT_CONDITION_CHANGED) {
601                handleExitConditionChanged((Uri) msg.obj);
602            } else if (msg.what == UPDATE_ZEN) {
603                handleUpdateZen(msg.arg1);
604            } else if (msg.what == NEXT_ALARM_CHANGED) {
605                handleNextAlarmChanged();
606            }
607        }
608    }
609
610    public interface Callback {
611        void onMoreSettings();
612        void onInteraction();
613        void onExpanded(boolean expanded);
614    }
615
616    // used as the view tag on condition rows
617    private static class ConditionTag {
618        RadioButton rb;
619        Uri conditionId;
620    }
621
622    private final class Favorites implements OnSharedPreferenceChangeListener {
623        private static final String KEY_MINUTE_INDEX = "minuteIndex";
624
625        private int mMinuteIndex;
626
627        private Favorites() {
628            prefs().registerOnSharedPreferenceChangeListener(this);
629            updateMinuteIndex();
630        }
631
632        public int getMinuteIndex() {
633            return mMinuteIndex;
634        }
635
636        public void setMinuteIndex(int minuteIndex) {
637            minuteIndex = clamp(minuteIndex);
638            if (minuteIndex == mMinuteIndex) return;
639            mMinuteIndex = clamp(minuteIndex);
640            if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex);
641            prefs().edit().putInt(KEY_MINUTE_INDEX, mMinuteIndex).apply();
642        }
643
644        @Override
645        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
646            updateMinuteIndex();
647        }
648
649        private SharedPreferences prefs() {
650            return mContext.getSharedPreferences(ZenModePanel.class.getSimpleName(), 0);
651        }
652
653        private void updateMinuteIndex() {
654            mMinuteIndex = clamp(prefs().getInt(KEY_MINUTE_INDEX, DEFAULT_BUCKET_INDEX));
655            if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex);
656        }
657
658        private int clamp(int index) {
659            return Math.max(-1, Math.min(MINUTE_BUCKETS.length - 1, index));
660        }
661    }
662
663    private final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() {
664        @Override
665        public void onSelected(Object value) {
666            if (value != null && mZenButtons.isShown()) {
667                if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + value);
668                mController.setZen((Integer) value);
669            }
670        }
671    };
672}
673