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