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