• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.systemui.volume;
18 
19 import android.animation.LayoutTransition;
20 import android.animation.LayoutTransition.TransitionListener;
21 import android.app.ActivityManager;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.SharedPreferences;
25 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
26 import android.content.res.Configuration;
27 import android.net.Uri;
28 import android.os.AsyncTask;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.Message;
32 import android.provider.Settings;
33 import android.provider.Settings.Global;
34 import android.service.notification.Condition;
35 import android.service.notification.ZenModeConfig;
36 import android.service.notification.ZenModeConfig.ZenRule;
37 import android.text.TextUtils;
38 import android.text.format.DateFormat;
39 import android.util.ArraySet;
40 import android.util.AttributeSet;
41 import android.util.Log;
42 import android.util.MathUtils;
43 import android.view.LayoutInflater;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.widget.CompoundButton;
47 import android.widget.CompoundButton.OnCheckedChangeListener;
48 import android.widget.ImageView;
49 import android.widget.LinearLayout;
50 import android.widget.RadioButton;
51 import android.widget.TextView;
52 
53 import com.android.internal.logging.MetricsLogger;
54 import com.android.systemui.Prefs;
55 import com.android.systemui.R;
56 import com.android.systemui.statusbar.policy.ZenModeController;
57 
58 import java.io.FileDescriptor;
59 import java.io.PrintWriter;
60 import java.util.Arrays;
61 import java.util.Calendar;
62 import java.util.GregorianCalendar;
63 import java.util.Locale;
64 import java.util.Objects;
65 
66 public class ZenModePanel extends LinearLayout {
67     private static final String TAG = "ZenModePanel";
68     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
69 
70     private static final int SECONDS_MS = 1000;
71     private static final int MINUTES_MS = 60 * SECONDS_MS;
72 
73     private static final int[] MINUTE_BUCKETS = DEBUG
74             ? new int[] { 0, 1, 2, 5, 15, 30, 45, 60, 120, 180, 240, 480 }
75             : ZenModeConfig.MINUTE_BUCKETS;
76     private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
77     private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
78     private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
79     private static final int FOREVER_CONDITION_INDEX = 0;
80     private static final int COUNTDOWN_CONDITION_INDEX = 1;
81     private static final int COUNTDOWN_ALARM_CONDITION_INDEX = 2;
82     private static final int COUNTDOWN_CONDITION_COUNT = 2;
83 
84     public static final Intent ZEN_SETTINGS
85             = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS);
86     public static final Intent ZEN_PRIORITY_SETTINGS
87             = new Intent(Settings.ACTION_ZEN_MODE_PRIORITY_SETTINGS);
88 
89     private final Context mContext;
90     private final LayoutInflater mInflater;
91     private final H mHandler = new H();
92     private final ZenPrefs mPrefs;
93     private final TransitionHelper mTransitionHelper = new TransitionHelper();
94     private final Uri mForeverId;
95     private final SpTexts mSpTexts;
96 
97     private String mTag = TAG + "/" + Integer.toHexString(System.identityHashCode(this));
98 
99     private SegmentedButtons mZenButtons;
100     private View mZenIntroduction;
101     private TextView mZenIntroductionMessage;
102     private View mZenIntroductionConfirm;
103     private TextView mZenIntroductionCustomize;
104     private LinearLayout mZenConditions;
105     private TextView mZenAlarmWarning;
106 
107     private Callback mCallback;
108     private ZenModeController mController;
109     private boolean mCountdownConditionSupported;
110     private boolean mRequestingConditions;
111     private Condition mExitCondition;
112     private int mBucketIndex = -1;
113     private boolean mExpanded;
114     private boolean mHidden;
115     private int mSessionZen;
116     private int mAttachedZen;
117     private boolean mAttached;
118     private Condition mSessionExitCondition;
119     private Condition[] mConditions;
120     private Condition mTimeCondition;
121     private boolean mVoiceCapable;
122 
ZenModePanel(Context context, AttributeSet attrs)123     public ZenModePanel(Context context, AttributeSet attrs) {
124         super(context, attrs);
125         mContext = context;
126         mPrefs = new ZenPrefs();
127         mInflater = LayoutInflater.from(mContext.getApplicationContext());
128         mForeverId = Condition.newId(mContext).appendPath("forever").build();
129         mSpTexts = new SpTexts(mContext);
130         mVoiceCapable = Util.isVoiceCapable(mContext);
131         if (DEBUG) Log.d(mTag, "new ZenModePanel");
132     }
133 
dump(FileDescriptor fd, PrintWriter pw, String[] args)134     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
135         pw.println("ZenModePanel state:");
136         pw.print("  mCountdownConditionSupported="); pw.println(mCountdownConditionSupported);
137         pw.print("  mRequestingConditions="); pw.println(mRequestingConditions);
138         pw.print("  mAttached="); pw.println(mAttached);
139         pw.print("  mHidden="); pw.println(mHidden);
140         pw.print("  mExpanded="); pw.println(mExpanded);
141         pw.print("  mSessionZen="); pw.println(mSessionZen);
142         pw.print("  mAttachedZen="); pw.println(mAttachedZen);
143         pw.print("  mConfirmedPriorityIntroduction=");
144         pw.println(mPrefs.mConfirmedPriorityIntroduction);
145         pw.print("  mConfirmedSilenceIntroduction=");
146         pw.println(mPrefs.mConfirmedSilenceIntroduction);
147         pw.print("  mVoiceCapable="); pw.println(mVoiceCapable);
148         mTransitionHelper.dump(fd, pw, args);
149     }
150 
151     @Override
onFinishInflate()152     protected void onFinishInflate() {
153         super.onFinishInflate();
154 
155         mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons);
156         mZenButtons.addButton(R.string.interruption_level_none_twoline,
157                 R.string.interruption_level_none_with_warning,
158                 Global.ZEN_MODE_NO_INTERRUPTIONS);
159         mZenButtons.addButton(R.string.interruption_level_alarms_twoline,
160                 R.string.interruption_level_alarms,
161                 Global.ZEN_MODE_ALARMS);
162         mZenButtons.addButton(R.string.interruption_level_priority_twoline,
163                 R.string.interruption_level_priority,
164                 Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
165         mZenButtons.setCallback(mZenButtonsCallback);
166 
167         mZenIntroduction = findViewById(R.id.zen_introduction);
168         mZenIntroductionMessage = (TextView) findViewById(R.id.zen_introduction_message);
169         mSpTexts.add(mZenIntroductionMessage);
170         mZenIntroductionConfirm = findViewById(R.id.zen_introduction_confirm);
171         mZenIntroductionConfirm.setOnClickListener(new OnClickListener() {
172             @Override
173             public void onClick(View v) {
174                 confirmZenIntroduction();
175             }
176         });
177         mZenIntroductionCustomize = (TextView) findViewById(R.id.zen_introduction_customize);
178         mZenIntroductionCustomize.setOnClickListener(new OnClickListener() {
179             @Override
180             public void onClick(View v) {
181                 confirmZenIntroduction();
182                 if (mCallback != null) {
183                     mCallback.onPrioritySettings();
184                 }
185             }
186         });
187         mSpTexts.add(mZenIntroductionCustomize);
188 
189         mZenConditions = (LinearLayout) findViewById(R.id.zen_conditions);
190         mZenAlarmWarning = (TextView) findViewById(R.id.zen_alarm_warning);
191     }
192 
193     @Override
onConfigurationChanged(Configuration newConfig)194     protected void onConfigurationChanged(Configuration newConfig) {
195         super.onConfigurationChanged(newConfig);
196         if (mZenButtons != null) {
197             mZenButtons.updateLocale();
198         }
199     }
200 
confirmZenIntroduction()201     private void confirmZenIntroduction() {
202         final String prefKey = prefKeyForConfirmation(getSelectedZen(Global.ZEN_MODE_OFF));
203         if (prefKey == null) return;
204         if (DEBUG) Log.d(TAG, "confirmZenIntroduction " + prefKey);
205         Prefs.putBoolean(mContext, prefKey, true);
206         mHandler.sendEmptyMessage(H.UPDATE_WIDGETS);
207     }
208 
prefKeyForConfirmation(int zen)209     private static String prefKeyForConfirmation(int zen) {
210         switch (zen) {
211             case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
212                 return Prefs.Key.DND_CONFIRMED_PRIORITY_INTRODUCTION;
213             case Global.ZEN_MODE_NO_INTERRUPTIONS:
214                 return Prefs.Key.DND_CONFIRMED_SILENCE_INTRODUCTION;
215             default:
216                 return null;
217         }
218     }
219 
220     @Override
onAttachedToWindow()221     protected void onAttachedToWindow() {
222         super.onAttachedToWindow();
223         if (DEBUG) Log.d(mTag, "onAttachedToWindow");
224         mAttached = true;
225         mAttachedZen = getSelectedZen(-1);
226         mSessionZen = mAttachedZen;
227         mTransitionHelper.clear();
228         setSessionExitCondition(copy(mExitCondition));
229         updateWidgets();
230         setRequestingConditions(!mHidden);
231     }
232 
233     @Override
onDetachedFromWindow()234     protected void onDetachedFromWindow() {
235         super.onDetachedFromWindow();
236         if (DEBUG) Log.d(mTag, "onDetachedFromWindow");
237         checkForAttachedZenChange();
238         mAttached = false;
239         mAttachedZen = -1;
240         mSessionZen = -1;
241         setSessionExitCondition(null);
242         setRequestingConditions(false);
243         mTransitionHelper.clear();
244     }
245 
setSessionExitCondition(Condition condition)246     private void setSessionExitCondition(Condition condition) {
247         if (Objects.equals(condition, mSessionExitCondition)) return;
248         if (DEBUG) Log.d(mTag, "mSessionExitCondition=" + getConditionId(condition));
249         mSessionExitCondition = condition;
250     }
251 
setHidden(boolean hidden)252     public void setHidden(boolean hidden) {
253         if (mHidden == hidden) return;
254         if (DEBUG) Log.d(mTag, "hidden=" + hidden);
255         mHidden = hidden;
256         setRequestingConditions(mAttached && !mHidden);
257         updateWidgets();
258     }
259 
checkForAttachedZenChange()260     private void checkForAttachedZenChange() {
261         final int selectedZen = getSelectedZen(-1);
262         if (DEBUG) Log.d(mTag, "selectedZen=" + selectedZen);
263         if (selectedZen != mAttachedZen) {
264             if (DEBUG) Log.d(mTag, "attachedZen: " + mAttachedZen + " -> " + selectedZen);
265             if (selectedZen == Global.ZEN_MODE_NO_INTERRUPTIONS) {
266                 mPrefs.trackNoneSelected();
267             }
268         }
269     }
270 
setExpanded(boolean expanded)271     private void setExpanded(boolean expanded) {
272         if (expanded == mExpanded) return;
273         if (DEBUG) Log.d(mTag, "setExpanded " + expanded);
274         mExpanded = expanded;
275         if (mExpanded && isShown()) {
276             ensureSelection();
277         }
278         updateWidgets();
279         fireExpanded();
280     }
281 
282     /** Start or stop requesting relevant zen mode exit conditions */
setRequestingConditions(final boolean requesting)283     private void setRequestingConditions(final boolean requesting) {
284         if (mRequestingConditions == requesting) return;
285         if (DEBUG) Log.d(mTag, "setRequestingConditions " + requesting);
286         mRequestingConditions = requesting;
287         if (mRequestingConditions) {
288             mTimeCondition = parseExistingTimeCondition(mContext, mExitCondition);
289             if (mTimeCondition != null) {
290                 mBucketIndex = -1;
291             } else {
292                 mBucketIndex = DEFAULT_BUCKET_INDEX;
293                 mTimeCondition = ZenModeConfig.toTimeCondition(mContext,
294                         MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
295             }
296             if (DEBUG) Log.d(mTag, "Initial bucket index: " + mBucketIndex);
297 
298             mConditions = null; // reset conditions
299             handleUpdateConditions();
300         } else {
301             hideAllConditions();
302         }
303     }
304 
init(ZenModeController controller)305     public void init(ZenModeController controller) {
306         mController = controller;
307         mCountdownConditionSupported = mController.isCountdownConditionSupported();
308         final int countdownDelta = mCountdownConditionSupported ? COUNTDOWN_CONDITION_COUNT : 0;
309         final int minConditions = 1 /*forever*/ + countdownDelta;
310         for (int i = 0; i < minConditions; i++) {
311             mZenConditions.addView(mInflater.inflate(R.layout.zen_mode_condition, this, false));
312         }
313         mSessionZen = getSelectedZen(-1);
314         handleUpdateManualRule(mController.getManualRule());
315         if (DEBUG) Log.d(mTag, "init mExitCondition=" + mExitCondition);
316         hideAllConditions();
317         mController.addCallback(mZenCallback);
318     }
319 
updateLocale()320     public void updateLocale() {
321         mZenButtons.updateLocale();
322     }
323 
setExitCondition(Condition exitCondition)324     private void setExitCondition(Condition exitCondition) {
325         if (Objects.equals(mExitCondition, exitCondition)) return;
326         mExitCondition = exitCondition;
327         if (DEBUG) Log.d(mTag, "mExitCondition=" + getConditionId(mExitCondition));
328         updateWidgets();
329     }
330 
getConditionId(Condition condition)331     private static Uri getConditionId(Condition condition) {
332         return condition != null ? condition.id : null;
333     }
334 
getRealConditionId(Condition condition)335     private Uri getRealConditionId(Condition condition) {
336         return isForever(condition) ? null : getConditionId(condition);
337     }
338 
sameConditionId(Condition lhs, Condition rhs)339     private static boolean sameConditionId(Condition lhs, Condition rhs) {
340         return lhs == null ? rhs == null : rhs != null && lhs.id.equals(rhs.id);
341     }
342 
copy(Condition condition)343     private static Condition copy(Condition condition) {
344         return condition == null ? null : condition.copy();
345     }
346 
setCallback(Callback callback)347     public void setCallback(Callback callback) {
348         mCallback = callback;
349     }
350 
handleUpdateManualRule(ZenRule rule)351     private void handleUpdateManualRule(ZenRule rule) {
352         final int zen = rule != null ? rule.zenMode : Global.ZEN_MODE_OFF;
353         handleUpdateZen(zen);
354         final Condition c = rule != null ? rule.condition : null;
355         handleExitConditionChanged(c);
356     }
357 
handleUpdateZen(int zen)358     private void handleUpdateZen(int zen) {
359         if (mSessionZen != -1 && mSessionZen != zen) {
360             setExpanded(isShown());
361             mSessionZen = zen;
362         }
363         mZenButtons.setSelectedValue(zen, false /* fromClick */);
364         updateWidgets();
365         handleUpdateConditions();
366         if (mExpanded) {
367             final Condition selected = getSelectedCondition();
368             if (!Objects.equals(mExitCondition, selected)) {
369                 select(selected);
370             }
371         }
372     }
373 
handleExitConditionChanged(Condition exitCondition)374     private void handleExitConditionChanged(Condition exitCondition) {
375         setExitCondition(exitCondition);
376         if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitCondition);
377         final int N = getVisibleConditions();
378         for (int i = 0; i < N; i++) {
379             final ConditionTag tag = getConditionTagAt(i);
380             if (tag != null) {
381                 if (sameConditionId(tag.condition, mExitCondition)) {
382                     bind(exitCondition, mZenConditions.getChildAt(i), i);
383                 }
384             }
385         }
386     }
387 
getSelectedCondition()388     private Condition getSelectedCondition() {
389         final int N = getVisibleConditions();
390         for (int i = 0; i < N; i++) {
391             final ConditionTag tag = getConditionTagAt(i);
392             if (tag != null && tag.rb.isChecked()) {
393                 return tag.condition;
394             }
395         }
396         return null;
397     }
398 
getSelectedZen(int defValue)399     private int getSelectedZen(int defValue) {
400         final Object zen = mZenButtons.getSelectedValue();
401         return zen != null ? (Integer) zen : defValue;
402     }
403 
updateWidgets()404     private void updateWidgets() {
405         if (mTransitionHelper.isTransitioning()) {
406             mTransitionHelper.pendingUpdateWidgets();
407             return;
408         }
409         final int zen = getSelectedZen(Global.ZEN_MODE_OFF);
410         final boolean zenImportant = zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
411         final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
412         final boolean introduction = (zenImportant && !mPrefs.mConfirmedPriorityIntroduction
413                         || zenNone && !mPrefs.mConfirmedSilenceIntroduction);
414 
415         mZenButtons.setVisibility(mHidden ? GONE : VISIBLE);
416         mZenIntroduction.setVisibility(introduction ? VISIBLE : GONE);
417         if (introduction) {
418             mZenIntroductionMessage.setText(zenImportant ? R.string.zen_priority_introduction
419                     : mVoiceCapable ? R.string.zen_silence_introduction_voice
420                     : R.string.zen_silence_introduction);
421             mZenIntroductionCustomize.setVisibility(zenImportant ? VISIBLE : GONE);
422         }
423         final String warning = computeAlarmWarningText(zenNone);
424         mZenAlarmWarning.setVisibility(warning != null ? VISIBLE : GONE);
425         mZenAlarmWarning.setText(warning);
426     }
427 
computeAlarmWarningText(boolean zenNone)428     private String computeAlarmWarningText(boolean zenNone) {
429         if (!zenNone) {
430             return null;
431         }
432         final long now = System.currentTimeMillis();
433         final long nextAlarm = mController.getNextAlarm();
434         if (nextAlarm < now) {
435             return null;
436         }
437         int warningRes = 0;
438         if (mSessionExitCondition == null || isForever(mSessionExitCondition)) {
439             warningRes = R.string.zen_alarm_warning_indef;
440         } else {
441             final long time = ZenModeConfig.tryParseCountdownConditionId(mSessionExitCondition.id);
442             if (time > now && nextAlarm < time) {
443                 warningRes = R.string.zen_alarm_warning;
444             }
445         }
446         if (warningRes == 0) {
447             return null;
448         }
449         final boolean soon = (nextAlarm - now) < 24 * 60 * 60 * 1000;
450         final boolean is24 = DateFormat.is24HourFormat(mContext, ActivityManager.getCurrentUser());
451         final String skeleton = soon ? (is24 ? "Hm" : "hma") : (is24 ? "EEEHm" : "EEEhma");
452         final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
453         final CharSequence formattedTime = DateFormat.format(pattern, nextAlarm);
454         final int templateRes = soon ? R.string.alarm_template : R.string.alarm_template_far;
455         final String template = getResources().getString(templateRes, formattedTime);
456         return getResources().getString(warningRes, template);
457     }
458 
459     private static Condition parseExistingTimeCondition(Context context, Condition condition) {
460         if (condition == null) return null;
461         final long time = ZenModeConfig.tryParseCountdownConditionId(condition.id);
462         if (time == 0) return null;
463         final long now = System.currentTimeMillis();
464         final long span = time - now;
465         if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null;
466         return ZenModeConfig.toTimeCondition(context,
467                 time, Math.round(span / (float) MINUTES_MS), ActivityManager.getCurrentUser(),
468                 false /*shortVersion*/);
469     }
470 
471     private void handleUpdateConditions() {
472         if (mTransitionHelper.isTransitioning()) {
473             return;
474         }
475         final int conditionCount = mConditions == null ? 0 : mConditions.length;
476         if (DEBUG) Log.d(mTag, "handleUpdateConditions conditionCount=" + conditionCount);
477         // forever
478         bind(forever(), mZenConditions.getChildAt(FOREVER_CONDITION_INDEX),
479                 FOREVER_CONDITION_INDEX);
480         // countdown
481         if (mCountdownConditionSupported && mTimeCondition != null) {
482             bind(mTimeCondition, mZenConditions.getChildAt(COUNTDOWN_CONDITION_INDEX),
483                     COUNTDOWN_CONDITION_INDEX);
484         }
485         // countdown until alarm
486         if (mCountdownConditionSupported) {
487             Condition nextAlarmCondition = getTimeUntilNextAlarmCondition();
488             if (nextAlarmCondition != null) {
489                 bind(nextAlarmCondition,
490                         mZenConditions.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX),
491                         COUNTDOWN_ALARM_CONDITION_INDEX);
492             }
493         }
494         // ensure something is selected
495         if (mExpanded && isShown()) {
496             ensureSelection();
497         }
498     }
499 
500     private Condition forever() {
501         return new Condition(mForeverId, foreverSummary(mContext), "", "", 0 /*icon*/,
502                 Condition.STATE_TRUE, 0 /*flags*/);
503     }
504 
505     private static String foreverSummary(Context context) {
506         return context.getString(com.android.internal.R.string.zen_mode_forever);
507     }
508 
509     // Returns a time condition if the next alarm is within the next week.
510     private Condition getTimeUntilNextAlarmCondition() {
511         GregorianCalendar weekRange = new GregorianCalendar();
512         final long now = weekRange.getTimeInMillis();
513         setToMidnight(weekRange);
514         weekRange.add(Calendar.DATE, 6);
515         final long nextAlarmMs = mController.getNextAlarm();
516         if (nextAlarmMs > 0) {
517             GregorianCalendar nextAlarm = new GregorianCalendar();
518             nextAlarm.setTimeInMillis(nextAlarmMs);
519             setToMidnight(nextAlarm);
520 
521             if (weekRange.compareTo(nextAlarm) >= 0) {
522                 return ZenModeConfig.toNextAlarmCondition(mContext, now, nextAlarmMs,
523                         ActivityManager.getCurrentUser());
524             }
525         }
526         return null;
527     }
528 
setToMidnight(Calendar calendar)529     private void setToMidnight(Calendar calendar) {
530         calendar.set(Calendar.HOUR_OF_DAY, 0);
531         calendar.set(Calendar.MINUTE, 0);
532         calendar.set(Calendar.SECOND, 0);
533         calendar.set(Calendar.MILLISECOND, 0);
534     }
535 
getConditionTagAt(int index)536     private ConditionTag getConditionTagAt(int index) {
537         return (ConditionTag) mZenConditions.getChildAt(index).getTag();
538     }
539 
getVisibleConditions()540     private int getVisibleConditions() {
541         int rt = 0;
542         final int N = mZenConditions.getChildCount();
543         for (int i = 0; i < N; i++) {
544             rt += mZenConditions.getChildAt(i).getVisibility() == VISIBLE ? 1 : 0;
545         }
546         return rt;
547     }
548 
hideAllConditions()549     private void hideAllConditions() {
550         final int N = mZenConditions.getChildCount();
551         for (int i = 0; i < N; i++) {
552             mZenConditions.getChildAt(i).setVisibility(GONE);
553         }
554     }
555 
ensureSelection()556     private void ensureSelection() {
557         // are we left without anything selected?  if so, set a default
558         final int visibleConditions = getVisibleConditions();
559         if (visibleConditions == 0) return;
560         for (int i = 0; i < visibleConditions; i++) {
561             final ConditionTag tag = getConditionTagAt(i);
562             if (tag != null && tag.rb.isChecked()) {
563                 if (DEBUG) Log.d(mTag, "Not selecting a default, checked=" + tag.condition);
564                 return;
565             }
566         }
567         final ConditionTag foreverTag = getConditionTagAt(FOREVER_CONDITION_INDEX);
568         if (foreverTag == null) return;
569         if (DEBUG) Log.d(mTag, "Selecting a default");
570         final int favoriteIndex = mPrefs.getMinuteIndex();
571         if (favoriteIndex == -1 || !mCountdownConditionSupported) {
572             foreverTag.rb.setChecked(true);
573         } else {
574             mTimeCondition = ZenModeConfig.toTimeCondition(mContext,
575                     MINUTE_BUCKETS[favoriteIndex], ActivityManager.getCurrentUser());
576             mBucketIndex = favoriteIndex;
577             bind(mTimeCondition, mZenConditions.getChildAt(COUNTDOWN_CONDITION_INDEX),
578                     COUNTDOWN_CONDITION_INDEX);
579             getConditionTagAt(COUNTDOWN_CONDITION_INDEX).rb.setChecked(true);
580         }
581     }
582 
isCountdown(Condition c)583     private static boolean isCountdown(Condition c) {
584         return c != null && ZenModeConfig.isValidCountdownConditionId(c.id);
585     }
586 
isForever(Condition c)587     private boolean isForever(Condition c) {
588         return c != null && mForeverId.equals(c.id);
589     }
590 
bind(final Condition condition, final View row, final int rowId)591     private void bind(final Condition condition, final View row, final int rowId) {
592         if (condition == null) throw new IllegalArgumentException("condition must not be null");
593         final boolean enabled = condition.state == Condition.STATE_TRUE;
594         final ConditionTag tag =
595                 row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag();
596         row.setTag(tag);
597         final boolean first = tag.rb == null;
598         if (tag.rb == null) {
599             tag.rb = (RadioButton) row.findViewById(android.R.id.checkbox);
600         }
601         tag.condition = condition;
602         final Uri conditionId = getConditionId(tag.condition);
603         if (DEBUG) Log.d(mTag, "bind i=" + mZenConditions.indexOfChild(row) + " first=" + first
604                 + " condition=" + conditionId);
605         tag.rb.setEnabled(enabled);
606         final boolean checked = (mSessionExitCondition != null
607                     || mAttachedZen != Global.ZEN_MODE_OFF)
608                 && (sameConditionId(mSessionExitCondition, tag.condition));
609         if (checked != tag.rb.isChecked()) {
610             if (DEBUG) Log.d(mTag, "bind checked=" + checked + " condition=" + conditionId);
611             tag.rb.setChecked(checked);
612         }
613         tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
614             @Override
615             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
616                 if (mExpanded && isChecked) {
617                     if (DEBUG) Log.d(mTag, "onCheckedChanged " + conditionId);
618                     final int N = getVisibleConditions();
619                     for (int i = 0; i < N; i++) {
620                         final ConditionTag childTag = getConditionTagAt(i);
621                         if (childTag == null || childTag == tag) continue;
622                         childTag.rb.setChecked(false);
623                     }
624                     MetricsLogger.action(mContext, MetricsLogger.QS_DND_CONDITION_SELECT);
625                     select(tag.condition);
626                     announceConditionSelection(tag);
627                 }
628             }
629         });
630 
631         if (tag.lines == null) {
632             tag.lines = row.findViewById(android.R.id.content);
633         }
634         if (tag.line1 == null) {
635             tag.line1 = (TextView) row.findViewById(android.R.id.text1);
636             mSpTexts.add(tag.line1);
637         }
638         if (tag.line2 == null) {
639             tag.line2 = (TextView) row.findViewById(android.R.id.text2);
640             mSpTexts.add(tag.line2);
641         }
642         final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1
643                 : condition.summary;
644         final String line2 = condition.line2;
645         tag.line1.setText(line1);
646         if (TextUtils.isEmpty(line2)) {
647             tag.line2.setVisibility(GONE);
648         } else {
649             tag.line2.setVisibility(VISIBLE);
650             tag.line2.setText(line2);
651         }
652         tag.lines.setEnabled(enabled);
653         tag.lines.setAlpha(enabled ? 1 : .4f);
654 
655         final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
656         button1.setOnClickListener(new OnClickListener() {
657             @Override
658             public void onClick(View v) {
659                 onClickTimeButton(row, tag, false /*down*/, rowId);
660             }
661         });
662 
663         final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
664         button2.setOnClickListener(new OnClickListener() {
665             @Override
666             public void onClick(View v) {
667                 onClickTimeButton(row, tag, true /*up*/, rowId);
668             }
669         });
670         tag.lines.setOnClickListener(new OnClickListener() {
671             @Override
672             public void onClick(View v) {
673                 tag.rb.setChecked(true);
674             }
675         });
676 
677         final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
678         if (rowId != COUNTDOWN_ALARM_CONDITION_INDEX && time > 0) {
679             button1.setVisibility(VISIBLE);
680             button2.setVisibility(VISIBLE);
681             if (mBucketIndex > -1) {
682                 button1.setEnabled(mBucketIndex > 0);
683                 button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
684             } else {
685                 final long span = time - System.currentTimeMillis();
686                 button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
687                 final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
688                         MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser());
689                 button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
690             }
691 
692             button1.setAlpha(button1.isEnabled() ? 1f : .5f);
693             button2.setAlpha(button2.isEnabled() ? 1f : .5f);
694         } else {
695             button1.setVisibility(GONE);
696             button2.setVisibility(GONE);
697         }
698         // wire up interaction callbacks for newly-added condition rows
699         if (first) {
700             Interaction.register(tag.rb, mInteractionCallback);
701             Interaction.register(tag.lines, mInteractionCallback);
702             Interaction.register(button1, mInteractionCallback);
703             Interaction.register(button2, mInteractionCallback);
704         }
705         row.setVisibility(VISIBLE);
706     }
707 
announceConditionSelection(ConditionTag tag)708     private void announceConditionSelection(ConditionTag tag) {
709         final int zen = getSelectedZen(Global.ZEN_MODE_OFF);
710         String modeText;
711         switch(zen) {
712             case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
713                 modeText = mContext.getString(R.string.interruption_level_priority);
714                 break;
715             case Global.ZEN_MODE_NO_INTERRUPTIONS:
716                 modeText = mContext.getString(R.string.interruption_level_none);
717                 break;
718             case Global.ZEN_MODE_ALARMS:
719                 modeText = mContext.getString(R.string.interruption_level_alarms);
720                 break;
721             default:
722                 return;
723         }
724         announceForAccessibility(mContext.getString(R.string.zen_mode_and_condition, modeText,
725                 tag.line1.getText()));
726     }
727 
onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId)728     private void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) {
729         MetricsLogger.action(mContext, MetricsLogger.QS_DND_TIME, up);
730         Condition newCondition = null;
731         final int N = MINUTE_BUCKETS.length;
732         if (mBucketIndex == -1) {
733             // not on a known index, search for the next or prev bucket by time
734             final Uri conditionId = getConditionId(tag.condition);
735             final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
736             final long now = System.currentTimeMillis();
737             for (int i = 0; i < N; i++) {
738                 int j = up ? i : N - 1 - i;
739                 final int bucketMinutes = MINUTE_BUCKETS[j];
740                 final long bucketTime = now + bucketMinutes * MINUTES_MS;
741                 if (up && bucketTime > time || !up && bucketTime < time) {
742                     mBucketIndex = j;
743                     newCondition = ZenModeConfig.toTimeCondition(mContext,
744                             bucketTime, bucketMinutes, ActivityManager.getCurrentUser(),
745                             false /*shortVersion*/);
746                     break;
747                 }
748             }
749             if (newCondition == null) {
750                 mBucketIndex = DEFAULT_BUCKET_INDEX;
751                 newCondition = ZenModeConfig.toTimeCondition(mContext,
752                         MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
753             }
754         } else {
755             // on a known index, simply increment or decrement
756             mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
757             newCondition = ZenModeConfig.toTimeCondition(mContext,
758                     MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
759         }
760         mTimeCondition = newCondition;
761         bind(mTimeCondition, row, rowId);
762         tag.rb.setChecked(true);
763         select(mTimeCondition);
764         announceConditionSelection(tag);
765     }
766 
select(final Condition condition)767     private void select(final Condition condition) {
768         if (DEBUG) Log.d(mTag, "select " + condition);
769         if (mSessionZen == -1 || mSessionZen == Global.ZEN_MODE_OFF) {
770             if (DEBUG) Log.d(mTag, "Ignoring condition selection outside of manual zen");
771             return;
772         }
773         final Uri realConditionId = getRealConditionId(condition);
774         if (mController != null) {
775             AsyncTask.execute(new Runnable() {
776                 @Override
777                 public void run() {
778                     mController.setZen(mSessionZen, realConditionId, TAG + ".selectCondition");
779                 }
780             });
781         }
782         setExitCondition(condition);
783         if (realConditionId == null) {
784             mPrefs.setMinuteIndex(-1);
785         } else if (isCountdown(condition) && mBucketIndex != -1) {
786             mPrefs.setMinuteIndex(mBucketIndex);
787         }
788         setSessionExitCondition(copy(condition));
789     }
790 
fireInteraction()791     private void fireInteraction() {
792         if (mCallback != null) {
793             mCallback.onInteraction();
794         }
795     }
796 
fireExpanded()797     private void fireExpanded() {
798         if (mCallback != null) {
799             mCallback.onExpanded(mExpanded);
800         }
801     }
802 
803     private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() {
804         @Override
805         public void onManualRuleChanged(ZenRule rule) {
806             mHandler.obtainMessage(H.MANUAL_RULE_CHANGED, rule).sendToTarget();
807         }
808     };
809 
810     private final class H extends Handler {
811         private static final int MANUAL_RULE_CHANGED = 2;
812         private static final int UPDATE_WIDGETS = 3;
813 
H()814         private H() {
815             super(Looper.getMainLooper());
816         }
817 
818         @Override
handleMessage(Message msg)819         public void handleMessage(Message msg) {
820             switch (msg.what) {
821                 case MANUAL_RULE_CHANGED: handleUpdateManualRule((ZenRule) msg.obj); break;
822                 case UPDATE_WIDGETS: updateWidgets(); break;
823             }
824         }
825     }
826 
827     public interface Callback {
onPrioritySettings()828         void onPrioritySettings();
onInteraction()829         void onInteraction();
onExpanded(boolean expanded)830         void onExpanded(boolean expanded);
831     }
832 
833     // used as the view tag on condition rows
834     private static class ConditionTag {
835         RadioButton rb;
836         View lines;
837         TextView line1;
838         TextView line2;
839         Condition condition;
840     }
841 
842     private final class ZenPrefs implements OnSharedPreferenceChangeListener {
843         private final int mNoneDangerousThreshold;
844 
845         private int mMinuteIndex;
846         private int mNoneSelected;
847         private boolean mConfirmedPriorityIntroduction;
848         private boolean mConfirmedSilenceIntroduction;
849 
ZenPrefs()850         private ZenPrefs() {
851             mNoneDangerousThreshold = mContext.getResources()
852                     .getInteger(R.integer.zen_mode_alarm_warning_threshold);
853             Prefs.registerListener(mContext, this);
854             updateMinuteIndex();
855             updateNoneSelected();
856             updateConfirmedPriorityIntroduction();
857             updateConfirmedSilenceIntroduction();
858         }
859 
trackNoneSelected()860         public void trackNoneSelected() {
861             mNoneSelected = clampNoneSelected(mNoneSelected + 1);
862             if (DEBUG) Log.d(mTag, "Setting none selected: " + mNoneSelected + " threshold="
863                     + mNoneDangerousThreshold);
864             Prefs.putInt(mContext, Prefs.Key.DND_NONE_SELECTED, mNoneSelected);
865         }
866 
getMinuteIndex()867         public int getMinuteIndex() {
868             return mMinuteIndex;
869         }
870 
setMinuteIndex(int minuteIndex)871         public void setMinuteIndex(int minuteIndex) {
872             minuteIndex = clampIndex(minuteIndex);
873             if (minuteIndex == mMinuteIndex) return;
874             mMinuteIndex = clampIndex(minuteIndex);
875             if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex);
876             Prefs.putInt(mContext, Prefs.Key.DND_FAVORITE_BUCKET_INDEX, mMinuteIndex);
877         }
878 
879         @Override
onSharedPreferenceChanged(SharedPreferences prefs, String key)880         public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
881             updateMinuteIndex();
882             updateNoneSelected();
883             updateConfirmedPriorityIntroduction();
884             updateConfirmedSilenceIntroduction();
885         }
886 
updateMinuteIndex()887         private void updateMinuteIndex() {
888             mMinuteIndex = clampIndex(Prefs.getInt(mContext,
889                     Prefs.Key.DND_FAVORITE_BUCKET_INDEX, DEFAULT_BUCKET_INDEX));
890             if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex);
891         }
892 
clampIndex(int index)893         private int clampIndex(int index) {
894             return MathUtils.constrain(index, -1, MINUTE_BUCKETS.length - 1);
895         }
896 
updateNoneSelected()897         private void updateNoneSelected() {
898             mNoneSelected = clampNoneSelected(Prefs.getInt(mContext,
899                     Prefs.Key.DND_NONE_SELECTED, 0));
900             if (DEBUG) Log.d(mTag, "None selected: " + mNoneSelected);
901         }
902 
clampNoneSelected(int noneSelected)903         private int clampNoneSelected(int noneSelected) {
904             return MathUtils.constrain(noneSelected, 0, Integer.MAX_VALUE);
905         }
906 
updateConfirmedPriorityIntroduction()907         private void updateConfirmedPriorityIntroduction() {
908             final boolean confirmed =  Prefs.getBoolean(mContext,
909                     Prefs.Key.DND_CONFIRMED_PRIORITY_INTRODUCTION, false);
910             if (confirmed == mConfirmedPriorityIntroduction) return;
911             mConfirmedPriorityIntroduction = confirmed;
912             if (DEBUG) Log.d(mTag, "Confirmed priority introduction: "
913                     + mConfirmedPriorityIntroduction);
914         }
915 
updateConfirmedSilenceIntroduction()916         private void updateConfirmedSilenceIntroduction() {
917             final boolean confirmed =  Prefs.getBoolean(mContext,
918                     Prefs.Key.DND_CONFIRMED_SILENCE_INTRODUCTION, false);
919             if (confirmed == mConfirmedSilenceIntroduction) return;
920             mConfirmedSilenceIntroduction = confirmed;
921             if (DEBUG) Log.d(mTag, "Confirmed silence introduction: "
922                     + mConfirmedSilenceIntroduction);
923         }
924     }
925 
926     private final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() {
927         @Override
928         public void onSelected(final Object value, boolean fromClick) {
929             if (value != null && mZenButtons.isShown() && isAttachedToWindow()) {
930                 final int zen = (Integer) value;
931                 if (fromClick) {
932                     MetricsLogger.action(mContext, MetricsLogger.QS_DND_ZEN_SELECT, zen);
933                 }
934                 if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + zen);
935                 final Uri realConditionId = getRealConditionId(mSessionExitCondition);
936                 AsyncTask.execute(new Runnable() {
937                     @Override
938                     public void run() {
939                         mController.setZen(zen, realConditionId, TAG + ".selectZen");
940                         if (zen != Global.ZEN_MODE_OFF) {
941                             Prefs.putInt(mContext, Prefs.Key.DND_FAVORITE_ZEN, zen);
942                         }
943                     }
944                 });
945             }
946         }
947 
948         @Override
949         public void onInteraction() {
950             fireInteraction();
951         }
952     };
953 
954     private final Interaction.Callback mInteractionCallback = new Interaction.Callback() {
955         @Override
956         public void onInteraction() {
957             fireInteraction();
958         }
959     };
960 
961     private final class TransitionHelper implements TransitionListener, Runnable {
962         private final ArraySet<View> mTransitioningViews = new ArraySet<View>();
963 
964         private boolean mTransitioning;
965         private boolean mPendingUpdateWidgets;
966 
clear()967         public void clear() {
968             mTransitioningViews.clear();
969             mPendingUpdateWidgets = false;
970         }
971 
dump(FileDescriptor fd, PrintWriter pw, String[] args)972         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
973             pw.println("  TransitionHelper state:");
974             pw.print("    mPendingUpdateWidgets="); pw.println(mPendingUpdateWidgets);
975             pw.print("    mTransitioning="); pw.println(mTransitioning);
976             pw.print("    mTransitioningViews="); pw.println(mTransitioningViews);
977         }
978 
pendingUpdateWidgets()979         public void pendingUpdateWidgets() {
980             mPendingUpdateWidgets = true;
981         }
982 
isTransitioning()983         public boolean isTransitioning() {
984             return !mTransitioningViews.isEmpty();
985         }
986 
987         @Override
startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType)988         public void startTransition(LayoutTransition transition,
989                 ViewGroup container, View view, int transitionType) {
990             mTransitioningViews.add(view);
991             updateTransitioning();
992         }
993 
994         @Override
endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType)995         public void endTransition(LayoutTransition transition,
996                 ViewGroup container, View view, int transitionType) {
997             mTransitioningViews.remove(view);
998             updateTransitioning();
999         }
1000 
1001         @Override
run()1002         public void run() {
1003             if (DEBUG) Log.d(mTag, "TransitionHelper run"
1004                     + " mPendingUpdateWidgets=" + mPendingUpdateWidgets);
1005             if (mPendingUpdateWidgets) {
1006                 updateWidgets();
1007             }
1008             mPendingUpdateWidgets = false;
1009         }
1010 
updateTransitioning()1011         private void updateTransitioning() {
1012             final boolean transitioning = isTransitioning();
1013             if (mTransitioning == transitioning) return;
1014             mTransitioning = transitioning;
1015             if (DEBUG) Log.d(mTag, "TransitionHelper mTransitioning=" + mTransitioning);
1016             if (!mTransitioning) {
1017                 if (mPendingUpdateWidgets) {
1018                     mHandler.post(this);
1019                 } else {
1020                     mPendingUpdateWidgets = false;
1021                 }
1022             }
1023         }
1024     }
1025 }
1026