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