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