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