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