Introduced the visual stability manager
Notifications used to roam around and reorder whenever
they wished. Those little beasts must be tamed, hence
a new visual stability manager is introduced that
dictates the terms of their interplay.
Test: manual: add heads-up and see if they correctly appear
Test: runtest -x packages/SystemUI/tests/src/com/android/systemui/notification/VisualStabilityManagerTest.java
Bug: 33773401
Merged-In: I8d7596fa7c14e0df68459a77d445f618d517ad51
Change-Id: I8d7596fa7c14e0df68459a77d445f618d517ad51
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index 1afdc15e..dda82c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -103,6 +103,7 @@
import com.android.systemui.recents.Recents;
import com.android.systemui.statusbar.NotificationData.Entry;
import com.android.systemui.statusbar.NotificationGuts.OnGutsClosedListener;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NavigationBarView;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -118,14 +119,14 @@
import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.Stack;
import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_HIGH;
public abstract class BaseStatusBar extends SystemUI implements
CommandQueue.Callbacks, ActivatableNotificationView.OnActivatedListener,
ExpandableNotificationRow.ExpansionLogger, NotificationData.Environment,
- ExpandableNotificationRow.OnExpandClickListener,
- OnGutsClosedListener {
+ ExpandableNotificationRow.OnExpandClickListener, OnGutsClosedListener {
public static final String TAG = "StatusBar";
public static final boolean DEBUG = false;
public static final boolean MULTIUSER_DEBUG = false;
@@ -179,6 +180,9 @@
// for heads up notifications
protected HeadsUpManager mHeadsUpManager;
+ // handling reordering
+ protected VisualStabilityManager mVisualStabilityManager = new VisualStabilityManager();
+
protected int mCurrentUserId = 0;
final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>();
@@ -2245,9 +2249,8 @@
*/
protected void updateRowStates() {
mKeyguardIconOverflowContainer.getIconsView().removeAllViews();
+ final int N = mStackScroller.getChildCount();
- ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
- final int N = activeNotifications.size();
int visibleNotifications = 0;
boolean onKeyguard = mState == StatusBarState.KEYGUARD;
@@ -2255,14 +2258,23 @@
if (onKeyguard) {
maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
}
- for (int i = 0; i < N; i++) {
- NotificationData.Entry entry = activeNotifications.get(i);
+ Stack<ExpandableNotificationRow> stack = new Stack<>();
+ for (int i = N - 1; i >= 0; i--) {
+ View child = mStackScroller.getChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ continue;
+ }
+ stack.push((ExpandableNotificationRow) child);
+ }
+ while(!stack.isEmpty()) {
+ ExpandableNotificationRow row = stack.pop();
+ NotificationData.Entry entry = row.getEntry();
boolean childNotification = mGroupManager.isChildInGroupWithSummary(entry.notification);
if (onKeyguard) {
- entry.row.setOnKeyguard(true);
+ row.setOnKeyguard(true);
} else {
- entry.row.setOnKeyguard(false);
- entry.row.setSystemExpanded(visibleNotifications == 0 && !childNotification);
+ row.setOnKeyguard(false);
+ row.setSystemExpanded(visibleNotifications == 0 && !childNotification);
}
boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
entry.notification) && !entry.row.isRemoved();
@@ -2289,6 +2301,14 @@
visibleNotifications++;
}
}
+ if (row.isSummaryWithChildren()) {
+ List<ExpandableNotificationRow> notificationChildren =
+ row.getNotificationChildren();
+ int size = notificationChildren.size();
+ for (int i = size - 1; i >= 0; i--) {
+ stack.push(notificationChildren.get(i));
+ }
+ }
}
mStackScroller.updateOverflowContainerVisibility(onKeyguard
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index caf5447..b5036ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -48,6 +48,7 @@
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.statusbar.notification.HybridNotificationView;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
@@ -318,6 +319,10 @@
return mStatusBarNotification;
}
+ public NotificationData.Entry getEntry() {
+ return mEntry;
+ }
+
public boolean isHeadsUp() {
return mIsHeadsUp;
}
@@ -451,10 +456,15 @@
* Apply the order given in the list to the children.
*
* @param childOrder the new list order
+ * @param visualStabilityManager
+ * @param callback the callback to invoked in case it is not allowed
* @return whether the list order has changed
*/
- public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
- return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder);
+ public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
+ VisualStabilityManager visualStabilityManager,
+ VisualStabilityManager.Callback callback) {
+ return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder,
+ visualStabilityManager, callback);
}
public void getChildrenStates(StackScrollState resultState) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
new file mode 100644
index 0000000..4a52acc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * An object that can determine the visibility of a Notification.
+ */
+public interface VisibilityLocationProvider {
+
+ /**
+ * @return whether the view is in a visible location right now.
+ */
+ boolean isInVisibleLocation(ExpandableNotificationRow row);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisualStabilityManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisualStabilityManager.java
new file mode 100644
index 0000000..eaf552c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisualStabilityManager.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.util.ArraySet;
+import android.view.View;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+
+import java.util.ArrayList;
+
+/**
+ * A manager that ensures that notifications are visually stable. It will suppress reorderings
+ * and reorder at the right time when they are out of view.
+ */
+public class VisualStabilityManager implements OnHeadsUpChangedListener {
+
+ private final ArrayList<Callback> mCallbacks = new ArrayList<>();
+
+ private boolean mPanelExpanded;
+ private boolean mScreenOn;
+ private boolean mReorderingAllowed;
+ private VisibilityLocationProvider mVisibilityLocationProvider;
+ private ArraySet<View> mAllowedReorderViews = new ArraySet<>();
+ private ArraySet<View> mAddedChildren = new ArraySet<>();
+
+ /**
+ * Add a callback to invoke when reordering is allowed again.
+ * @param callback
+ */
+ public void addReorderingAllowedCallback(Callback callback) {
+ if (mCallbacks.contains(callback)) {
+ return;
+ }
+ mCallbacks.add(callback);
+ }
+
+ /**
+ * Set the panel to be expanded.
+ */
+ public void setPanelExpanded(boolean expanded) {
+ mPanelExpanded = expanded;
+ updateReorderingAllowed();
+ }
+
+ /**
+ * @param screenOn whether the screen is on
+ */
+ public void setScreenOn(boolean screenOn) {
+ mScreenOn = screenOn;
+ updateReorderingAllowed();
+ }
+
+ private void updateReorderingAllowed() {
+ boolean reorderingAllowed = !mScreenOn || !mPanelExpanded;
+ boolean changed = reorderingAllowed && !mReorderingAllowed;
+ mReorderingAllowed = reorderingAllowed;
+ if (changed) {
+ notifyCallbacks();
+ }
+ }
+
+ private void notifyCallbacks() {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ Callback callback = mCallbacks.get(i);
+ callback.onReorderingAllowed();
+ }
+ mCallbacks.clear();
+ }
+
+ /**
+ * @return whether reordering is currently allowed in general.
+ */
+ public boolean isReorderingAllowed() {
+ return mReorderingAllowed;
+ }
+
+ /**
+ * @return whether a specific notification is allowed to reorder. Certain notifications are
+ * allowed to reorder even if {@link #isReorderingAllowed()} returns false, like newly added
+ * notifications or heads-up notifications that are out of view.
+ */
+ public boolean canReorderNotification(ExpandableNotificationRow row) {
+ if (mReorderingAllowed) {
+ return true;
+ }
+ if (mAddedChildren.contains(row)) {
+ return true;
+ }
+ if (mAllowedReorderViews.contains(row)
+ && !mVisibilityLocationProvider.isInVisibleLocation(row)) {
+ return true;
+ }
+ return false;
+ }
+
+ public void setVisibilityLocationProvider(
+ VisibilityLocationProvider visibilityLocationProvider) {
+ mVisibilityLocationProvider = visibilityLocationProvider;
+ }
+
+ public void onReorderingFinished() {
+ mAllowedReorderViews.clear();
+ mAddedChildren.clear();
+ }
+
+ @Override
+ public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+ if (isHeadsUp) {
+ // Heads up notifications should in general be allowed to reorder if they are out of
+ // view and stay at the current location if they aren't.
+ mAllowedReorderViews.add(entry.row);
+ }
+ }
+
+ /**
+ * Notify the visual stability manager that a new view was added and should be allowed to
+ * reorder next time.
+ */
+ public void notifyViewAddition(View view) {
+ mAddedChildren.add(view);
+ }
+
+ public interface Callback {
+ /**
+ * Called when reordering is allowed again.
+ */
+ void onReorderingAllowed();
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
index 2c8339a..f25e599 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -23,6 +23,7 @@
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -35,7 +36,7 @@
/**
* A class to handle notifications and their corresponding groups.
*/
-public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChangedListener {
+public class NotificationGroupManager implements OnHeadsUpChangedListener {
private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
private OnGroupChangeListener mListener;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index ede6fd0..9c700b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -61,6 +61,7 @@
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.stack.StackStateAnimator;
@@ -70,7 +71,7 @@
ExpandableView.OnHeightChangedListener,
View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener,
- HeadsUpManager.OnHeadsUpChangedListener {
+ OnHeadsUpChangedListener {
private static final boolean DEBUG = false;
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 3025359..bb5e69c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -167,6 +167,7 @@
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.SignalClusterView;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
import com.android.systemui.statusbar.policy.AccessibilityController;
import com.android.systemui.statusbar.policy.BatteryController;
@@ -185,6 +186,7 @@
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.NetworkControllerImpl;
import com.android.systemui.statusbar.policy.NextAlarmController;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.policy.PreviewInflater;
import com.android.systemui.statusbar.policy.RotationLockControllerImpl;
import com.android.systemui.statusbar.policy.SecurityControllerImpl;
@@ -195,7 +197,6 @@
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout
.OnChildLocationsChangedListener;
import com.android.systemui.statusbar.stack.StackStateAnimator;
-import com.android.systemui.statusbar.stack.StackViewState;
import com.android.systemui.volume.VolumeComponent;
import java.io.FileDescriptor;
@@ -210,7 +211,7 @@
public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener,
- HeadsUpManager.OnHeadsUpChangedListener {
+ OnHeadsUpChangedListener, VisualStabilityManager.Callback {
static final String TAG = "PhoneStatusBar";
public static final boolean DEBUG = BaseStatusBar.DEBUG;
public static final boolean SPEW = false;
@@ -565,9 +566,6 @@
*/
protected boolean mStartedGoingToSleep;
- private static final int VISIBLE_LOCATIONS = StackViewState.LOCATION_FIRST_HUN
- | StackViewState.LOCATION_MAIN_AREA;
-
private final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
new OnChildLocationsChangedListener() {
@Override
@@ -616,8 +614,7 @@
for (int i = 0; i < N; i++) {
Entry entry = activeNotifications.get(i);
String key = entry.notification.getKey();
- boolean isVisible =
- (mStackScroller.getChildLocation(entry.row) & VISIBLE_LOCATIONS) != 0;
+ boolean isVisible = mStackScroller.isInVisibleLocation(entry.row);
NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible);
boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
if (isVisible) {
@@ -776,9 +773,11 @@
mHeadsUpManager.addListener(this);
mHeadsUpManager.addListener(mNotificationPanel);
mHeadsUpManager.addListener(mGroupManager);
+ mHeadsUpManager.addListener(mVisualStabilityManager);
mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
mNotificationData.setHeadsUpManager(mHeadsUpManager);
mGroupManager.setHeadsUpManager(mHeadsUpManager);
+ mHeadsUpManager.setVisualStabilityManager(mVisualStabilityManager);
if (MULTIUSER_DEBUG) {
mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById(
@@ -808,6 +807,7 @@
mStackScroller.setGroupManager(mGroupManager);
mStackScroller.setHeadsUpManager(mHeadsUpManager);
mGroupManager.setOnGroupChangeListener(mStackScroller);
+ mVisualStabilityManager.setVisibilityLocationProvider(mStackScroller);
inflateOverflowContainer();
inflateEmptyShadeView();
@@ -1753,6 +1753,11 @@
final int N = activeNotifications.size();
for (int i=0; i<N; i++) {
Entry ent = activeNotifications.get(i);
+ if (ent.row.isDismissed() || ent.row.isRemoved()) {
+ // we don't want to update removed notifications because they could
+ // temporarily become children if they were isolated before.
+ continue;
+ }
int vis = ent.notification.getNotification().visibility;
// Display public version of the notification if we need to redact.
@@ -1816,6 +1821,7 @@
for (int i=0; i<toShow.size(); i++) {
View v = toShow.get(i);
if (v.getParent() == null) {
+ mVisualStabilityManager.notifyViewAddition(v);
mStackScroller.addView(v);
}
}
@@ -1837,12 +1843,17 @@
if (child != targetChild) {
// Oops, wrong notification at this position. Put the right one
// here and advance both lists.
- mStackScroller.changeViewPosition(targetChild, i);
+ if (mVisualStabilityManager.canReorderNotification(targetChild)) {
+ mStackScroller.changeViewPosition(targetChild, i);
+ } else {
+ mVisualStabilityManager.addReorderingAllowedCallback(this);
+ }
}
j++;
}
+ mVisualStabilityManager.onReorderingFinished();
// clear the map again for the next usage
mTmpChildOrderMap.clear();
@@ -1886,13 +1897,14 @@
childIndex++) {
ExpandableNotificationRow childView = orderedChildren.get(childIndex);
if (children == null || !children.contains(childView)) {
+ mVisualStabilityManager.notifyViewAddition(childView);
parent.addChildNotification(childView, childIndex);
mStackScroller.notifyGroupChildAdded(childView);
}
}
// Finally after removing and adding has been beformed we can apply the order.
- orderChanged |= parent.applyChildOrder(orderedChildren);
+ orderChanged |= parent.applyChildOrder(orderedChildren, mVisualStabilityManager, this);
}
if (orderChanged) {
mStackScroller.generateChildOrderChangedEvent();
@@ -1969,7 +1981,7 @@
}
private void updateSpeedbump() {
- int speedbumpIndex = -1;
+ int speedBumpIndex = 0;
int currentIndex = 0;
final int N = mStackScroller.getChildCount();
for (int i = 0; i < N; i++) {
@@ -1978,13 +1990,12 @@
continue;
}
ExpandableNotificationRow row = (ExpandableNotificationRow) view;
- if (mNotificationData.isAmbient(row.getStatusBarNotification().getKey())) {
- speedbumpIndex = currentIndex;
- break;
- }
currentIndex++;
+ if (!mNotificationData.isAmbient(row.getStatusBarNotification().getKey())) {
+ speedBumpIndex = currentIndex;
+ }
}
- mStackScroller.updateSpeedBumpIndex(speedbumpIndex);
+ mStackScroller.updateSpeedBumpIndex(speedBumpIndex);
}
public static boolean isTopLevelChild(Entry entry) {
@@ -2654,7 +2665,7 @@
public void setPanelExpanded(boolean isExpanded) {
mStatusBarWindowManager.setPanelExpanded(isExpanded);
-
+ mVisualStabilityManager.setPanelExpanded(isExpanded);
if (isExpanded && getBarState() != StatusBarState.KEYGUARD) {
if (DEBUG) {
Log.v(TAG, "clearing notification effects from setPanelExpanded");
@@ -2680,6 +2691,11 @@
mFalsingManager.onScreenOff();
}
+ @Override
+ public void onReorderingAllowed() {
+ updateNotifications();
+ }
+
/**
* All changes to the status bar and notifications funnel through here and are batched.
*/
@@ -2904,7 +2920,6 @@
mNotificationPanel.closeQs();
mExpandedVisible = false;
-
visibilityChanged(false);
// Shrink the window to the size of the status bar only
@@ -4806,6 +4821,7 @@
mWakeUpComingFromTouch = false;
mWakeUpTouchLocation = null;
mStackScroller.setAnimationsEnabled(false);
+ mVisualStabilityManager.setScreenOn(false);
updateVisibleToUser();
if (mLaunchCameraOnFinishedGoingToSleep) {
mLaunchCameraOnFinishedGoingToSleep = false;
@@ -4824,6 +4840,7 @@
public void onStartedWakingUp() {
mDeviceInteractive = true;
mStackScroller.setAnimationsEnabled(true);
+ mVisualStabilityManager.setScreenOn(true);
mNotificationPanel.setTouchDisabled(false);
updateVisibleToUser();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 35e084d..9f2d446 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -35,7 +35,7 @@
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.ScrimView;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.stack.StackStateAnimator;
/**
@@ -43,7 +43,7 @@
* security method gets shown).
*/
public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
- HeadsUpManager.OnHeadsUpChangedListener {
+ OnHeadsUpChangedListener {
public static final long ANIMATION_DURATION = 220;
public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR
= new PathInterpolator(0f, 0, 0.7f, 1f);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
index f6c0942..0b3231e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -23,6 +23,7 @@
import android.os.SystemClock;
import android.provider.Settings;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Pools;
import android.view.View;
@@ -33,6 +34,7 @@
import com.android.systemui.R;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
@@ -48,7 +50,8 @@
* A manager which handles heads up notifications which is a special mode where
* they simply peek from the top of the screen.
*/
-public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener {
+public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener,
+ VisualStabilityManager.Callback {
private static final String TAG = "HeadsUpManager";
private static final boolean DEBUG = false;
private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
@@ -96,6 +99,8 @@
private boolean mReleaseOnExpandFinish;
private boolean mTrackingHeadsUp;
private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
+ private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
+ = new ArraySet<>();
private boolean mIsExpanded;
private boolean mHasPinnedNotification;
private int[] mTmpTwoArray = new int[2];
@@ -103,6 +108,7 @@
private boolean mWaitingOnCollapseWhenGoingAway;
private boolean mIsObserving;
private boolean mRemoteInputActive;
+ private VisualStabilityManager mVisualStabilityManager;
public HeadsUpManager(final Context context, View statusBarWindowView,
NotificationGroupManager groupManager) {
@@ -602,6 +608,21 @@
}
}
+ @Override
+ public void onReorderingAllowed() {
+ for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
+ if (isHeadsUp(entry.key)) {
+ // Maybe the heads-up was removed already
+ removeHeadsUpEntry(entry);
+ }
+ }
+ mEntriesToRemoveWhenReorderingAllowed.clear();
+ }
+
+ public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) {
+ mVisualStabilityManager = visualStabilityManager;
+ }
+
/**
* This represents a notification and how long it is in a heads up mode. It also manages its
* lifecycle automatically when created.
@@ -622,7 +643,10 @@
mRemoveHeadsUpRunnable = new Runnable() {
@Override
public void run() {
- if (!mTrackingHeadsUp) {
+ if (!mVisualStabilityManager.isReorderingAllowed()) {
+ mEntriesToRemoveWhenReorderingAllowed.add(entry);
+ mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManager.this);
+ } else if (!mTrackingHeadsUp) {
removeHeadsUpEntry(entry);
} else {
mEntriesToRemoveAfterExpand.add(entry);
@@ -646,6 +670,9 @@
if (mEntriesToRemoveAfterExpand.contains(entry)) {
mEntriesToRemoveAfterExpand.remove(entry);
}
+ if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
+ mEntriesToRemoveWhenReorderingAllowed.remove(entry);
+ }
if (!isSticky()) {
long finishTime = postTime + mHeadsUpNotificationDecay;
long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
@@ -715,30 +742,4 @@
}
}
- public interface OnHeadsUpChangedListener {
- /**
- * The state whether there exist pinned heads-ups or not changed.
- *
- * @param inPinnedMode whether there are any pinned heads-ups
- */
- void onHeadsUpPinnedModeChanged(boolean inPinnedMode);
-
- /**
- * A notification was just pinned to the top.
- */
- void onHeadsUpPinned(ExpandableNotificationRow headsUp);
-
- /**
- * A notification was just unpinned from the top.
- */
- void onHeadsUpUnPinned(ExpandableNotificationRow headsUp);
-
- /**
- * A notification just became a heads up or turned back to its normal state.
- *
- * @param entry the entry of the changed notification
- * @param isHeadsUp whether the notification is now a headsUp notification
- */
- void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
new file mode 100644
index 0000000..5444f06
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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 com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+
+/**
+ * A listener to heads up changes
+ */
+public interface OnHeadsUpChangedListener {
+ /**
+ * The state whether there exist pinned heads-ups or not changed.
+ *
+ * @param inPinnedMode whether there are any pinned heads-ups
+ */
+ default void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {}
+
+ /**
+ * A notification was just pinned to the top.
+ */
+ default void onHeadsUpPinned(ExpandableNotificationRow headsUp) {}
+
+ /**
+ * A notification was just unpinned from the top.
+ */
+ default void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {}
+
+ /**
+ * A notification just became a heads up or turned back to its normal state.
+ *
+ * @param entry the entry of the changed notification
+ * @param isHeadsUp whether the notification is now a headsUp notification
+ */
+ default void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
index d7920a9..24aae38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -38,6 +38,7 @@
import com.android.systemui.statusbar.notification.HybridNotificationView;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.NotificationViewWrapper;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NotificationPanelView;
import java.util.ArrayList;
@@ -315,9 +316,13 @@
* Apply the order given in the list to the children.
*
* @param childOrder the new list order
+ * @param visualStabilityManager
+ * @param callback
* @return whether the list order has changed
*/
- public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
+ public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
+ VisualStabilityManager visualStabilityManager,
+ VisualStabilityManager.Callback callback) {
if (childOrder == null) {
return false;
}
@@ -326,9 +331,13 @@
ExpandableNotificationRow child = mChildren.get(i);
ExpandableNotificationRow desiredChild = childOrder.get(i);
if (child != desiredChild) {
- mChildren.remove(desiredChild);
- mChildren.add(i, desiredChild);
- result = true;
+ if (visualStabilityManager.canReorderNotification(desiredChild)) {
+ mChildren.remove(desiredChild);
+ mChildren.add(i, desiredChild);
+ result = true;
+ } else {
+ visualStabilityManager.addReorderingAllowedCallback(callback);
+ }
}
}
updateExpansionStates();
@@ -484,6 +493,7 @@
}
childState.location = parentState.location;
yPosition += intrinsicHeight;
+
}
if (mOverflowNumber != null) {
ExpandableNotificationRow overflowView = mChildren.get(Math.min(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index a6fe438..8029eab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -75,6 +75,7 @@
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import com.android.systemui.statusbar.phone.ScrimController;
@@ -92,7 +93,7 @@
public class NotificationStackScrollLayout extends ViewGroup
implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener,
- SettingsIconRowListener, ScrollContainer {
+ SettingsIconRowListener, ScrollContainer, VisibilityLocationProvider {
public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
private static final String TAG = "StackScroller";
@@ -536,21 +537,19 @@
mListener = listener;
}
- /**
- * Returns the location the given child is currently rendered at.
- *
- * @param child the child to get the location for
- * @return one of {@link StackViewState}'s <code>LOCATION_*</code> constants
- */
- public int getChildLocation(View child) {
- StackViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
+ @Override
+ public boolean isInVisibleLocation(ExpandableNotificationRow row) {
+ StackViewState childViewState = mCurrentStackScrollState.getViewStateForView(row);
if (childViewState == null) {
- return StackViewState.LOCATION_UNKNOWN;
+ return false;
}
- if (childViewState.gone) {
- return StackViewState.LOCATION_GONE;
+ if ((childViewState.location &= StackViewState.VISIBLE_LOCATIONS) == 0) {
+ return false;
}
- return childViewState.location;
+ if (row.getVisibility() != View.VISIBLE) {
+ return false;
+ }
+ return true;
}
private void setMaxLayoutHeight(int maxLayoutHeight) {
@@ -4581,5 +4580,4 @@
return length;
}
}
-
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackViewState.java
index ecdee4e..f22a410 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackViewState.java
@@ -31,6 +31,10 @@
public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x10;
/** The view isn't layouted at all. */
public static final int LOCATION_GONE = 0x40;
+ /**
+ * The visible locations of a view.
+ */
+ public static final int VISIBLE_LOCATIONS = LOCATION_FIRST_HUN | LOCATION_MAIN_AREA;
public int height;
public boolean dimmed;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notification/VisualStabilityManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/notification/VisualStabilityManagerTest.java
new file mode 100644
index 0000000..3b8d6b5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notification/VisualStabilityManagerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.service.notification.StatusBarNotification;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+public class VisualStabilityManagerTest extends SysuiTestCase {
+
+ private VisualStabilityManager mVisualStabilityManager = new VisualStabilityManager();
+ private VisualStabilityManager.Callback mCallback = mock(VisualStabilityManager.Callback.class);
+ private VisibilityLocationProvider mLocationProvider = mock(VisibilityLocationProvider.class);
+ private ExpandableNotificationRow mRow = mock(ExpandableNotificationRow.class);
+ private NotificationData.Entry mEntry;
+
+ @Before
+ public void setUp() {
+ mVisualStabilityManager.setVisibilityLocationProvider(mLocationProvider);
+ mEntry = new NotificationData.Entry(mock(StatusBarNotification.class),
+ mock(StatusBarIconView.class));
+ mEntry.row = mRow;
+ }
+
+ public void testPanelExpansion() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), false);
+ mVisualStabilityManager.setPanelExpanded(false);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), true);
+ }
+
+ public void testScreenOn() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), false);
+ mVisualStabilityManager.setScreenOn(false);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), true);
+ }
+
+ @Test
+ public void testReorderingAllowedChangesScreenOn() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ assertEquals(mVisualStabilityManager.isReorderingAllowed(), false);
+ mVisualStabilityManager.setScreenOn(false);
+ assertEquals(mVisualStabilityManager.isReorderingAllowed(), true);
+ }
+
+ public void testReorderingAllowedChangesPanel() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ assertEquals(mVisualStabilityManager.isReorderingAllowed(), false);
+ mVisualStabilityManager.setPanelExpanded(false);
+ assertEquals(mVisualStabilityManager.isReorderingAllowed(), true);
+ }
+
+ public void testCallBackCalledScreenOn() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.addReorderingAllowedCallback(mCallback);
+ mVisualStabilityManager.setScreenOn(false);
+ verify(mCallback).onReorderingAllowed();
+ }
+
+ public void testCallBackCalledPanelExpanded() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.addReorderingAllowedCallback(mCallback);
+ mVisualStabilityManager.setPanelExpanded(false);
+ verify(mCallback).onReorderingAllowed();
+ }
+
+ public void testCallBackExactlyOnce() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.addReorderingAllowedCallback(mCallback);
+ mVisualStabilityManager.setScreenOn(false);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.setScreenOn(false);
+ verify(mCallback).onReorderingAllowed();
+ }
+
+ public void testAddedCanReorder() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.notifyViewAddition(mRow);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), true);
+ }
+
+ public void testReorderingVisibleHeadsUpNotAllowed() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ when(mLocationProvider.isInVisibleLocation(anyObject())).thenReturn(true);
+ mVisualStabilityManager.onHeadsUpStateChanged(mEntry, true);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), false);
+ }
+
+ public void testReorderingVisibleHeadsUpAllowed() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ when(mLocationProvider.isInVisibleLocation(anyObject())).thenReturn(false);
+ mVisualStabilityManager.onHeadsUpStateChanged(mEntry, true);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), true);
+ }
+
+ public void testReorderingVisibleHeadsUpAllowedOnce() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ when(mLocationProvider.isInVisibleLocation(anyObject())).thenReturn(false);
+ mVisualStabilityManager.onHeadsUpStateChanged(mEntry, true);
+ mVisualStabilityManager.onReorderingFinished();
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), false);
+ }
+}