Merge "Don't let the heads up close too quickly."

This commit is contained in:
Chris Wren
2015-02-09 14:28:02 +00:00
committed by Android (Google) Code Review
8 changed files with 536 additions and 165 deletions

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,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;
}

View File

@@ -127,7 +127,7 @@ public class TvStatusBar extends BaseStatusBar {
}
@Override
public void resetHeadsUpDecayTimer() {
public void scheduleHeadsUpDecay(long delay) {
}
@Override

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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);