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