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