• 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.toTimeCondition(mContext, nextAlarmMs,
548                         Math.round((nextAlarmMs - now) / (float) MINUTES_MS),
549                         ActivityManager.getCurrentUser(), true);
550             }
551         }
552         return null;
553     }
554 
setToMidnight(Calendar calendar)555     private void setToMidnight(Calendar calendar) {
556         calendar.set(Calendar.HOUR_OF_DAY, 0);
557         calendar.set(Calendar.MINUTE, 0);
558         calendar.set(Calendar.SECOND, 0);
559         calendar.set(Calendar.MILLISECOND, 0);
560     }
561 
getConditionTagAt(int index)562     private ConditionTag getConditionTagAt(int index) {
563         return (ConditionTag) mZenRadioGroupContent.getChildAt(index).getTag();
564     }
565 
getVisibleConditions()566     private int getVisibleConditions() {
567         int rt = 0;
568         final int N = mZenRadioGroupContent.getChildCount();
569         for (int i = 0; i < N; i++) {
570             rt += mZenRadioGroupContent.getChildAt(i).getVisibility() == VISIBLE ? 1 : 0;
571         }
572         return rt;
573     }
574 
hideAllConditions()575     private void hideAllConditions() {
576         final int N = mZenRadioGroupContent.getChildCount();
577         for (int i = 0; i < N; i++) {
578             mZenRadioGroupContent.getChildAt(i).setVisibility(GONE);
579         }
580     }
581 
ensureSelection()582     private void ensureSelection() {
583         // are we left without anything selected?  if so, set a default
584         final int visibleConditions = getVisibleConditions();
585         if (visibleConditions == 0) return;
586         for (int i = 0; i < visibleConditions; i++) {
587             final ConditionTag tag = getConditionTagAt(i);
588             if (tag != null && tag.rb.isChecked()) {
589                 if (DEBUG) Log.d(mTag, "Not selecting a default, checked=" + tag.condition);
590                 return;
591             }
592         }
593         final ConditionTag foreverTag = getConditionTagAt(FOREVER_CONDITION_INDEX);
594         if (foreverTag == null) return;
595         if (DEBUG) Log.d(mTag, "Selecting a default");
596         final int favoriteIndex = mPrefs.getMinuteIndex();
597         if (favoriteIndex == -1 || !mCountdownConditionSupported) {
598             foreverTag.rb.setChecked(true);
599         } else {
600             mTimeCondition = ZenModeConfig.toTimeCondition(mContext,
601                     MINUTE_BUCKETS[favoriteIndex], ActivityManager.getCurrentUser());
602             mBucketIndex = favoriteIndex;
603             bind(mTimeCondition, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
604                     COUNTDOWN_CONDITION_INDEX);
605             getConditionTagAt(COUNTDOWN_CONDITION_INDEX).rb.setChecked(true);
606         }
607     }
608 
isCountdown(Condition c)609     private static boolean isCountdown(Condition c) {
610         return c != null && ZenModeConfig.isValidCountdownConditionId(c.id);
611     }
612 
isForever(Condition c)613     private boolean isForever(Condition c) {
614         return c != null && mForeverId.equals(c.id);
615     }
616 
bind(final Condition condition, final View row, final int rowId)617     private void bind(final Condition condition, final View row, final int rowId) {
618         if (condition == null) throw new IllegalArgumentException("condition must not be null");
619         final boolean enabled = condition.state == Condition.STATE_TRUE;
620         final ConditionTag tag =
621                 row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag();
622         row.setTag(tag);
623         final boolean first = tag.rb == null;
624         if (tag.rb == null) {
625             tag.rb = (RadioButton) mZenRadioGroup.getChildAt(rowId);
626         }
627         tag.condition = condition;
628         final Uri conditionId = getConditionId(tag.condition);
629         if (DEBUG) Log.d(mTag, "bind i=" + mZenRadioGroupContent.indexOfChild(row) + " first="
630                 + first + " condition=" + conditionId);
631         tag.rb.setEnabled(enabled);
632         tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
633             @Override
634             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
635                 if (mExpanded && isChecked) {
636                     tag.rb.setChecked(true);
637                     if (DEBUG) Log.d(mTag, "onCheckedChanged " + conditionId);
638                     MetricsLogger.action(mContext, MetricsEvent.QS_DND_CONDITION_SELECT);
639                     select(tag.condition);
640                     announceConditionSelection(tag);
641                 }
642             }
643         });
644 
645         if (tag.lines == null) {
646             tag.lines = row.findViewById(android.R.id.content);
647         }
648         if (tag.line1 == null) {
649             tag.line1 = (TextView) row.findViewById(android.R.id.text1);
650             mSpTexts.add(tag.line1);
651         }
652         if (tag.line2 == null) {
653             tag.line2 = (TextView) row.findViewById(android.R.id.text2);
654             mSpTexts.add(tag.line2);
655         }
656         final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1
657                 : condition.summary;
658         final String line2 = condition.line2;
659         tag.line1.setText(line1);
660         if (TextUtils.isEmpty(line2)) {
661             tag.line2.setVisibility(GONE);
662         } else {
663             tag.line2.setVisibility(VISIBLE);
664             tag.line2.setText(line2);
665         }
666         tag.lines.setEnabled(enabled);
667         tag.lines.setAlpha(enabled ? 1 : .4f);
668 
669         final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
670         button1.setOnClickListener(new OnClickListener() {
671             @Override
672             public void onClick(View v) {
673                 onClickTimeButton(row, tag, false /*down*/, rowId);
674             }
675         });
676 
677         final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
678         button2.setOnClickListener(new OnClickListener() {
679             @Override
680             public void onClick(View v) {
681                 onClickTimeButton(row, tag, true /*up*/, rowId);
682             }
683         });
684         tag.lines.setOnClickListener(new OnClickListener() {
685             @Override
686             public void onClick(View v) {
687                 tag.rb.setChecked(true);
688             }
689         });
690 
691         final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
692         if (rowId != COUNTDOWN_ALARM_CONDITION_INDEX && time > 0) {
693             button1.setVisibility(VISIBLE);
694             button2.setVisibility(VISIBLE);
695             if (mBucketIndex > -1) {
696                 button1.setEnabled(mBucketIndex > 0);
697                 button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
698             } else {
699                 final long span = time - System.currentTimeMillis();
700                 button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
701                 final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
702                         MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser());
703                 button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
704             }
705 
706             button1.setAlpha(button1.isEnabled() ? 1f : .5f);
707             button2.setAlpha(button2.isEnabled() ? 1f : .5f);
708         } else {
709             button1.setVisibility(GONE);
710             button2.setVisibility(GONE);
711         }
712         // wire up interaction callbacks for newly-added condition rows
713         if (first) {
714             Interaction.register(tag.rb, mInteractionCallback);
715             Interaction.register(tag.lines, mInteractionCallback);
716             Interaction.register(button1, mInteractionCallback);
717             Interaction.register(button2, mInteractionCallback);
718         }
719         row.setVisibility(VISIBLE);
720     }
721 
announceConditionSelection(ConditionTag tag)722     private void announceConditionSelection(ConditionTag tag) {
723         final int zen = getSelectedZen(Global.ZEN_MODE_OFF);
724         String modeText;
725         switch(zen) {
726             case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
727                 modeText = mContext.getString(R.string.interruption_level_priority);
728                 break;
729             case Global.ZEN_MODE_NO_INTERRUPTIONS:
730                 modeText = mContext.getString(R.string.interruption_level_none);
731                 break;
732             case Global.ZEN_MODE_ALARMS:
733                 modeText = mContext.getString(R.string.interruption_level_alarms);
734                 break;
735             default:
736                 return;
737         }
738         announceForAccessibility(mContext.getString(R.string.zen_mode_and_condition, modeText,
739                 tag.line1.getText()));
740     }
741 
onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId)742     private void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) {
743         MetricsLogger.action(mContext, MetricsEvent.QS_DND_TIME, up);
744         Condition newCondition = null;
745         final int N = MINUTE_BUCKETS.length;
746         if (mBucketIndex == -1) {
747             // not on a known index, search for the next or prev bucket by time
748             final Uri conditionId = getConditionId(tag.condition);
749             final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
750             final long now = System.currentTimeMillis();
751             for (int i = 0; i < N; i++) {
752                 int j = up ? i : N - 1 - i;
753                 final int bucketMinutes = MINUTE_BUCKETS[j];
754                 final long bucketTime = now + bucketMinutes * MINUTES_MS;
755                 if (up && bucketTime > time || !up && bucketTime < time) {
756                     mBucketIndex = j;
757                     newCondition = ZenModeConfig.toTimeCondition(mContext,
758                             bucketTime, bucketMinutes, ActivityManager.getCurrentUser(),
759                             false /*shortVersion*/);
760                     break;
761                 }
762             }
763             if (newCondition == null) {
764                 mBucketIndex = DEFAULT_BUCKET_INDEX;
765                 newCondition = ZenModeConfig.toTimeCondition(mContext,
766                         MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
767             }
768         } else {
769             // on a known index, simply increment or decrement
770             mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
771             newCondition = ZenModeConfig.toTimeCondition(mContext,
772                     MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
773         }
774         mTimeCondition = newCondition;
775         bind(mTimeCondition, row, rowId);
776         tag.rb.setChecked(true);
777         select(mTimeCondition);
778         announceConditionSelection(tag);
779     }
780 
select(final Condition condition)781     private void select(final Condition condition) {
782         if (DEBUG) Log.d(mTag, "select " + condition);
783         if (mSessionZen == -1 || mSessionZen == Global.ZEN_MODE_OFF) {
784             if (DEBUG) Log.d(mTag, "Ignoring condition selection outside of manual zen");
785             return;
786         }
787         final Uri realConditionId = getRealConditionId(condition);
788         if (mController != null) {
789             AsyncTask.execute(new Runnable() {
790                 @Override
791                 public void run() {
792                     mController.setZen(mSessionZen, realConditionId, TAG + ".selectCondition");
793                 }
794             });
795         }
796         setExitCondition(condition);
797         if (realConditionId == null) {
798             mPrefs.setMinuteIndex(-1);
799         } else if (isCountdown(condition) && mBucketIndex != -1) {
800             mPrefs.setMinuteIndex(mBucketIndex);
801         }
802         setSessionExitCondition(copy(condition));
803     }
804 
fireInteraction()805     private void fireInteraction() {
806         if (mCallback != null) {
807             mCallback.onInteraction();
808         }
809     }
810 
fireExpanded()811     private void fireExpanded() {
812         if (mCallback != null) {
813             mCallback.onExpanded(mExpanded);
814         }
815     }
816 
817     private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() {
818         @Override
819         public void onManualRuleChanged(ZenRule rule) {
820             mHandler.obtainMessage(H.MANUAL_RULE_CHANGED, rule).sendToTarget();
821         }
822     };
823 
824     private final class H extends Handler {
825         private static final int MANUAL_RULE_CHANGED = 2;
826         private static final int UPDATE_WIDGETS = 3;
827 
H()828         private H() {
829             super(Looper.getMainLooper());
830         }
831 
832         @Override
handleMessage(Message msg)833         public void handleMessage(Message msg) {
834             switch (msg.what) {
835                 case MANUAL_RULE_CHANGED: handleUpdateManualRule((ZenRule) msg.obj); break;
836                 case UPDATE_WIDGETS: updateWidgets(); break;
837             }
838         }
839     }
840 
841     public interface Callback {
onPrioritySettings()842         void onPrioritySettings();
onInteraction()843         void onInteraction();
onExpanded(boolean expanded)844         void onExpanded(boolean expanded);
845     }
846 
847     // used as the view tag on condition rows
848     private static class ConditionTag {
849         RadioButton rb;
850         View lines;
851         TextView line1;
852         TextView line2;
853         Condition condition;
854     }
855 
856     private final class ZenPrefs implements OnSharedPreferenceChangeListener {
857         private final int mNoneDangerousThreshold;
858 
859         private int mMinuteIndex;
860         private int mNoneSelected;
861         private boolean mConfirmedPriorityIntroduction;
862         private boolean mConfirmedSilenceIntroduction;
863 
ZenPrefs()864         private ZenPrefs() {
865             mNoneDangerousThreshold = mContext.getResources()
866                     .getInteger(R.integer.zen_mode_alarm_warning_threshold);
867             Prefs.registerListener(mContext, this);
868             updateMinuteIndex();
869             updateNoneSelected();
870             updateConfirmedPriorityIntroduction();
871             updateConfirmedSilenceIntroduction();
872         }
873 
trackNoneSelected()874         public void trackNoneSelected() {
875             mNoneSelected = clampNoneSelected(mNoneSelected + 1);
876             if (DEBUG) Log.d(mTag, "Setting none selected: " + mNoneSelected + " threshold="
877                     + mNoneDangerousThreshold);
878             Prefs.putInt(mContext, Prefs.Key.DND_NONE_SELECTED, mNoneSelected);
879         }
880 
getMinuteIndex()881         public int getMinuteIndex() {
882             return mMinuteIndex;
883         }
884 
setMinuteIndex(int minuteIndex)885         public void setMinuteIndex(int minuteIndex) {
886             minuteIndex = clampIndex(minuteIndex);
887             if (minuteIndex == mMinuteIndex) return;
888             mMinuteIndex = clampIndex(minuteIndex);
889             if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex);
890             Prefs.putInt(mContext, Prefs.Key.DND_FAVORITE_BUCKET_INDEX, mMinuteIndex);
891         }
892 
893         @Override
onSharedPreferenceChanged(SharedPreferences prefs, String key)894         public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
895             updateMinuteIndex();
896             updateNoneSelected();
897             updateConfirmedPriorityIntroduction();
898             updateConfirmedSilenceIntroduction();
899         }
900 
updateMinuteIndex()901         private void updateMinuteIndex() {
902             mMinuteIndex = clampIndex(Prefs.getInt(mContext,
903                     Prefs.Key.DND_FAVORITE_BUCKET_INDEX, DEFAULT_BUCKET_INDEX));
904             if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex);
905         }
906 
clampIndex(int index)907         private int clampIndex(int index) {
908             return MathUtils.constrain(index, -1, MINUTE_BUCKETS.length - 1);
909         }
910 
updateNoneSelected()911         private void updateNoneSelected() {
912             mNoneSelected = clampNoneSelected(Prefs.getInt(mContext,
913                     Prefs.Key.DND_NONE_SELECTED, 0));
914             if (DEBUG) Log.d(mTag, "None selected: " + mNoneSelected);
915         }
916 
clampNoneSelected(int noneSelected)917         private int clampNoneSelected(int noneSelected) {
918             return MathUtils.constrain(noneSelected, 0, Integer.MAX_VALUE);
919         }
920 
updateConfirmedPriorityIntroduction()921         private void updateConfirmedPriorityIntroduction() {
922             final boolean confirmed =  Prefs.getBoolean(mContext,
923                     Prefs.Key.DND_CONFIRMED_PRIORITY_INTRODUCTION, false);
924             if (confirmed == mConfirmedPriorityIntroduction) return;
925             mConfirmedPriorityIntroduction = confirmed;
926             if (DEBUG) Log.d(mTag, "Confirmed priority introduction: "
927                     + mConfirmedPriorityIntroduction);
928         }
929 
updateConfirmedSilenceIntroduction()930         private void updateConfirmedSilenceIntroduction() {
931             final boolean confirmed =  Prefs.getBoolean(mContext,
932                     Prefs.Key.DND_CONFIRMED_SILENCE_INTRODUCTION, false);
933             if (confirmed == mConfirmedSilenceIntroduction) return;
934             mConfirmedSilenceIntroduction = confirmed;
935             if (DEBUG) Log.d(mTag, "Confirmed silence introduction: "
936                     + mConfirmedSilenceIntroduction);
937         }
938     }
939 
940     protected final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() {
941         @Override
942         public void onSelected(final Object value, boolean fromClick) {
943             if (value != null && mZenButtons.isShown() && isAttachedToWindow()) {
944                 final int zen = (Integer) value;
945                 if (fromClick) {
946                     MetricsLogger.action(mContext, MetricsEvent.QS_DND_ZEN_SELECT, zen);
947                 }
948                 if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + zen);
949                 final Uri realConditionId = getRealConditionId(mSessionExitCondition);
950                 AsyncTask.execute(new Runnable() {
951                     @Override
952                     public void run() {
953                         mController.setZen(zen, realConditionId, TAG + ".selectZen");
954                         if (zen != Global.ZEN_MODE_OFF) {
955                             Prefs.putInt(mContext, Prefs.Key.DND_FAVORITE_ZEN, zen);
956                         }
957                     }
958                 });
959             }
960         }
961 
962         @Override
963         public void onInteraction() {
964             fireInteraction();
965         }
966     };
967 
968     private final Interaction.Callback mInteractionCallback = new Interaction.Callback() {
969         @Override
970         public void onInteraction() {
971             fireInteraction();
972         }
973     };
974 
975     private final class TransitionHelper implements TransitionListener, Runnable {
976         private final ArraySet<View> mTransitioningViews = new ArraySet<View>();
977 
978         private boolean mTransitioning;
979         private boolean mPendingUpdateWidgets;
980 
clear()981         public void clear() {
982             mTransitioningViews.clear();
983             mPendingUpdateWidgets = false;
984         }
985 
dump(FileDescriptor fd, PrintWriter pw, String[] args)986         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
987             pw.println("  TransitionHelper state:");
988             pw.print("    mPendingUpdateWidgets="); pw.println(mPendingUpdateWidgets);
989             pw.print("    mTransitioning="); pw.println(mTransitioning);
990             pw.print("    mTransitioningViews="); pw.println(mTransitioningViews);
991         }
992 
pendingUpdateWidgets()993         public void pendingUpdateWidgets() {
994             mPendingUpdateWidgets = true;
995         }
996 
isTransitioning()997         public boolean isTransitioning() {
998             return !mTransitioningViews.isEmpty();
999         }
1000 
1001         @Override
startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType)1002         public void startTransition(LayoutTransition transition,
1003                 ViewGroup container, View view, int transitionType) {
1004             mTransitioningViews.add(view);
1005             updateTransitioning();
1006         }
1007 
1008         @Override
endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType)1009         public void endTransition(LayoutTransition transition,
1010                 ViewGroup container, View view, int transitionType) {
1011             mTransitioningViews.remove(view);
1012             updateTransitioning();
1013         }
1014 
1015         @Override
run()1016         public void run() {
1017             if (DEBUG) Log.d(mTag, "TransitionHelper run"
1018                     + " mPendingUpdateWidgets=" + mPendingUpdateWidgets);
1019             if (mPendingUpdateWidgets) {
1020                 updateWidgets();
1021             }
1022             mPendingUpdateWidgets = false;
1023         }
1024 
updateTransitioning()1025         private void updateTransitioning() {
1026             final boolean transitioning = isTransitioning();
1027             if (mTransitioning == transitioning) return;
1028             mTransitioning = transitioning;
1029             if (DEBUG) Log.d(mTag, "TransitionHelper mTransitioning=" + mTransitioning);
1030             if (!mTransitioning) {
1031                 if (mPendingUpdateWidgets) {
1032                     mHandler.post(this);
1033                 } else {
1034                     mPendingUpdateWidgets = false;
1035                 }
1036             }
1037         }
1038     }
1039 }
1040