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