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