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