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