Home | History | Annotate | Download | only in volume
      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 
    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 
    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
    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 
    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
    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
    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 
    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 
    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 
    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 
    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 */
    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 
    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 
    318     public void updateLocale() {
    319         mZenButtons.updateLocale();
    320     }
    321 
    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 
    330     private static Uri getConditionId(Condition condition) {
    331         return condition != null ? condition.id : null;
    332     }
    333 
    334     private static boolean sameConditionId(Condition lhs, Condition rhs) {
    335         return lhs == null ? rhs == null : rhs != null && lhs.id.equals(rhs.id);
    336     }
    337 
    338     private static Condition copy(Condition condition) {
    339         return condition == null ? null : condition.copy();
    340     }
    341 
    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 
    353     public void setCallback(Callback callback) {
    354         mCallback = callback;
    355     }
    356 
    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 
    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 
    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 
    391     private int getSelectedZen(int defValue) {
    392         final Object zen = mZenButtons.getSelectedValue();
    393         return zen != null ? (Integer) zen : defValue;
    394     }
    395 
    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 
    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 
    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 
    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 
    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 
    497     private Condition forever() {
    498         return new Condition(mForeverId, foreverSummary(), "", "", 0 /*icon*/, Condition.STATE_TRUE,
    499                 0 /*flags*/);
    500     }
    501 
    502     private String foreverSummary() {
    503         return mContext.getString(com.android.internal.R.string.zen_mode_forever);
    504     }
    505 
    506     private ConditionTag getConditionTagAt(int index) {
    507         return (ConditionTag) mZenConditions.getChildAt(index).getTag();
    508     }
    509 
    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 
    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 
    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 
    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 
    566     private boolean isCountdown(Condition c) {
    567         return c != null && ZenModeConfig.isValidCountdownConditionId(c.id);
    568     }
    569 
    570     private boolean isForever(Condition c) {
    571         return c != null && mForeverId.equals(c.id);
    572     }
    573 
    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 
    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 
    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 
    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 
    763     private void fireMoreSettings() {
    764         if (mCallback != null) {
    765             mCallback.onMoreSettings();
    766         }
    767     }
    768 
    769     private void fireInteraction() {
    770         if (mCallback != null) {
    771             mCallback.onInteraction();
    772         }
    773     }
    774 
    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 
    802         private H() {
    803             super(Looper.getMainLooper());
    804         }
    805 
    806         @Override
    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 {
    819         void onMoreSettings();
    820         void onInteraction();
    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 
    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 
    850         public boolean isNoneDangerous() {
    851             return mNoneSelected < mNoneDangerousThreshold;
    852         }
    853 
    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 
    861         public int getMinuteIndex() {
    862             return mMinuteIndex;
    863         }
    864 
    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
    874         public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    875             updateMinuteIndex();
    876             updateNoneSelected();
    877         }
    878 
    879         private SharedPreferences prefs() {
    880             return mContext.getSharedPreferences(mContext.getPackageName(), 0);
    881         }
    882 
    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 
    888         private int clampIndex(int index) {
    889             return MathUtils.constrain(index, -1, MINUTE_BUCKETS.length - 1);
    890         }
    891 
    892         private void updateNoneSelected() {
    893             mNoneSelected = clampNoneSelected(prefs().getInt(KEY_NONE_SELECTED, 0));
    894             if (DEBUG) Log.d(mTag, "None selected: " + mNoneSelected);
    895         }
    896 
    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 
    936         public void clear() {
    937             mTransitioningViews.clear();
    938             mPendingUpdateConditions = mPendingUpdateWidgets = false;
    939         }
    940 
    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 
    949         public void pendingUpdateConditions() {
    950             mPendingUpdateConditions = true;
    951         }
    952 
    953         public void pendingUpdateWidgets() {
    954             mPendingUpdateWidgets = true;
    955         }
    956 
    957         public boolean isTransitioning() {
    958             return !mTransitioningViews.isEmpty();
    959         }
    960 
    961         @Override
    962         public void startTransition(LayoutTransition transition,
    963                 ViewGroup container, View view, int transitionType) {
    964             mTransitioningViews.add(view);
    965             updateTransitioning();
    966         }
    967 
    968         @Override
    969         public void endTransition(LayoutTransition transition,
    970                 ViewGroup container, View view, int transitionType) {
    971             mTransitioningViews.remove(view);
    972             updateTransitioning();
    973         }
    974 
    975         @Override
    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 
    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