HeadsUpManager.java revision 31d9ef7a402b58b10758da1d71ff5e2181abe8a4
1/* 2 * Copyright (C) 2011 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.statusbar.policy; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.database.ContentObserver; 22import android.os.Handler; 23import android.os.SystemClock; 24import android.provider.Settings; 25import android.util.ArrayMap; 26import android.util.Log; 27import android.util.Pools; 28import android.view.ViewTreeObserver; 29import android.view.accessibility.AccessibilityEvent; 30 31import com.android.systemui.R; 32import com.android.systemui.statusbar.ExpandableNotificationRow; 33import com.android.systemui.statusbar.NotificationData; 34import com.android.systemui.statusbar.phone.PhoneStatusBar; 35 36import java.io.FileDescriptor; 37import java.io.PrintWriter; 38import java.util.HashMap; 39import java.util.HashSet; 40import java.util.Stack; 41import java.util.TreeSet; 42 43public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener { 44 private static final String TAG = "HeadsUpManager"; 45 private static final boolean DEBUG = false; 46 private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; 47 48 private final int mHeadsUpNotificationDecay; 49 private final int mMinimumDisplayTime; 50 51 private final int mTouchSensitivityDelay; 52 private final ArrayMap<String, Long> mSnoozedPackages; 53 private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>(); 54 private final int mDefaultSnoozeLengthMs; 55 private final Handler mHandler = new Handler(); 56 private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() { 57 58 private Stack<HeadsUpEntry> mPoolObjects = new Stack<>(); 59 60 @Override 61 public HeadsUpEntry acquire() { 62 if (!mPoolObjects.isEmpty()) { 63 return mPoolObjects.pop(); 64 } 65 return new HeadsUpEntry(); 66 } 67 68 @Override 69 public boolean release(HeadsUpEntry instance) { 70 instance.removeAutoCancelCallbacks(); 71 mPoolObjects.push(instance); 72 return true; 73 } 74 }; 75 76 77 private PhoneStatusBar mBar; 78 private int mSnoozeLengthMs; 79 private ContentObserver mSettingsObserver; 80 private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>(); 81 private TreeSet<HeadsUpEntry> mSortedEntries = new TreeSet<>(); 82 private HashSet<String> mSwipedOutKeys = new HashSet<>(); 83 private int mUser; 84 private Clock mClock; 85 private boolean mReleaseOnExpandFinish; 86 private boolean mTrackingHeadsUp; 87 private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>(); 88 private boolean mIsExpanded; 89 private boolean mHasPinnedHeadsUp; 90 private int[] mTmpTwoArray = new int[2]; 91 92 public HeadsUpManager(final Context context, ViewTreeObserver observer) { 93 Resources resources = context.getResources(); 94 mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay); 95 if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay); 96 mSnoozedPackages = new ArrayMap<>(); 97 mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms); 98 mSnoozeLengthMs = mDefaultSnoozeLengthMs; 99 mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time); 100 mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay); 101 mClock = new Clock(); 102 103 mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(), 104 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs); 105 mSettingsObserver = new ContentObserver(mHandler) { 106 @Override 107 public void onChange(boolean selfChange) { 108 final int packageSnoozeLengthMs = Settings.Global.getInt( 109 context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); 110 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { 111 mSnoozeLengthMs = packageSnoozeLengthMs; 112 if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); 113 } 114 } 115 }; 116 context.getContentResolver().registerContentObserver( 117 Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, 118 mSettingsObserver); 119 if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); 120 observer.addOnComputeInternalInsetsListener(this); 121 } 122 123 public void setBar(PhoneStatusBar bar) { 124 mBar = bar; 125 } 126 127 public void addListener(OnHeadsUpChangedListener listener) { 128 mListeners.add(listener); 129 } 130 131 public PhoneStatusBar getBar() { 132 return mBar; 133 } 134 135 /** 136 * Called when posting a new notification to the heads up. 137 */ 138 public void showNotification(NotificationData.Entry headsUp) { 139 if (DEBUG) Log.v(TAG, "showNotification"); 140 addHeadsUpEntry(headsUp); 141 updateNotification(headsUp, true); 142 headsUp.setInterruption(); 143 } 144 145 /** 146 * Called when updating or posting a notification to the heads up. 147 */ 148 public void updateNotification(NotificationData.Entry headsUp, boolean alert) { 149 if (DEBUG) Log.v(TAG, "updateNotification"); 150 151 headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */); 152 headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 153 154 if (alert) { 155 HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key); 156 headsUpEntry.updateEntry(); 157 setEntryToShade(headsUpEntry, mIsExpanded, false /* justAdded */, false); 158 } 159 } 160 161 private void addHeadsUpEntry(NotificationData.Entry entry) { 162 HeadsUpEntry headsUpEntry = mEntryPool.acquire(); 163 164 // This will also add the entry to the sortedList 165 headsUpEntry.setEntry(entry); 166 mHeadsUpEntries.put(entry.key, headsUpEntry); 167 entry.row.setHeadsUp(true); 168 setEntryToShade(headsUpEntry, mIsExpanded /* inShade */, true /* justAdded */, false); 169 for (OnHeadsUpChangedListener listener : mListeners) { 170 listener.OnHeadsUpStateChanged(entry, true); 171 } 172 entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 173 } 174 175 private void setEntryToShade(HeadsUpEntry headsUpEntry, boolean inShade, boolean justAdded, 176 boolean forceImmediate) { 177 ExpandableNotificationRow row = headsUpEntry.entry.row; 178 if (row.isInShade() != inShade || justAdded) { 179 row.setInShade(inShade); 180 if (!justAdded || !inShade) { 181 updatePinnedHeadsUpState(forceImmediate); 182 for (OnHeadsUpChangedListener listener : mListeners) { 183 listener.OnHeadsUpPinnedChanged(row, !inShade); 184 } 185 } 186 } 187 } 188 189 private void removeHeadsUpEntry(NotificationData.Entry entry) { 190 HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key); 191 mSortedEntries.remove(remove); 192 mEntryPool.release(remove); 193 entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 194 entry.row.setHeadsUp(false); 195 setEntryToShade(remove, true /* inShade */, false /* justAdded */, 196 false /* forceImmediate */); 197 for (OnHeadsUpChangedListener listener : mListeners) { 198 listener.OnHeadsUpStateChanged(entry, false); 199 } 200 } 201 202 private void updatePinnedHeadsUpState(boolean forceImmediate) { 203 boolean hasPinnedHeadsUp = hasPinnedHeadsUpInternal(); 204 if (hasPinnedHeadsUp == mHasPinnedHeadsUp) { 205 return; 206 } 207 mHasPinnedHeadsUp = hasPinnedHeadsUp; 208 for (OnHeadsUpChangedListener listener :mListeners) { 209 listener.OnPinnedHeadsUpExistChanged(hasPinnedHeadsUp, forceImmediate); 210 } 211 } 212 213 /** 214 * React to the removal of the notification in the heads up. 215 * 216 * @return true if the notification was removed and false if it still needs to be kept around 217 * for a bit since it wasn't shown long enough 218 */ 219 public boolean removeNotification(String key) { 220 if (DEBUG) Log.v(TAG, "remove"); 221 if (wasShownLongEnough(key)) { 222 releaseImmediately(key); 223 return true; 224 } else { 225 getHeadsUpEntry(key).hideAsSoonAsPossible(); 226 return false; 227 } 228 } 229 230 private boolean wasShownLongEnough(String key) { 231 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 232 HeadsUpEntry topEntry = getTopEntry(); 233 if (mSwipedOutKeys.contains(key)) { 234 // We always instantly dismiss views being manually swiped out. 235 mSwipedOutKeys.remove(key); 236 return true; 237 } 238 if (headsUpEntry != topEntry) { 239 return true; 240 } 241 return headsUpEntry.wasShownLongEnough(); 242 } 243 244 public boolean isHeadsUp(String key) { 245 return mHeadsUpEntries.containsKey(key); 246 } 247 248 249 /** 250 * Push any current Heads Up notification down into the shade. 251 */ 252 public void releaseAllImmediately() { 253 if (DEBUG) Log.v(TAG, "releaseAllImmediately"); 254 HashSet<String> keys = new HashSet<>(mHeadsUpEntries.keySet()); 255 for (String key: keys) { 256 releaseImmediately(key); 257 } 258 } 259 260 public void releaseImmediately(String key) { 261 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 262 if (headsUpEntry == null) { 263 return; 264 } 265 NotificationData.Entry shadeEntry = headsUpEntry.entry; 266 removeHeadsUpEntry(shadeEntry); 267 } 268 269 public boolean isSnoozed(String packageName) { 270 final String key = snoozeKey(packageName, mUser); 271 Long snoozedUntil = mSnoozedPackages.get(key); 272 if (snoozedUntil != null) { 273 if (snoozedUntil > SystemClock.elapsedRealtime()) { 274 if (DEBUG) Log.v(TAG, key + " snoozed"); 275 return true; 276 } 277 mSnoozedPackages.remove(packageName); 278 } 279 return false; 280 } 281 282 public void snooze() { 283 for (String key: mHeadsUpEntries.keySet()) { 284 HeadsUpEntry entry = mHeadsUpEntries.get(key); 285 String packageName = entry.entry.notification.getPackageName(); 286 mSnoozedPackages.put(snoozeKey(packageName, mUser), 287 SystemClock.elapsedRealtime() + mSnoozeLengthMs); 288 } 289 mReleaseOnExpandFinish = true; 290 } 291 292 private static String snoozeKey(String packageName, int user) { 293 return user + "," + packageName; 294 } 295 296 private HeadsUpEntry getHeadsUpEntry(String key) { 297 return mHeadsUpEntries.get(key); 298 } 299 300 public NotificationData.Entry getEntry(String key) { 301 return mHeadsUpEntries.get(key).entry; 302 } 303 304 public TreeSet<HeadsUpEntry> getSortedEntries() { 305 return mSortedEntries; 306 } 307 308 public HeadsUpEntry getTopEntry() { 309 return mSortedEntries.isEmpty() ? null : mSortedEntries.first(); 310 } 311 312 /** 313 * @param key the key of the touched notification 314 * @return whether the touch is valid and should not be discarded 315 */ 316 public boolean shouldSwallowClick(String key) { 317 if (mClock.currentTimeMillis() < mHeadsUpEntries.get(key).postTime) { 318 return true; 319 } 320 return false; 321 } 322 323 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { 324 if (!mIsExpanded && mHasPinnedHeadsUp) { 325 int minX = Integer.MAX_VALUE; 326 int maxX = 0; 327 int minY = Integer.MAX_VALUE; 328 int maxY = 0; 329 for (HeadsUpEntry entry: mSortedEntries) { 330 ExpandableNotificationRow row = entry.entry.row; 331 if (!row.isInShade()) { 332 row.getLocationOnScreen(mTmpTwoArray); 333 minX = Math.min(minX, mTmpTwoArray[0]); 334 minY = Math.min(minY, 0); 335 maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth()); 336 maxY = Math.max(maxY, row.getHeadsUpHeight()); 337 } 338 } 339 340 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 341 info.touchableRegion.set(minX, minY, maxX, maxY); 342 } 343 } 344 345 public void setUser(int user) { 346 mUser = user; 347 } 348 349 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 350 pw.println("HeadsUpManager state:"); 351 pw.print(" mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay); 352 pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); 353 pw.print(" now="); pw.println(SystemClock.elapsedRealtime()); 354 pw.print(" mUser="); pw.println(mUser); 355 for (HeadsUpEntry entry: mSortedEntries) { 356 pw.print(" HeadsUpEntry="); pw.println(entry.entry); 357 } 358 int N = mSnoozedPackages.size(); 359 pw.println(" snoozed packages: " + N); 360 for (int i = 0; i < N; i++) { 361 pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); 362 pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); 363 } 364 } 365 366 public boolean hasPinnedHeadsUp() { 367 return mHasPinnedHeadsUp; 368 } 369 370 private boolean hasPinnedHeadsUpInternal() { 371 for (String key: mHeadsUpEntries.keySet()) { 372 HeadsUpEntry entry = mHeadsUpEntries.get(key); 373 if (!entry.entry.row.isInShade()) { 374 return true; 375 } 376 } 377 return false; 378 } 379 380 public void addSwipedOutKey(String key) { 381 mSwipedOutKeys.add(key); 382 } 383 384 public float getHighestPinnedHeadsUp() { 385 float max = 0; 386 for (HeadsUpEntry entry: mSortedEntries) { 387 if (!entry.entry.row.isInShade()) { 388 max = Math.max(max, entry.entry.row.getActualHeight()); 389 } 390 } 391 return max; 392 } 393 394 public void releaseAllToShade() { 395 for (String key: mHeadsUpEntries.keySet()) { 396 HeadsUpEntry entry = mHeadsUpEntries.get(key); 397 setEntryToShade(entry, true /* toShade */, false /* justAdded */, 398 true /* forceImmediate */); 399 } 400 } 401 402 public void onExpandingFinished() { 403 if (mReleaseOnExpandFinish) { 404 releaseAllImmediately(); 405 mReleaseOnExpandFinish = false; 406 } else { 407 for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { 408 removeHeadsUpEntry(entry); 409 } 410 mEntriesToRemoveAfterExpand.clear(); 411 } 412 } 413 414 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 415 mTrackingHeadsUp = trackingHeadsUp; 416 } 417 418 public void setIsExpanded(boolean isExpanded) { 419 if (isExpanded != mIsExpanded) { 420 mIsExpanded = isExpanded; 421 if (isExpanded) { 422 releaseAllToShade(); 423 } 424 } 425 } 426 427 public int getTopHeadsUpHeight() { 428 HeadsUpEntry topEntry = getTopEntry(); 429 return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0; 430 } 431 432 public int compare(NotificationData.Entry a, NotificationData.Entry b) { 433 HeadsUpEntry aEntry = getHeadsUpEntry(a.key); 434 HeadsUpEntry bEntry = getHeadsUpEntry(b.key); 435 if (aEntry == null || bEntry == null) { 436 return aEntry == null ? 1 : -1; 437 } 438 return aEntry.compareTo(bEntry); 439 } 440 441 public class HeadsUpEntry implements Comparable<HeadsUpEntry> { 442 public NotificationData.Entry entry; 443 public long postTime; 444 public long earliestRemovaltime; 445 private Runnable mRemoveHeadsUpRunnable; 446 447 public void setEntry(final NotificationData.Entry entry) { 448 this.entry = entry; 449 450 // The actual post time will be just after the heads-up really slided in 451 postTime = mClock.currentTimeMillis() + mTouchSensitivityDelay; 452 mRemoveHeadsUpRunnable = new Runnable() { 453 @Override 454 public void run() { 455 if (!mTrackingHeadsUp) { 456 removeHeadsUpEntry(entry); 457 } else { 458 mEntriesToRemoveAfterExpand.add(entry); 459 } 460 } 461 }; 462 updateEntry(); 463 } 464 465 public void updateEntry() { 466 long currentTime = mClock.currentTimeMillis(); 467 earliestRemovaltime = currentTime + mMinimumDisplayTime; 468 postTime = Math.max(postTime, currentTime); 469 removeAutoCancelCallbacks(); 470 if (canEntryDecay()) { 471 long finishTime = postTime + mHeadsUpNotificationDecay; 472 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime); 473 mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay); 474 } 475 updateSortOrder(HeadsUpEntry.this); 476 } 477 478 private boolean canEntryDecay() { 479 return entry.notification.getNotification().fullScreenIntent == null; 480 } 481 482 @Override 483 public int compareTo(HeadsUpEntry o) { 484 return postTime < o.postTime ? 1 485 : postTime == o.postTime ? 0 486 : -1; 487 } 488 489 public void removeAutoCancelCallbacks() { 490 mHandler.removeCallbacks(mRemoveHeadsUpRunnable); 491 } 492 493 public boolean wasShownLongEnough() { 494 return earliestRemovaltime < mClock.currentTimeMillis(); 495 } 496 497 public void hideAsSoonAsPossible() { 498 removeAutoCancelCallbacks(); 499 mHandler.postDelayed(mRemoveHeadsUpRunnable, 500 earliestRemovaltime - mClock.currentTimeMillis()); 501 } 502 } 503 504 /** 505 * Update the sorted heads up order. 506 * 507 * @param headsUpEntry the headsUp that changed 508 */ 509 private void updateSortOrder(HeadsUpEntry headsUpEntry) { 510 mSortedEntries.remove(headsUpEntry); 511 mSortedEntries.add(headsUpEntry); 512 } 513 514 public static class Clock { 515 public long currentTimeMillis() { 516 return SystemClock.elapsedRealtime(); 517 } 518 } 519 520 public interface OnHeadsUpChangedListener { 521 void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly); 522 void OnHeadsUpPinnedChanged(ExpandableNotificationRow headsUp, boolean isHeadsUp); 523 void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp); 524 } 525} 526