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