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