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
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 2659009..42b50d4 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. -->
<integer name="heads_up_default_snooze_length_ms">60000</integer>
+ <!-- Minimum display time for a heads up notification, in milliseconds. -->
+ <integer name="heads_up_notification_minimum_time">3000</integer>
+
<!-- milliseconds before the heads up notification accepts touches. -->
<integer name="heads_up_sensitivity_delay">700</integer>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index a0ea25f..3b99af1 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 @@
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 @@
// Do nothing
}
- public abstract void resetHeadsUpDecayTimer();
+ public abstract void scheduleHeadsUpDecay(long delay);
public abstract void scheduleHeadsUpOpen();
@@ -1353,8 +1352,7 @@
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 @@
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 @@
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 @@
&& 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 @@
}
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 @@
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 @@
// 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 3ee7fb2..ece69d3 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 @@
if (!mUseHeadsUp) {
Log.d(TAG, "dismissing any existing heads up notification on disable event");
setHeadsUpVisibility(false);
- mHeadsUpNotificationView.release();
+ mHeadsUpNotificationView.releaseImmediately();
removeHeadsUpView();
} else {
addHeadsUpView();
@@ -1213,33 +1213,6 @@
}
@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);
updateNotifications();
@@ -1247,9 +1220,8 @@
@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 @@
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 @@
}
}
+ @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 @@
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 @@
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 2e96dd5..1566cd1 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.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.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 @@
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<String, Long> mSnoozedPackages;
@@ -68,6 +75,7 @@
private PhoneStatusBar mBar;
+ private long mLingerUntilMs;
private long mStartTouchTime;
private ViewGroup mContentHolder;
private int mSnoozeLengthMs;
@@ -76,6 +84,14 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
}
+
@Override
protected void onDetachedFromWindow() {
mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
@@ -290,11 +375,13 @@
@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 @@
@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 @@
@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 @@
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 @@
}
}
- 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 @@
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 @@
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 0a14cf5..6f2a392 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 @@
}
@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 0000000..3fdb3d2
--- /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 0000000..e8a80d9
--- /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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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 260dea0..5d88407 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.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.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 @@
@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);