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