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