From 930eccaf44239d010cbafb246651697996f567b5 Mon Sep 17 00:00:00 2001 From: Chris Wren Date: Wed, 12 Nov 2014 17:43:41 -0500 Subject: [PATCH] Don't let the heads up close too quickly. The public API of HeadsUpNotificaitonView was not well suited to the new requirements, so it changed slightly. Old API: - showNotification: show or update a notification - clear: close the window and forget the notification - release: send the notification to the shade and forget about it. - releaseAndClose: release and close the window - dismiss: clear the notification if clearable, or release it New API: - showNotification: show a new notification - updateNotification: show a new version of the same notification - removeNotification: respond to a cancel - release: send the notification to the shade at some point - releaseImmediately: send the notification to the shade right now The new API makes updating vs. posting and removing vs. releasing more explicit. There is a new internal concept: lingering. The heads up lingers after an event that would have closed it if the minimum visibility time has not been satisfied. In the case that the notification was deleted, the heads up may be visible, but mHeadsUp will be null. In this case, touches on the notification views are disabled. More responsibility for control of the heads of policy was moved into the HeadsUpNotificaitonView class. This should continue on master. Some changes to support testing. Added a test to cover all the edge cases for minimum visibility time: 1. extend visibility when canceled too soon 2. extend when updated with a low-priority version, fast update. 3. extend when updated with a low-priority version, slow update. 4. don't extend the visibility in any other case TODO: Policy parts of HeadsUpNotificationView should be split out into a separate HeadsUpNotificationPolicy class, and even more of the policy should be lifted from status bar that new class. Bug: 17878008 Change-Id: I192419d0685dd022ee7edcd792e346a4f39c6adb --- packages/SystemUI/res/values/config.xml | 3 + .../systemui/statusbar/BaseStatusBar.java | 60 ++-- .../statusbar/phone/PhoneStatusBar.java | 81 +++--- .../policy/HeadsUpNotificationView.java | 256 +++++++++++------ .../systemui/statusbar/tv/TvStatusBar.java | 2 +- .../com/android/systemui/SysuiTestCase.java | 31 +++ .../policy/HeadsUpNotificationTest.java | 261 ++++++++++++++++++ .../policy/NetworkControllerBaseTest.java | 7 +- 8 files changed, 536 insertions(+), 165 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpNotificationTest.java diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 2659009364fcd..42b50d4348650 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -146,6 +146,9 @@ before the app can interrupt again. --> 60000 + + 3000 + 700 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index a0ea25f518f41..3b99af10bb7aa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -127,7 +127,6 @@ public abstract class BaseStatusBar extends SystemUI implements protected static final int MSG_SHOW_HEADS_UP = 1028; protected static final int MSG_HIDE_HEADS_UP = 1029; protected static final int MSG_ESCALATE_HEADS_UP = 1030; - protected static final int MSG_DECAY_HEADS_UP = 1031; protected static final boolean ENABLE_HEADS_UP = true; // scores above this threshold should be displayed in heads up mode. @@ -1153,7 +1152,7 @@ public abstract class BaseStatusBar extends SystemUI implements // Do nothing } - public abstract void resetHeadsUpDecayTimer(); + public abstract void scheduleHeadsUpDecay(long delay); public abstract void scheduleHeadsUpOpen(); @@ -1353,8 +1352,7 @@ public abstract class BaseStatusBar extends SystemUI implements PendingIntent contentIntent = sbn.getNotification().contentIntent; if (contentIntent != null) { - final View.OnClickListener listener = makeClicker(contentIntent, sbn.getKey(), - isHeadsUp); + final View.OnClickListener listener = makeClicker(contentIntent, sbn.getKey()); row.setOnClickListener(listener); } else { row.setOnClickListener(null); @@ -1515,20 +1513,17 @@ public abstract class BaseStatusBar extends SystemUI implements return true; } - public NotificationClicker makeClicker(PendingIntent intent, String notificationKey, - boolean forHun) { - return new NotificationClicker(intent, notificationKey, forHun); + public NotificationClicker makeClicker(PendingIntent intent, String notificationKey) { + return new NotificationClicker(intent, notificationKey); } protected class NotificationClicker implements View.OnClickListener { private PendingIntent mIntent; private final String mNotificationKey; - private boolean mIsHeadsUp; - public NotificationClicker(PendingIntent intent, String notificationKey, boolean forHun) { + public NotificationClicker(PendingIntent intent, String notificationKey) { mIntent = intent; mNotificationKey = notificationKey; - mIsHeadsUp = forHun; } public void onClick(final View v) { @@ -1541,12 +1536,12 @@ public abstract class BaseStatusBar extends SystemUI implements mCurrentUserId); dismissKeyguardThenExecute(new OnDismissAction() { public boolean onDismiss() { - if (mIsHeadsUp) { + if (mNotificationKey.equals(mHeadsUpNotificationView.getKey())) { // Release the HUN notification to the shade. // // In most cases, when FLAG_AUTO_CANCEL is set, the notification will // become canceled shortly by NoMan, but we can't assume that. - mHeadsUpNotificationView.releaseAndClose(); + mHeadsUpNotificationView.releaseImmediately(); } new Thread() { @Override @@ -1893,7 +1888,7 @@ public abstract class BaseStatusBar extends SystemUI implements && oldPublicContentView.getLayoutId() == publicContentView.getLayoutId()); final boolean shouldInterrupt = shouldInterrupt(notification); - final boolean alertAgain = alertAgain(oldEntry, n); + final boolean alertAgain = shouldInterrupt && alertAgain(oldEntry, n); boolean updateSuccessful = false; if (contentsUnchanged && bigContentsUnchanged && headsUpContentsUnchanged && publicUnchanged) { @@ -1916,14 +1911,12 @@ public abstract class BaseStatusBar extends SystemUI implements } if (wasHeadsUp) { - if (shouldInterrupt) { - updateHeadsUpViews(oldEntry, notification); - if (alertAgain) { - resetHeadsUpDecayTimer(); - } - } else { + // Release may hang on to the views for a bit, so we should always update them. + updateHeadsUpViews(oldEntry, notification); + mHeadsUpNotificationView.updateNotification(oldEntry, alertAgain); + if (!shouldInterrupt) { // we updated the notification above, so release to build a new shade entry - mHeadsUpNotificationView.releaseAndClose(); + mHeadsUpNotificationView.release(); return; } } else { @@ -1946,23 +1939,19 @@ public abstract class BaseStatusBar extends SystemUI implements if (!updateSuccessful) { if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key); if (wasHeadsUp) { - if (shouldInterrupt) { - if (DEBUG) Log.d(TAG, "rebuilding heads up for key: " + key); - Entry newEntry = new Entry(notification, null); - ViewGroup holder = mHeadsUpNotificationView.getHolder(); - if (inflateViewsForHeadsUp(newEntry, holder)) { - mHeadsUpNotificationView.showNotification(newEntry); - if (alertAgain) { - resetHeadsUpDecayTimer(); - } - } else { - Log.w(TAG, "Couldn't create new updated headsup for package " - + contentView.getPackage()); - } + if (DEBUG) Log.d(TAG, "rebuilding heads up for key: " + key); + Entry newEntry = new Entry(notification, null); + ViewGroup holder = mHeadsUpNotificationView.getHolder(); + if (inflateViewsForHeadsUp(newEntry, holder)) { + mHeadsUpNotificationView.updateNotification(newEntry, alertAgain); } else { + Log.w(TAG, "Couldn't create new updated headsup for package " + + contentView.getPackage()); + } + if (!shouldInterrupt) { if (DEBUG) Log.d(TAG, "releasing heads up for key: " + key); oldEntry.notification = notification; - mHeadsUpNotificationView.releaseAndClose(); + mHeadsUpNotificationView.release(); return; } } else { @@ -2032,8 +2021,7 @@ public abstract class BaseStatusBar extends SystemUI implements // update the contentIntent final PendingIntent contentIntent = notification.getNotification().contentIntent; if (contentIntent != null) { - final View.OnClickListener listener = makeClicker(contentIntent, notification.getKey(), - isHeadsUp); + final View.OnClickListener listener = makeClicker(contentIntent, notification.getKey()); entry.row.setOnClickListener(listener); } else { entry.row.setOnClickListener(null); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 3ee7fb2d422e7..ece69d3a84818 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -361,7 +361,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (!mUseHeadsUp) { Log.d(TAG, "dismissing any existing heads up notification on disable event"); setHeadsUpVisibility(false); - mHeadsUpNotificationView.release(); + mHeadsUpNotificationView.releaseImmediately(); removeHeadsUpView(); } else { addHeadsUpView(); @@ -1212,33 +1212,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, setAreThereNotifications(); } - @Override - public void resetHeadsUpDecayTimer() { - mHandler.removeMessages(MSG_DECAY_HEADS_UP); - if (mUseHeadsUp && mHeadsUpNotificationDecay > 0 - && mHeadsUpNotificationView.isClearable()) { - mHandler.sendEmptyMessageDelayed(MSG_DECAY_HEADS_UP, mHeadsUpNotificationDecay); - } - } - - @Override - public void scheduleHeadsUpOpen() { - mHandler.removeMessages(MSG_SHOW_HEADS_UP); - mHandler.sendEmptyMessage(MSG_SHOW_HEADS_UP); - } - - @Override - public void scheduleHeadsUpClose() { - mHandler.removeMessages(MSG_HIDE_HEADS_UP); - mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); - } - - @Override - public void scheduleHeadsUpEscalation() { - mHandler.removeMessages(MSG_ESCALATE_HEADS_UP); - mHandler.sendEmptyMessage(MSG_ESCALATE_HEADS_UP); - } - @Override protected void updateNotificationRanking(RankingMap ranking) { mNotificationData.updateRanking(ranking); @@ -1247,9 +1220,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public void removeNotification(String key, RankingMap ranking) { - if (ENABLE_HEADS_UP && mHeadsUpNotificationView.getEntry() != null - && key.equals(mHeadsUpNotificationView.getEntry().notification.getKey())) { - mHeadsUpNotificationView.clear(); + if (ENABLE_HEADS_UP) { + mHeadsUpNotificationView.removeNotification(key); } StatusBarNotification old = removeNotificationViews(key, ranking); @@ -1870,16 +1842,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, case MSG_SHOW_HEADS_UP: setHeadsUpVisibility(true); break; - case MSG_DECAY_HEADS_UP: - mHeadsUpNotificationView.release(); - setHeadsUpVisibility(false); - break; - case MSG_HIDE_HEADS_UP: - mHeadsUpNotificationView.release(); - setHeadsUpVisibility(false); - break; case MSG_ESCALATE_HEADS_UP: escalateHeadsUp(); + case MSG_HIDE_HEADS_UP: + mHeadsUpNotificationView.releaseImmediately(); setHeadsUpVisibility(false); break; case MSG_LAUNCH_TRANSITION_TIMEOUT: @@ -1889,11 +1855,41 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } + @Override + public void scheduleHeadsUpDecay(long delay) { + mHandler.removeMessages(MSG_HIDE_HEADS_UP); + if (mHeadsUpNotificationView.isClearable()) { + mHandler.sendEmptyMessageDelayed(MSG_HIDE_HEADS_UP, delay); + } + } + + @Override + public void scheduleHeadsUpOpen() { + mHandler.removeMessages(MSG_HIDE_HEADS_UP); + mHandler.removeMessages(MSG_SHOW_HEADS_UP); + mHandler.sendEmptyMessage(MSG_SHOW_HEADS_UP); + } + + @Override + public void scheduleHeadsUpClose() { + mHandler.removeMessages(MSG_HIDE_HEADS_UP); + if (mHeadsUpNotificationView.getVisibility() != View.GONE) { + mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); + } + } + + @Override + public void scheduleHeadsUpEscalation() { + mHandler.removeMessages(MSG_HIDE_HEADS_UP); + mHandler.removeMessages(MSG_ESCALATE_HEADS_UP); + mHandler.sendEmptyMessage(MSG_ESCALATE_HEADS_UP); + } + /** if the interrupting notification had a fullscreen intent, fire it now. */ private void escalateHeadsUp() { if (mHeadsUpNotificationView.getEntry() != null) { final StatusBarNotification sbn = mHeadsUpNotificationView.getEntry().notification; - mHeadsUpNotificationView.release(); + mHeadsUpNotificationView.releaseImmediately(); final Notification notification = sbn.getNotification(); if (notification.fullScreenIntent != null) { if (DEBUG) @@ -2734,10 +2730,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mHeadsUpNotificationView.setVisibility(vis ? View.VISIBLE : View.GONE); } - public void onHeadsUpDismissed() { - mHeadsUpNotificationView.dismiss(); - } - /** * Reload some of our resources when the configuration changes. * @@ -2772,7 +2764,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mEdgeBorder = res.getDimensionPixelSize(R.dimen.status_bar_edge_ignore); - mHeadsUpNotificationDecay = res.getInteger(R.integer.heads_up_notification_decay); mRowMinHeight = res.getDimensionPixelSize(R.dimen.notification_min_height); mRowMaxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java index 2e96dd5e4e697..1566cd1bdc579 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java @@ -25,6 +25,9 @@ import android.graphics.Rect; import android.os.SystemClock; import android.provider.Settings; import android.util.ArrayMap; +import android.graphics.Outline; +import android.graphics.Rect; +import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; @@ -36,6 +39,7 @@ import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.ExpandHelper; import com.android.systemui.Gefingerpoken; import com.android.systemui.R; @@ -58,6 +62,9 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. Rect mTmpRect = new Rect(); int[] mTmpTwoArray = new int[2]; + private final int mHeadsUpNotificationDecay; + private final int mMinimumDisplayTime; + private final int mTouchSensitivityDelay; private final float mMaxAlpha = 1f; private final ArrayMap mSnoozedPackages; @@ -68,6 +75,7 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. private PhoneStatusBar mBar; + private long mLingerUntilMs; private long mStartTouchTime; private ViewGroup mContentHolder; private int mSnoozeLengthMs; @@ -76,6 +84,14 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. private NotificationData.Entry mHeadsUp; private int mUser; private String mMostRecentPackageName; + private boolean mTouched; + private Clock mClock; + + public static class Clock { + public long currentTimeMillis() { + return SystemClock.elapsedRealtime(); + } + } public HeadsUpNotificationView(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -89,6 +105,24 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. mSnoozedPackages = new ArrayMap<>(); mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms); mSnoozeLengthMs = mDefaultSnoozeLengthMs; + mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time); + mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay); + mClock = new Clock(); + } + + @VisibleForTesting + public HeadsUpNotificationView(Context context, Clock clock, SwipeHelper swipeHelper, + EdgeSwipeHelper edgeSwipeHelper, int headsUpNotificationDecay, int minimumDisplayTime, + int touchSensitivityDelay, int snoozeLength) { + super(context, null); + mClock = clock; + mSwipeHelper = swipeHelper; + mEdgeSwipeHelper = edgeSwipeHelper; + mMinimumDisplayTime = minimumDisplayTime; + mHeadsUpNotificationDecay = headsUpNotificationDecay; + mTouchSensitivityDelay = touchSensitivityDelay; + mSnoozedPackages = new ArrayMap<>(); + mDefaultSnoozeLengthMs = snoozeLength; } public void updateResources() { @@ -104,90 +138,141 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. mBar = bar; } + public PhoneStatusBar getBar() { + return mBar; + } + public ViewGroup getHolder() { return mContentHolder; } - public boolean showNotification(NotificationData.Entry headsUp) { - if (mHeadsUp != null && headsUp != null && !mHeadsUp.key.equals(headsUp.key)) { + /** + * Called when posting a new notification to the heads up. + */ + public void showNotification(NotificationData.Entry headsUp) { + if (DEBUG) Log.v(TAG, "showNotification"); + if (mHeadsUp != null) { // bump any previous heads up back to the shade - release(); + releaseImmediately(); + } + mTouched = false; + updateNotification(headsUp, true); + mLingerUntilMs = mClock.currentTimeMillis() + mMinimumDisplayTime; + } + + /** + * Called when updating or posting a notification to the heads up. + */ + public void updateNotification(NotificationData.Entry headsUp, boolean alert) { + if (DEBUG) Log.v(TAG, "updateNotification"); + + if (alert) { + mBar.scheduleHeadsUpDecay(mHeadsUpNotificationDecay); + } + invalidate(); + + if (mHeadsUp == headsUp) { + // This is an in-place update. Noting more to do. + return; } mHeadsUp = headsUp; + if (mContentHolder != null) { mContentHolder.removeAllViews(); } if (mHeadsUp != null) { mMostRecentPackageName = mHeadsUp.notification.getPackageName(); - mHeadsUp.row.setSystemExpanded(true); - mHeadsUp.row.setSensitive(false); - mHeadsUp.row.setHeadsUp(true); - mHeadsUp.row.setHideSensitive( - false, false /* animated */, 0 /* delay */, 0 /* duration */); - if (mContentHolder == null) { - // too soon! - return false; + if (mHeadsUp.row != null) { // only null in tests + mHeadsUp.row.setSystemExpanded(true); + mHeadsUp.row.setSensitive(false); + mHeadsUp.row.setHeadsUp(true); + mHeadsUp.row.setHideSensitive( + false, false /* animated */, 0 /* delay */, 0 /* duration */); } - mContentHolder.setX(0); - mContentHolder.setVisibility(View.VISIBLE); - mContentHolder.setAlpha(mMaxAlpha); - mContentHolder.addView(mHeadsUp.row); - sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - mSwipeHelper.snapChild(mContentHolder, 1f); mStartTouchTime = SystemClock.elapsedRealtime() + mTouchSensitivityDelay; + if (mContentHolder != null) { // only null in tests and before we are attached to a window + mContentHolder.setX(0); + mContentHolder.setVisibility(View.VISIBLE); + mContentHolder.setAlpha(mMaxAlpha); + mContentHolder.addView(mHeadsUp.row); + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + + mSwipeHelper.snapChild(mContentHolder, 1f); + } mHeadsUp.setInterruption(); - // 2. Animate mHeadsUpNotificationView in + // Make sure the heads up window is open. mBar.scheduleHeadsUpOpen(); - - // 3. Set alarm to age the notification off - mBar.resetHeadsUpDecayTimer(); } - return true; + } + + /** + * Possibly enter the lingering state by delaying the closing of the window. + * + * @return true if the notification has entered the lingering state. + */ + private boolean startLingering(boolean removed) { + final long now = mClock.currentTimeMillis(); + if (!mTouched && mHeadsUp != null && now < mLingerUntilMs) { + if (removed) { + mHeadsUp = null; + } + mBar.scheduleHeadsUpDecay(mLingerUntilMs - now); + return true; + } + return false; + } + + /** + * React to the removal of the notification in the heads up. + */ + public void removeNotification(String key) { + if (DEBUG) Log.v(TAG, "remove"); + if (mHeadsUp == null || !mHeadsUp.key.equals(key)) { + return; + } + if (!startLingering(/* removed */ true)) { + mHeadsUp = null; + releaseImmediately(); + } + } + + /** + * Ask for any current Heads Up notification to be pushed down into the shade. + */ + public void release() { + if (DEBUG) Log.v(TAG, "release"); + if (!startLingering(/* removed */ false)) { + releaseImmediately(); + } + } + + /** + * Push any current Heads Up notification down into the shade. + */ + public void releaseImmediately() { + if (DEBUG) Log.v(TAG, "releaseImmediately"); + if (mHeadsUp != null) { + mBar.displayNotificationFromHeadsUp(mHeadsUp.notification); + } + mHeadsUp = null; + mBar.scheduleHeadsUpClose(); } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); + if (DEBUG) Log.v(TAG, "onVisibilityChanged: " + visibility); if (changedView.getVisibility() == VISIBLE) { + mStartTouchTime = mClock.currentTimeMillis() + mTouchSensitivityDelay; sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } - public boolean isShowing(String key) { - return mHeadsUp != null && mHeadsUp.key.equals(key); - } - - /** Discard the Heads Up notification. */ - public void clear() { - mHeadsUp = null; - mBar.scheduleHeadsUpClose(); - } - - /** Respond to dismissal of the Heads Up window. */ - public void dismiss() { - if (mHeadsUp == null) return; - if (mHeadsUp.notification.isClearable()) { - mBar.onNotificationClear(mHeadsUp.notification); - } else { - release(); - } - mHeadsUp = null; - mBar.scheduleHeadsUpClose(); - } - - /** Push any current Heads Up notification down into the shade. */ - public void release() { - if (mHeadsUp != null) { - mBar.displayNotificationFromHeadsUp(mHeadsUp.notification); - } - mHeadsUp = null; - } - public boolean isSnoozed(String packageName) { final String key = snoozeKey(packageName, mUser); Long snoozedUntil = mSnoozedPackages.get(key); @@ -206,16 +291,15 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. mSnoozedPackages.put(snoozeKey(mMostRecentPackageName, mUser), SystemClock.elapsedRealtime() + mSnoozeLengthMs); } - releaseAndClose(); + releaseImmediately(); } private static String snoozeKey(String packageName, int user) { return user + "," + packageName; } - public void releaseAndClose() { - release(); - mBar.scheduleHeadsUpClose(); + public boolean isShowing(String key) { + return mHeadsUp != null && mHeadsUp.key.equals(key); } public NotificationData.Entry getEntry() { @@ -228,19 +312,19 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. // ViewGroup methods - private static final ViewOutlineProvider CONTENT_HOLDER_OUTLINE_PROVIDER = - new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - int outlineLeft = view.getPaddingLeft(); - int outlineTop = view.getPaddingTop(); +private static final ViewOutlineProvider CONTENT_HOLDER_OUTLINE_PROVIDER = + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + int outlineLeft = view.getPaddingLeft(); + int outlineTop = view.getPaddingTop(); - // Apply padding to shadow. - outline.setRect(outlineLeft, outlineTop, - view.getWidth() - outlineLeft - view.getPaddingRight(), - view.getHeight() - outlineTop - view.getPaddingBottom()); - } - }; + // Apply padding to shadow. + outline.setRect(outlineLeft, outlineTop, + view.getWidth() - outlineLeft - view.getPaddingRight(), + view.getHeight() - outlineTop - view.getPaddingBottom()); + } + }; @Override public void onAttachedToWindow() { @@ -248,7 +332,7 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. float touchSlop = viewConfiguration.getScaledTouchSlop(); mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext()); mSwipeHelper.setMaxSwipeProgress(mMaxAlpha); - mEdgeSwipeHelper = new EdgeSwipeHelper(touchSlop); + mEdgeSwipeHelper = new EdgeSwipeHelper(this, touchSlop); int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); @@ -282,6 +366,7 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. getViewTreeObserver().addOnComputeInternalInsetsListener(this); } + @Override protected void onDetachedFromWindow() { mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); @@ -290,11 +375,13 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); - if (SystemClock.elapsedRealtime() < mStartTouchTime) { + if (mClock.currentTimeMillis() < mStartTouchTime) { return true; } + mTouched = true; return mEdgeSwipeHelper.onInterceptTouchEvent(ev) || mSwipeHelper.onInterceptTouchEvent(ev) + || mHeadsUp == null // lingering || super.onInterceptTouchEvent(ev); } @@ -316,12 +403,17 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. @Override public boolean onTouchEvent(MotionEvent ev) { - if (SystemClock.elapsedRealtime() < mStartTouchTime) { + if (mClock.currentTimeMillis() < mStartTouchTime) { return false; } - mBar.resetHeadsUpDecayTimer(); + + final boolean wasRemoved = mHeadsUp == null; + if (!wasRemoved) { + mBar.scheduleHeadsUpDecay(mHeadsUpNotificationDecay); + } return mEdgeSwipeHelper.onTouchEvent(ev) || mSwipeHelper.onTouchEvent(ev) + || wasRemoved || super.onTouchEvent(ev); } @@ -390,7 +482,11 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. @Override public void onChildDismissed(View v) { Log.v(TAG, "User swiped heads up to dismiss"); - mBar.onHeadsUpDismissed(); + if (mHeadsUp != null && mHeadsUp.notification.isClearable()) { + mBar.onNotificationClear(mHeadsUp.notification); + mHeadsUp = null; + } + releaseImmediately(); } @Override @@ -448,6 +544,8 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. pw.println("HeadsUpNotificationView state:"); pw.print(" mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay); pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); + pw.print(" mLingerUntilMs="); pw.println(mLingerUntilMs); + pw.print(" mTouched="); pw.println(mTouched); pw.print(" mMostRecentPackageName="); pw.println(mMostRecentPackageName); pw.print(" mStartTouchTime="); pw.println(mStartTouchTime); pw.print(" now="); pw.println(SystemClock.elapsedRealtime()); @@ -465,14 +563,16 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. } } - private class EdgeSwipeHelper implements Gefingerpoken { + public static class EdgeSwipeHelper implements Gefingerpoken { private static final boolean DEBUG_EDGE_SWIPE = false; private final float mTouchSlop; + private final HeadsUpNotificationView mHeadsUpView; private boolean mConsuming; private float mFirstY; private float mFirstX; - public EdgeSwipeHelper(float touchSlop) { + public EdgeSwipeHelper(HeadsUpNotificationView headsUpView, float touchSlop) { + mHeadsUpView = headsUpView; mTouchSlop = touchSlop; } @@ -492,10 +592,10 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. final float daX = Math.abs(ev.getX() - mFirstX); final float daY = Math.abs(dY); if (!mConsuming && daX < daY && daY > mTouchSlop) { - snooze(); + mHeadsUpView.snooze(); if (dY > 0) { if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found an open"); - mBar.animateExpandNotificationsPanel(); + mHeadsUpView.getBar().animateExpandNotificationsPanel(); } mConsuming = true; } @@ -503,7 +603,7 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: - if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action done" ); + if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action done"); mConsuming = false; break; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java index 0a14cf5a34c1d..6f2a3924a7086 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java @@ -127,7 +127,7 @@ public class TvStatusBar extends BaseStatusBar { } @Override - public void resetHeadsUpDecayTimer() { + public void scheduleHeadsUpDecay(long delay) { } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java new file mode 100644 index 0000000000000..3fdb3d2996325 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui; + +import android.test.AndroidTestCase; + +/** + * Base class that does System UI specific setup. + */ +public class SysuiTestCase extends AndroidTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + // Mockito stuff. + System.setProperty("dexmaker.dexcache", mContext.getCacheDir().getPath()); + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpNotificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpNotificationTest.java new file mode 100644 index 0000000000000..e8a80d9f587d2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpNotificationTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.os.*; +import android.service.notification.StatusBarNotification; +import com.android.systemui.SwipeHelper; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.NotificationData; +import com.android.systemui.statusbar.phone.PhoneStatusBar; + +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +/** + * Test the Heads Up Notification. + * + * Specifically the policy that a notificaiton must remain visibile for a minimum period of time. + */ +public class HeadsUpNotificationTest extends SysuiTestCase { + private static final String TAG = "HeadsUpNotificationTest"; + + private static int TOUCH_SENSITIVITY = 100; + private static int NOTIFICATION_DECAY = 10000; + private static int MINIMUM_DISPLAY_TIME = 3000; + private static int SNOOZE_TIME = 60000; + private static long TOO_SOON = 1000L; // less than MINIMUM_DISPLAY_TIME + private static long LATER = 5000L; // more than MINIMUM_DISPLAY_TIME + private static long REMAINING_VISIBILITY = MINIMUM_DISPLAY_TIME - TOO_SOON; + + protected HeadsUpNotificationView mHeadsUp; + + @Mock protected PhoneStatusBar mMockStatusBar; + @Mock private HeadsUpNotificationView.Clock mClock; + @Mock private SwipeHelper mMockSwipeHelper; + @Mock private HeadsUpNotificationView.EdgeSwipeHelper mMockEdgeSwipeHelper; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + MockitoAnnotations.initMocks(this); + + mHeadsUp = new HeadsUpNotificationView(mContext, + mClock, mMockSwipeHelper, mMockEdgeSwipeHelper, + NOTIFICATION_DECAY, MINIMUM_DISPLAY_TIME, TOUCH_SENSITIVITY, SNOOZE_TIME); + mHeadsUp.setBar(mMockStatusBar); + } + + private NotificationData.Entry makeNotification(String key) { + StatusBarNotification sbn = mock(StatusBarNotification.class); + when(sbn.getKey()).thenReturn(key); + return new NotificationData.Entry(sbn, null); + } + + public void testPostAndDecay() { + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose(); + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpOpen(); + ArgumentCaptor decayArg = ArgumentCaptor.forClass(Long.class); + Mockito.verify(mMockStatusBar).scheduleHeadsUpDecay(decayArg.capture()); + // New notification gets a full decay time. + assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue()); + } + + public void testPostAndDeleteTooSoon() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(TOO_SOON); + mHeadsUp.removeNotification(a.key); + ArgumentCaptor decayArg = ArgumentCaptor.forClass(Long.class); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose(); + Mockito.verify(mMockStatusBar).scheduleHeadsUpDecay(decayArg.capture()); + // Leave the window up for the balance of the minumum time. + assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue()); + } + + public void testPostAndDeleteLater() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(LATER); + mHeadsUp.removeNotification(a.key); + // Delete closes immediately if the minimum time window is satisfied. + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose(); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt()); + } + + // This is a bad test. It should not care that there is a call to scheduleHeadsUpClose(), + // but it happens that there will be one, so it is important that it happen before the + // call to scheduleHeadsUpOpen(), so that the final state is open. + // Maybe mMockStatusBar should instead be a fake that tracks the open/closed state. + public void testPostAndReplaceTooSoon() { + InOrder callOrder = inOrder(mMockStatusBar); + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(TOO_SOON); + NotificationData.Entry b = makeNotification("b"); + mHeadsUp.showNotification(b); + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose(); + ArgumentCaptor decayArg = ArgumentCaptor.forClass(Long.class); + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture()); + // New notification gets a full decay time. + assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue()); + + // Make sure close was called before open, so that the heads up stays open. + callOrder.verify(mMockStatusBar).scheduleHeadsUpClose(); + callOrder.verify(mMockStatusBar).scheduleHeadsUpOpen(); + } + + public void testPostAndUpdateAlertAgain() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(TOO_SOON); + mHeadsUp.updateNotification(a, true); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose(); + ArgumentCaptor decayArg = ArgumentCaptor.forClass(Long.class); + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture()); + // Alert again gets a full decay time. + assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue()); + } + + public void testPostAndUpdateAlertAgainFastFail() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(TOO_SOON); + NotificationData.Entry a_prime = makeNotification("a"); + mHeadsUp.updateNotification(a_prime, true); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose(); + ArgumentCaptor decayArg = ArgumentCaptor.forClass(Long.class); + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture()); + // Alert again gets a full decay time. + assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue()); + } + + public void testPostAndUpdateNoAlertAgain() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(TOO_SOON); + mHeadsUp.updateNotification(a, false); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose(); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt()); + } + + public void testPostAndUpdateNoAlertAgainFastFail() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(TOO_SOON); + NotificationData.Entry a_prime = makeNotification("a"); + mHeadsUp.updateNotification(a_prime, false); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose(); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt()); + } + + public void testPostAndUpdateLowPriorityTooSoon() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(TOO_SOON); + mHeadsUp.release(); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose(); + ArgumentCaptor decayArg = ArgumentCaptor.forClass(Long.class); + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture()); + // Down grade on update leaves the window up for the balance of the minumum time. + assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue()); + } + + public void testPostAndUpdateLowPriorityTooSoonFastFail() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(TOO_SOON); + NotificationData.Entry a_prime = makeNotification("a"); + mHeadsUp.updateNotification(a_prime, false); + mHeadsUp.release(); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose(); + ArgumentCaptor decayArg = ArgumentCaptor.forClass(Long.class); + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture()); + // Down grade on update leaves the window up for the balance of the minumum time. + assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue()); + } + + public void testPostAndUpdateLowPriorityLater() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(LATER); + mHeadsUp.release(); + // Down grade on update closes immediately if the minimum time window is satisfied. + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose(); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt()); + } + + public void testPostAndUpdateLowPriorityLaterFastFail() { + when(mClock.currentTimeMillis()).thenReturn(0L); + NotificationData.Entry a = makeNotification("a"); + mHeadsUp.showNotification(a); + reset(mMockStatusBar); + + when(mClock.currentTimeMillis()).thenReturn(LATER); + NotificationData.Entry a_prime = makeNotification("a"); + mHeadsUp.updateNotification(a_prime, false); + mHeadsUp.release(); + // Down grade on update closes immediately if the minimum time window is satisfied. + Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose(); + Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java index 260dea0125830..5d884076a1f32 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java @@ -29,11 +29,11 @@ import android.telephony.SignalStrength; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; -import android.test.AndroidTestCase; import android.util.Log; import com.android.internal.telephony.IccCardConstants; import com.android.internal.telephony.cdma.EriInfo; +import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.policy.NetworkController.NetworkSignalChangedCallback; import com.android.systemui.statusbar.policy.NetworkControllerImpl.Config; import com.android.systemui.statusbar.policy.NetworkControllerImpl.SignalCluster; @@ -46,7 +46,7 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.List; -public class NetworkControllerBaseTest extends AndroidTestCase { +public class NetworkControllerBaseTest extends SysuiTestCase { private static final String TAG = "NetworkControllerBaseTest"; protected static final int DEFAULT_LEVEL = 2; protected static final int DEFAULT_SIGNAL_STRENGTH = @@ -76,9 +76,6 @@ public class NetworkControllerBaseTest extends AndroidTestCase { @Override protected void setUp() throws Exception { super.setUp(); - // Mockito stuff. - System.setProperty("dexmaker.dexcache", mContext.getCacheDir().getPath()); - Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); mMockWm = mock(WifiManager.class); mMockTm = mock(TelephonyManager.class);