diff options
8 files changed, 530 insertions, 159 deletions
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 2659009364fc..42b50d434865 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 a0ea25f518f4..3b99af10bb7a 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 3ee7fb2d422e..ece69d3a8481 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(); @@ -1213,33 +1213,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } @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 @@ 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 2e96dd5e4e69..1566cd1bdc57 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<String, Long> 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,88 +138,139 @@ 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; } - @Override - protected void onVisibilityChanged(View changedView, int visibility) { - super.onVisibilityChanged(changedView, visibility); - if (changedView.getVisibility() == VISIBLE) { - sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + /** + * 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; } - public boolean isShowing(String key) { - return mHeadsUp != null && mHeadsUp.key.equals(key); + /** + * 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(); + } } - /** Discard the Heads Up notification. */ - public void clear() { - mHeadsUp = null; - mBar.scheduleHeadsUpClose(); + /** + * 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(); + } } - /** 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(); + /** + * 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(); } - /** Push any current Heads Up notification down into the shade. */ - public void release() { - if (mHeadsUp != null) { - mBar.displayNotificationFromHeadsUp(mHeadsUp.notification); + @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); } - mHeadsUp = null; } public boolean isSnoozed(String packageName) { @@ -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(); - - // Apply padding to shadow. - outline.setRect(outlineLeft, outlineTop, - view.getWidth() - outlineLeft - view.getPaddingRight(), - view.getHeight() - outlineTop - view.getPaddingBottom()); - } - }; +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()); + } + }; @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 0a14cf5a34c1..6f2a3924a708 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 000000000000..3fdb3d299632 --- /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 000000000000..e8a80d9f587d --- /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 260dea012583..5d884076a1f3 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); |