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