ZenModePanel.java revision 8263c3e0647e0d9622585f1c196ceb8d2fde695e
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 17package com.android.systemui.volume; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.content.Context; 22import android.content.Intent; 23import android.content.SharedPreferences; 24import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 25import android.net.Uri; 26import android.os.Handler; 27import android.os.Looper; 28import android.os.Message; 29import android.provider.Settings; 30import android.provider.Settings.Global; 31import android.service.notification.Condition; 32import android.service.notification.ZenModeConfig; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.view.ContextThemeWrapper; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.animation.AnimationUtils; 39import android.view.animation.Interpolator; 40import android.widget.CompoundButton; 41import android.widget.CompoundButton.OnCheckedChangeListener; 42import android.widget.ImageView; 43import android.widget.LinearLayout; 44import android.widget.RadioButton; 45import android.widget.TextView; 46 47import com.android.systemui.R; 48import com.android.systemui.statusbar.policy.ZenModeController; 49 50import java.util.Arrays; 51import java.util.Objects; 52 53public class ZenModePanel extends LinearLayout { 54 private static final String TAG = "ZenModePanel"; 55 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 56 57 private static final int[] MINUTE_BUCKETS = DEBUG 58 ? new int[] { 1, 2, 5, 15, 30, 45, 60, 120, 180, 240, 480 } 59 : new int[] { 15, 30, 45, 60, 120, 180, 240, 480 }; 60 private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0]; 61 private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1]; 62 private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60); 63 private static final int FOREVER_CONDITION_INDEX = 0; 64 private static final int TIME_CONDITION_INDEX = 1; 65 private static final int FIRST_CONDITION_INDEX = 2; 66 private static final float SILENT_HINT_PULSE_SCALE = 1.1f; 67 68 private static final int SECONDS_MS = 1000; 69 private static final int MINUTES_MS = 60 * SECONDS_MS; 70 71 public static final Intent ZEN_SETTINGS = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); 72 73 private final Context mContext; 74 private final LayoutInflater mInflater; 75 private final H mHandler = new H(); 76 private final Favorites mFavorites; 77 private final Interpolator mFastOutSlowInInterpolator; 78 79 private char mLogTag = '?'; 80 private String mTag; 81 82 private SegmentedButtons mZenButtons; 83 private View mZenSubhead; 84 private TextView mZenSubheadCollapsed; 85 private TextView mZenSubheadExpanded; 86 private View mMoreSettings; 87 private LinearLayout mZenConditions; 88 89 private Callback mCallback; 90 private ZenModeController mController; 91 private boolean mRequestingConditions; 92 private Uri mExitConditionId; 93 private int mBucketIndex = -1; 94 private boolean mExpanded; 95 private int mAttachedZen; 96 private String mExitConditionText; 97 98 public ZenModePanel(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 mContext = context; 101 mFavorites = new Favorites(); 102 mInflater = LayoutInflater.from(mContext.getApplicationContext()); 103 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, 104 android.R.interpolator.fast_out_slow_in); 105 updateTag(); 106 if (DEBUG) Log.d(mTag, "new ZenModePanel"); 107 } 108 109 private void updateTag() { 110 mTag = TAG + "/" + mLogTag + "/" + Integer.toHexString(System.identityHashCode(this)); 111 } 112 113 @Override 114 protected void onFinishInflate() { 115 super.onFinishInflate(); 116 117 mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons); 118 mZenButtons.addButton(R.string.interruption_level_none, Global.ZEN_MODE_NO_INTERRUPTIONS); 119 mZenButtons.addButton(R.string.interruption_level_priority, 120 Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); 121 mZenButtons.addButton(R.string.interruption_level_all, Global.ZEN_MODE_OFF); 122 mZenButtons.setCallback(mZenButtonsCallback); 123 124 mZenSubhead = findViewById(R.id.zen_subhead); 125 126 mZenSubheadCollapsed = (TextView) findViewById(R.id.zen_subhead_collapsed); 127 mZenSubheadCollapsed.setOnClickListener(new View.OnClickListener() { 128 @Override 129 public void onClick(View v) { 130 setExpanded(true); 131 fireInteraction(); 132 } 133 }); 134 135 mZenSubheadExpanded = (TextView) findViewById(R.id.zen_subhead_expanded); 136 137 mMoreSettings = findViewById(R.id.zen_more_settings); 138 mMoreSettings.setOnClickListener(new View.OnClickListener() { 139 @Override 140 public void onClick(View v) { 141 fireMoreSettings(); 142 fireInteraction(); 143 } 144 }); 145 146 mZenConditions = (LinearLayout) findViewById(R.id.zen_conditions); 147 } 148 149 @Override 150 protected void onAttachedToWindow() { 151 super.onAttachedToWindow(); 152 if (DEBUG) Log.d(mTag, "onAttachedToWindow"); 153 mAttachedZen = getSelectedZen(-1); 154 refreshExitConditionText(); 155 } 156 157 @Override 158 protected void onDetachedFromWindow() { 159 super.onDetachedFromWindow(); 160 if (DEBUG) Log.d(mTag, "onDetachedFromWindow"); 161 mAttachedZen = -1; 162 setExpanded(false); 163 } 164 165 private void setExpanded(boolean expanded) { 166 if (expanded == mExpanded) return; 167 mExpanded = expanded; 168 updateWidgets(); 169 setRequestingConditions(mExpanded); 170 fireExpanded(); 171 } 172 173 /** Start or stop requesting relevant zen mode exit conditions */ 174 private void setRequestingConditions(boolean requesting) { 175 if (mRequestingConditions == requesting) return; 176 if (DEBUG) Log.d(mTag, "setRequestingConditions " + requesting); 177 mRequestingConditions = requesting; 178 if (mController != null) { 179 mController.requestConditions(mRequestingConditions); 180 } 181 if (mRequestingConditions) { 182 Condition timeCondition = parseExistingTimeCondition(mExitConditionId); 183 if (timeCondition != null) { 184 mBucketIndex = -1; 185 } else { 186 mBucketIndex = DEFAULT_BUCKET_INDEX; 187 timeCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]); 188 } 189 if (DEBUG) Log.d(mTag, "Initial bucket index: " + mBucketIndex); 190 handleUpdateConditions(new Condition[0]); // ensures forever exists 191 bind(timeCondition, mZenConditions.getChildAt(TIME_CONDITION_INDEX)); 192 checkForDefault(); 193 } else { 194 mZenConditions.removeAllViews(); 195 } 196 } 197 198 public void init(ZenModeController controller, char logTag) { 199 mController = controller; 200 mLogTag = logTag; 201 updateTag(); 202 setExitConditionId(mController.getExitConditionId()); 203 refreshExitConditionText(); 204 mAttachedZen = getSelectedZen(-1); 205 handleUpdateZen(mController.getZen()); 206 if (DEBUG) Log.d(mTag, "init mExitConditionId=" + mExitConditionId); 207 mZenConditions.removeAllViews(); 208 mController.addCallback(mZenCallback); 209 } 210 211 private void setExitConditionId(Uri exitConditionId) { 212 if (Objects.equals(mExitConditionId, exitConditionId)) return; 213 mExitConditionId = exitConditionId; 214 refreshExitConditionText(); 215 } 216 217 private void refreshExitConditionText() { 218 if (mExitConditionId == null) { 219 mExitConditionText = mContext.getString(R.string.zen_mode_forever); 220 } else if (ZenModeConfig.isValidCountdownConditionId(mExitConditionId)) { 221 mExitConditionText = parseExistingTimeCondition(mExitConditionId).summary; 222 } else { 223 mExitConditionText = "(until condition ends)"; // TODO persist current description 224 } 225 } 226 227 public void setCallback(Callback callback) { 228 mCallback = callback; 229 } 230 231 public void showSilentHint() { 232 if (DEBUG) Log.d(mTag, "showSilentHint"); 233 if (mZenButtons == null || mZenButtons.getChildCount() == 0) return; 234 final View noneButton = mZenButtons.getChildAt(0); 235 if (noneButton.getScaleX() != 1) return; // already running 236 noneButton.animate().cancel(); 237 noneButton.animate().scaleX(SILENT_HINT_PULSE_SCALE).scaleY(SILENT_HINT_PULSE_SCALE) 238 .setInterpolator(mFastOutSlowInInterpolator) 239 .setListener(new AnimatorListenerAdapter() { 240 @Override 241 public void onAnimationEnd(Animator animation) { 242 noneButton.animate().scaleX(1).scaleY(1).setListener(null); 243 } 244 }); 245 } 246 247 private void handleUpdateZen(int zen) { 248 if (mAttachedZen != -1 && mAttachedZen != zen) { 249 setExpanded(zen != Global.ZEN_MODE_OFF); 250 mAttachedZen = zen; 251 } 252 mZenButtons.setSelectedValue(zen); 253 updateWidgets(); 254 } 255 256 private int getSelectedZen(int defValue) { 257 final Object zen = mZenButtons.getSelectedValue(); 258 return zen != null ? (Integer) zen : defValue; 259 } 260 261 private void updateWidgets() { 262 final int zen = getSelectedZen(Global.ZEN_MODE_OFF); 263 final boolean zenOff = zen == Global.ZEN_MODE_OFF; 264 final boolean zenImportant = zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; 265 final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS; 266 final boolean foreverSelected = mExitConditionId == null; 267 268 mZenSubhead.setVisibility(!zenOff && (mExpanded || !foreverSelected) ? VISIBLE : GONE); 269 mZenSubheadExpanded.setVisibility(mExpanded ? VISIBLE : GONE); 270 mZenSubheadCollapsed.setVisibility(!mExpanded ? VISIBLE : GONE); 271 mMoreSettings.setVisibility(zenImportant && mExpanded ? VISIBLE : GONE); 272 mZenConditions.setVisibility(!zenOff && mExpanded ? VISIBLE : GONE); 273 274 if (zenNone) { 275 mZenSubheadExpanded.setText(R.string.zen_no_interruptions_with_warning); 276 mZenSubheadCollapsed.setText(mExitConditionText); 277 } else if (zenImportant) { 278 mZenSubheadExpanded.setText(R.string.zen_important_interruptions); 279 mZenSubheadCollapsed.setText(mExitConditionText); 280 } 281 } 282 283 private Condition parseExistingTimeCondition(Uri conditionId) { 284 final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId); 285 if (time == 0) return null; 286 final long span = time - System.currentTimeMillis(); 287 if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null; 288 return timeCondition(time, Math.round(span / (float)MINUTES_MS)); 289 } 290 291 private Condition newTimeCondition(int minutesFromNow) { 292 final long now = System.currentTimeMillis(); 293 return timeCondition(now + minutesFromNow * MINUTES_MS, minutesFromNow); 294 } 295 296 private Condition timeCondition(long time, int minutes) { 297 final int num = minutes < 60 ? minutes : Math.round(minutes / 60f); 298 final int resId = minutes < 60 299 ? R.plurals.zen_mode_duration_minutes 300 : R.plurals.zen_mode_duration_hours; 301 final String caption = mContext.getResources().getQuantityString(resId, num, num); 302 final Uri id = ZenModeConfig.toCountdownConditionId(time); 303 return new Condition(id, caption, "", "", 0, Condition.STATE_TRUE, 304 Condition.FLAG_RELEVANT_NOW); 305 } 306 307 private void handleUpdateConditions(Condition[] conditions) { 308 final int newCount = conditions == null ? 0 : conditions.length; 309 if (DEBUG) Log.d(mTag, "handleUpdateConditions newCount=" + newCount); 310 for (int i = mZenConditions.getChildCount(); i >= newCount + FIRST_CONDITION_INDEX; i--) { 311 mZenConditions.removeViewAt(i); 312 } 313 bind(null, mZenConditions.getChildAt(FOREVER_CONDITION_INDEX)); 314 for (int i = 0; i < newCount; i++) { 315 bind(conditions[i], mZenConditions.getChildAt(FIRST_CONDITION_INDEX + i)); 316 } 317 } 318 319 private ConditionTag getConditionTagAt(int index) { 320 return (ConditionTag) mZenConditions.getChildAt(index).getTag(); 321 } 322 323 private void checkForDefault() { 324 // are we left without anything selected? if so, set a default 325 for (int i = 0; i < mZenConditions.getChildCount(); i++) { 326 if (getConditionTagAt(i).rb.isChecked()) { 327 return; 328 } 329 } 330 if (DEBUG) Log.d(mTag, "Selecting a default"); 331 final int favoriteIndex = mFavorites.getMinuteIndex(); 332 if (favoriteIndex == -1) { 333 getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true); 334 } else { 335 final Condition c = newTimeCondition(MINUTE_BUCKETS[favoriteIndex]); 336 mBucketIndex = favoriteIndex; 337 bind(c, mZenConditions.getChildAt(TIME_CONDITION_INDEX)); 338 getConditionTagAt(TIME_CONDITION_INDEX).rb.setChecked(true); 339 } 340 } 341 342 private void handleExitConditionChanged(Uri exitCondition) { 343 setExitConditionId(exitCondition); 344 if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitConditionId); 345 final int N = mZenConditions.getChildCount(); 346 for (int i = 0; i < N; i++) { 347 final ConditionTag tag = getConditionTagAt(i); 348 tag.rb.setChecked(Objects.equals(tag.conditionId, exitCondition)); 349 } 350 } 351 352 private void bind(final Condition condition, View convertView) { 353 final boolean enabled = condition == null || condition.state == Condition.STATE_TRUE; 354 final View row; 355 if (convertView == null) { 356 row = mInflater.inflate(R.layout.zen_mode_condition, this, false); 357 if (DEBUG) Log.d(mTag, "Adding new condition view for: " + condition); 358 mZenConditions.addView(row); 359 } else { 360 row = convertView; 361 } 362 final ConditionTag tag = 363 row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag(); 364 row.setTag(tag); 365 if (tag.rb == null) { 366 tag.rb = (RadioButton) row.findViewById(android.R.id.checkbox); 367 } 368 tag.conditionId = condition != null ? condition.id : null; 369 tag.rb.setEnabled(enabled); 370 tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() { 371 @Override 372 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 373 if (mExpanded && isChecked) { 374 if (DEBUG) Log.d(mTag, "onCheckedChanged " + tag.conditionId); 375 final int N = mZenConditions.getChildCount(); 376 for (int i = 0; i < N; i++) { 377 ConditionTag childTag = getConditionTagAt(i); 378 if (childTag == tag) continue; 379 childTag.rb.setChecked(false); 380 } 381 select(tag.conditionId); 382 fireInteraction(); 383 } 384 } 385 }); 386 final TextView title = (TextView) row.findViewById(android.R.id.title); 387 if (condition == null) { 388 title.setText(R.string.zen_mode_forever); 389 } else { 390 title.setText(condition.summary); 391 } 392 title.setEnabled(enabled); 393 title.setAlpha(enabled ? 1 : .4f); 394 final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1); 395 button1.setOnClickListener(new OnClickListener() { 396 @Override 397 public void onClick(View v) { 398 onClickTimeButton(row, tag, false /*down*/); 399 } 400 }); 401 402 final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2); 403 button2.setOnClickListener(new OnClickListener() { 404 @Override 405 public void onClick(View v) { 406 onClickTimeButton(row, tag, true /*up*/); 407 } 408 }); 409 title.setOnClickListener(new OnClickListener() { 410 @Override 411 public void onClick(View v) { 412 tag.rb.setChecked(true); 413 fireInteraction(); 414 } 415 }); 416 417 final long time = ZenModeConfig.tryParseCountdownConditionId(tag.conditionId); 418 if (time > 0) { 419 if (mBucketIndex > -1) { 420 button1.setEnabled(mBucketIndex > 0); 421 button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1); 422 } else { 423 final long span = time - System.currentTimeMillis(); 424 button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS); 425 final Condition maxCondition = newTimeCondition(MAX_BUCKET_MINUTES); 426 button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary)); 427 } 428 429 button1.setAlpha(button1.isEnabled() ? 1f : .5f); 430 button2.setAlpha(button2.isEnabled() ? 1f : .5f); 431 } else { 432 button1.setVisibility(View.GONE); 433 button2.setVisibility(View.GONE); 434 } 435 } 436 437 private void onClickTimeButton(View row, ConditionTag tag, boolean up) { 438 Condition newCondition = null; 439 final int N = MINUTE_BUCKETS.length; 440 if (mBucketIndex == -1) { 441 // not on a known index, search for the next or prev bucket by time 442 final long time = ZenModeConfig.tryParseCountdownConditionId(tag.conditionId); 443 final long now = System.currentTimeMillis(); 444 for (int i = 0; i < N; i++) { 445 int j = up ? i : N - 1 - i; 446 final int bucketMinutes = MINUTE_BUCKETS[j]; 447 final long bucketTime = now + bucketMinutes * MINUTES_MS; 448 if (up && bucketTime > time || !up && bucketTime < time) { 449 mBucketIndex = j; 450 newCondition = timeCondition(bucketTime, bucketMinutes); 451 break; 452 } 453 } 454 if (newCondition == null) { 455 mBucketIndex = DEFAULT_BUCKET_INDEX; 456 newCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]); 457 } 458 } else { 459 // on a known index, simply increment or decrement 460 mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1))); 461 newCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]); 462 } 463 bind(newCondition, row); 464 tag.rb.setChecked(true); 465 select(newCondition.id); 466 fireInteraction(); 467 } 468 469 private void select(Uri conditionId) { 470 if (DEBUG) Log.d(mTag, "select " + conditionId); 471 if (mController != null) { 472 mController.setExitConditionId(conditionId); 473 } 474 setExitConditionId(conditionId); 475 if (conditionId == null) { 476 mFavorites.setMinuteIndex(-1); 477 } else if (ZenModeConfig.isValidCountdownConditionId(conditionId) && mBucketIndex != -1) { 478 mFavorites.setMinuteIndex(mBucketIndex); 479 } 480 } 481 482 private void fireMoreSettings() { 483 if (mCallback != null) { 484 mCallback.onMoreSettings(); 485 } 486 } 487 488 private void fireInteraction() { 489 if (mCallback != null) { 490 mCallback.onInteraction(); 491 } 492 } 493 494 private void fireExpanded() { 495 if (mCallback != null) { 496 mCallback.onExpanded(mExpanded); 497 } 498 } 499 500 private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() { 501 @Override 502 public void onZenChanged(int zen) { 503 mHandler.obtainMessage(H.UPDATE_ZEN, zen, 0).sendToTarget(); 504 } 505 @Override 506 public void onConditionsChanged(Condition[] conditions) { 507 mHandler.obtainMessage(H.UPDATE_CONDITIONS, conditions).sendToTarget(); 508 } 509 510 @Override 511 public void onExitConditionChanged(Uri exitConditionId) { 512 mHandler.obtainMessage(H.EXIT_CONDITION_CHANGED, exitConditionId).sendToTarget(); 513 } 514 }; 515 516 private final class H extends Handler { 517 private static final int UPDATE_CONDITIONS = 1; 518 private static final int EXIT_CONDITION_CHANGED = 2; 519 private static final int UPDATE_ZEN = 3; 520 521 private H() { 522 super(Looper.getMainLooper()); 523 } 524 525 @Override 526 public void handleMessage(Message msg) { 527 if (msg.what == UPDATE_CONDITIONS) { 528 handleUpdateConditions((Condition[])msg.obj); 529 checkForDefault(); 530 } else if (msg.what == EXIT_CONDITION_CHANGED) { 531 handleExitConditionChanged((Uri)msg.obj); 532 } else if (msg.what == UPDATE_ZEN) { 533 handleUpdateZen(msg.arg1); 534 } 535 } 536 } 537 538 public interface Callback { 539 void onMoreSettings(); 540 void onInteraction(); 541 void onExpanded(boolean expanded); 542 } 543 544 // used as the view tag on condition rows 545 private static class ConditionTag { 546 RadioButton rb; 547 Uri conditionId; 548 } 549 550 private final class Favorites implements OnSharedPreferenceChangeListener { 551 private static final String KEY_MINUTE_INDEX = "minuteIndex"; 552 553 private int mMinuteIndex; 554 555 private Favorites() { 556 prefs().registerOnSharedPreferenceChangeListener(this); 557 updateMinuteIndex(); 558 } 559 560 public int getMinuteIndex() { 561 return mMinuteIndex; 562 } 563 564 public void setMinuteIndex(int minuteIndex) { 565 minuteIndex = clamp(minuteIndex); 566 if (minuteIndex == mMinuteIndex) return; 567 mMinuteIndex = clamp(minuteIndex); 568 if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex); 569 prefs().edit().putInt(KEY_MINUTE_INDEX, mMinuteIndex).apply(); 570 } 571 572 @Override 573 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 574 updateMinuteIndex(); 575 } 576 577 private SharedPreferences prefs() { 578 return mContext.getSharedPreferences(ZenModePanel.class.getSimpleName(), 0); 579 } 580 581 private void updateMinuteIndex() { 582 mMinuteIndex = clamp(prefs().getInt(KEY_MINUTE_INDEX, DEFAULT_BUCKET_INDEX)); 583 if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex); 584 } 585 586 private int clamp(int index) { 587 return Math.max(-1, Math.min(MINUTE_BUCKETS.length - 1, index)); 588 } 589 } 590 591 private final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() { 592 @Override 593 public void onSelected(Object value) { 594 if (value != null && mZenButtons.isShown()) { 595 if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + value); 596 mController.setZen((Integer) value); 597 mController.setExitConditionId(null); 598 } 599 } 600 }; 601} 602