HeadsUpManager.java revision a59ecc3401de0c4bf1e13665158f54669f22d06c
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 headsUpEntry.entry.row.setInShade(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 headsUpEntry.entry.row.setInShade(true); 172 } 173 updatePinnedHeadsUpState(false); 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 removeHeadsUpEntry(NotificationData.Entry entry) { 181 HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key); 182 mSortedEntries.remove(remove); 183 mEntryPool.release(remove); 184 entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 185 entry.row.setHeadsUp(false); 186 updatePinnedHeadsUpState(false); 187 for (OnHeadsUpChangedListener listener : mListeners) { 188 listener.OnHeadsUpStateChanged(entry, false); 189 } 190 } 191 192 private void updatePinnedHeadsUpState(boolean forceImmediate) { 193 boolean hasPinnedHeadsUp = hasPinnedHeadsUpInternal(); 194 if (hasPinnedHeadsUp == mHasPinnedHeadsUp) { 195 return; 196 } 197 mHasPinnedHeadsUp = hasPinnedHeadsUp; 198 for (OnHeadsUpChangedListener listener :mListeners) { 199 listener.OnPinnedHeadsUpExistChanged(hasPinnedHeadsUp, forceImmediate); 200 } 201 } 202 203 /** 204 * React to the removal of the notification in the heads up. 205 * 206 * @return true if the notification was removed and false if it still needs to be kept around 207 * for a bit since it wasn't shown long enough 208 */ 209 public boolean removeNotification(String key) { 210 if (DEBUG) Log.v(TAG, "remove"); 211 if (wasShownLongEnough(key)) { 212 releaseImmediately(key); 213 return true; 214 } else { 215 getHeadsUpEntry(key).hideAsSoonAsPossible(); 216 return false; 217 } 218 } 219 220 private boolean wasShownLongEnough(String key) { 221 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 222 HeadsUpEntry topEntry = getTopEntry(); 223 if (mSwipedOutKeys.contains(key)) { 224 // We always instantly dismiss views being manually swiped out. 225 mSwipedOutKeys.remove(key); 226 return true; 227 } 228 if (headsUpEntry != topEntry) { 229 return true; 230 } 231 return headsUpEntry.wasShownLongEnough(); 232 } 233 234 public boolean isHeadsUp(String key) { 235 return mHeadsUpEntries.containsKey(key); 236 } 237 238 239 /** 240 * Push any current Heads Up notification down into the shade. 241 */ 242 public void releaseAllImmediately() { 243 if (DEBUG) Log.v(TAG, "releaseAllImmediately"); 244 HashSet<String> keys = new HashSet<>(mHeadsUpEntries.keySet()); 245 for (String key: keys) { 246 releaseImmediately(key); 247 } 248 } 249 250 public void releaseImmediately(String key) { 251 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 252 if (headsUpEntry == null) { 253 return; 254 } 255 NotificationData.Entry shadeEntry = headsUpEntry.entry; 256 removeHeadsUpEntry(shadeEntry); 257 } 258 259 public boolean isSnoozed(String packageName) { 260 final String key = snoozeKey(packageName, mUser); 261 Long snoozedUntil = mSnoozedPackages.get(key); 262 if (snoozedUntil != null) { 263 if (snoozedUntil > SystemClock.elapsedRealtime()) { 264 if (DEBUG) Log.v(TAG, key + " snoozed"); 265 return true; 266 } 267 mSnoozedPackages.remove(packageName); 268 } 269 return false; 270 } 271 272 public void snooze() { 273 for (String key: mHeadsUpEntries.keySet()) { 274 HeadsUpEntry entry = mHeadsUpEntries.get(key); 275 String packageName = entry.entry.notification.getPackageName(); 276 mSnoozedPackages.put(snoozeKey(packageName, mUser), 277 SystemClock.elapsedRealtime() + mSnoozeLengthMs); 278 } 279 mReleaseOnExpandFinish = true; 280 } 281 282 private static String snoozeKey(String packageName, int user) { 283 return user + "," + packageName; 284 } 285 286 private HeadsUpEntry getHeadsUpEntry(String key) { 287 return mHeadsUpEntries.get(key); 288 } 289 290 public NotificationData.Entry getEntry(String key) { 291 return mHeadsUpEntries.get(key).entry; 292 } 293 294 public TreeSet<HeadsUpEntry> getSortedEntries() { 295 return mSortedEntries; 296 } 297 298 public HeadsUpEntry getTopEntry() { 299 return mSortedEntries.isEmpty() ? null : mSortedEntries.first(); 300 } 301 302 /** 303 * @param key the key of the touched notification 304 * @return whether the touch is valid and should not be discarded 305 */ 306 public boolean shouldSwallowClick(String key) { 307 if (mClock.currentTimeMillis() < mHeadsUpEntries.get(key).postTime) { 308 return true; 309 } 310 return false; 311 } 312 313 public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { 314 // TODO: handle the shadow 315 //getBackground().setAlpha((int) (255 * swipeProgress)); 316 return false; 317 } 318 319 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { 320 if (!mIsExpanded && mHasPinnedHeadsUp) { 321 int minX = Integer.MAX_VALUE; 322 int maxX = 0; 323 int minY = Integer.MAX_VALUE; 324 int maxY = 0; 325 for (HeadsUpEntry entry: mSortedEntries) { 326 ExpandableNotificationRow row = entry.entry.row; 327 if (!row.isInShade()) { 328 row.getLocationOnScreen(mTmpTwoArray); 329 minX = Math.min(minX, mTmpTwoArray[0]); 330 minY = Math.min(minY, 0); 331 maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth()); 332 maxY = Math.max(maxY, row.getHeadsUpHeight()); 333 } 334 } 335 336 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 337 info.touchableRegion.set(minX, minY, maxX, maxY); 338 } 339 } 340 341 public void setUser(int user) { 342 mUser = user; 343 } 344 345 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 346 pw.println("HeadsUpManager state:"); 347 pw.print(" mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay); 348 pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); 349 pw.print(" now="); pw.println(SystemClock.elapsedRealtime()); 350 pw.print(" mUser="); pw.println(mUser); 351 for (HeadsUpEntry entry: mSortedEntries) { 352 pw.print(" HeadsUpEntry="); pw.println(entry.entry); 353 } 354 int N = mSnoozedPackages.size(); 355 pw.println(" snoozed packages: " + N); 356 for (int i = 0; i < N; i++) { 357 pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); 358 pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); 359 } 360 } 361 362 public boolean hasPinnedHeadsUp() { 363 return mHasPinnedHeadsUp; 364 } 365 366 private boolean hasPinnedHeadsUpInternal() { 367 for (String key: mHeadsUpEntries.keySet()) { 368 HeadsUpEntry entry = mHeadsUpEntries.get(key); 369 if (!entry.entry.row.isInShade()) { 370 return true; 371 } 372 } 373 return false; 374 } 375 376 public void addSwipedOutKey(String key) { 377 mSwipedOutKeys.add(key); 378 } 379 380 public float getHighestPinnedHeadsUp() { 381 float max = 0; 382 for (HeadsUpEntry entry: mSortedEntries) { 383 if (!entry.entry.row.isInShade()) { 384 max = Math.max(max, entry.entry.row.getActualHeight()); 385 } 386 } 387 return max; 388 } 389 390 public void releaseAllToShade() { 391 for (String key: mHeadsUpEntries.keySet()) { 392 HeadsUpEntry entry = mHeadsUpEntries.get(key); 393 entry.entry.row.setInShade(true); 394 } 395 updatePinnedHeadsUpState(true); 396 } 397 398 public void onExpandingFinished() { 399 if (mReleaseOnExpandFinish) { 400 releaseAllImmediately(); 401 mReleaseOnExpandFinish = false; 402 } else { 403 for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { 404 removeHeadsUpEntry(entry); 405 } 406 mEntriesToRemoveAfterExpand.clear(); 407 } 408 } 409 410 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 411 mTrackingHeadsUp = trackingHeadsUp; 412 } 413 414 public void setIsExpanded(boolean isExpanded) { 415 mIsExpanded = isExpanded; 416 } 417 418 public int getTopHeadsUpHeight() { 419 HeadsUpEntry topEntry = getTopEntry(); 420 return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0; 421 } 422 423 public class HeadsUpEntry implements Comparable<HeadsUpEntry> { 424 public NotificationData.Entry entry; 425 public long postTime; 426 public long earliestRemovaltime; 427 private Runnable mRemoveHeadsUpRunnable; 428 429 public void setEntry(final NotificationData.Entry entry) { 430 this.entry = entry; 431 432 // The actual post time will be just after the heads-up really slided in 433 postTime = mClock.currentTimeMillis() + mTouchSensitivityDelay; 434 mRemoveHeadsUpRunnable = new Runnable() { 435 @Override 436 public void run() { 437 if (!mTrackingHeadsUp) { 438 removeHeadsUpEntry(entry); 439 } else { 440 mEntriesToRemoveAfterExpand.add(entry); 441 } 442 } 443 }; 444 updateEntry(); 445 } 446 447 public void updateEntry() { 448 long currentTime = mClock.currentTimeMillis(); 449 postTime = Math.max(postTime, currentTime); 450 long finishTime = postTime + mHeadsUpNotificationDecay; 451 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime); 452 earliestRemovaltime = currentTime + mMinimumDisplayTime; 453 removeAutoCancelCallbacks(); 454 mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay); 455 updateSortOrder(HeadsUpEntry.this); 456 } 457 458 @Override 459 public int compareTo(HeadsUpEntry o) { 460 return postTime < o.postTime ? 1 461 : postTime == o.postTime ? 0 462 : -1; 463 } 464 465 public void removeAutoCancelCallbacks() { 466 mHandler.removeCallbacks(mRemoveHeadsUpRunnable); 467 } 468 469 public boolean wasShownLongEnough() { 470 return earliestRemovaltime < mClock.currentTimeMillis(); 471 } 472 473 public void hideAsSoonAsPossible() { 474 removeAutoCancelCallbacks(); 475 mHandler.postDelayed(mRemoveHeadsUpRunnable, 476 earliestRemovaltime - mClock.currentTimeMillis()); 477 } 478 } 479 480 /** 481 * Update the sorted heads up order. 482 * 483 * @param headsUpEntry the headsUp that changed 484 */ 485 private void updateSortOrder(HeadsUpEntry headsUpEntry) { 486 mSortedEntries.remove(headsUpEntry); 487 mSortedEntries.add(headsUpEntry); 488 } 489 490 public static class Clock { 491 public long currentTimeMillis() { 492 return SystemClock.elapsedRealtime(); 493 } 494 } 495 496 public interface OnHeadsUpChangedListener { 497 void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly); 498 void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp); 499 } 500} 501