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);
+    }
+}