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