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