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