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