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