Merge "Make LegacyTypeTracker testable" into qt-dev
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 2906710..0e10de8 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -510,7 +510,7 @@
      * The absence of a connection type.
      * @hide
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     public static final int TYPE_NONE        = -1;
 
     /**
@@ -627,7 +627,7 @@
      * {@hide}
      */
     @Deprecated
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     public static final int TYPE_MOBILE_FOTA = 10;
 
     /**
@@ -645,7 +645,7 @@
      * {@hide}
      */
     @Deprecated
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     public static final int TYPE_MOBILE_CBS  = 12;
 
     /**
@@ -655,7 +655,7 @@
      * {@hide}
      */
     @Deprecated
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     public static final int TYPE_WIFI_P2P    = 13;
 
     /**
@@ -674,7 +674,7 @@
      * {@hide}
      */
     @Deprecated
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     public static final int TYPE_MOBILE_EMERGENCY = 15;
 
     /**
@@ -775,7 +775,7 @@
      */
     public static final String PRIVATE_DNS_DEFAULT_MODE_FALLBACK = PRIVATE_DNS_MODE_OPPORTUNISTIC;
 
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     private final IConnectivityManager mService;
     /**
      * A kludge to facilitate static access where a Context pointer isn't available, like in the
@@ -867,7 +867,7 @@
      * {@hide}
      */
     @Deprecated
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     public static boolean isNetworkTypeMobile(int networkType) {
         switch (networkType) {
             case TYPE_MOBILE:
@@ -1304,7 +1304,7 @@
      */
     @Deprecated
     @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     public LinkProperties getLinkProperties(int networkType) {
         try {
             return mService.getLinkPropertiesForType(networkType);
@@ -3042,7 +3042,7 @@
      */
     @Deprecated
     @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     public boolean isNetworkSupported(int networkType) {
         try {
             return mService.isNetworkSupported(networkType);
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index 4332d8a..1c6a484 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -25,6 +25,7 @@
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.media.MediaPlayer;
+import android.os.Build;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.DataUnit;
@@ -169,7 +170,7 @@
 
     private static INetworkStatsService sStatsService;
 
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     private synchronized static INetworkStatsService getStatsService() {
         if (sStatsService == null) {
             sStatsService = INetworkStatsService.Stub.asInterface(
@@ -979,7 +980,7 @@
      * Interfaces are never removed from this list, so counters should always be
      * monotonic.
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     private static String[] getMobileIfaces() {
         try {
             return getStatsService().getMobileIfaces();
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b3e94e3..21a8f4c 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -572,6 +572,15 @@
     <!-- Integer size limit, in KB, for a single WifiLogger ringbuffer, in verbose logging mode -->
     <integer translatable="false" name="config_wifi_logger_ring_buffer_verbose_size_limit_kb">1024</integer>
 
+    <!-- Array indicating wifi fatal firmware alert error code list from driver -->
+    <integer-array translatable="false" name="config_wifi_fatal_firmware_alert_error_code_list">
+        <!-- Example:
+        <item>0</item>
+        <item>1</item>
+        <item>2</item>
+        -->
+    </integer-array>
+
     <!-- Boolean indicating whether or not wifi should turn off when emergency call is made -->
     <bool translatable="false" name="config_wifi_turn_off_during_emergency_call">false</bool>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 2f34c94..186b84c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -381,6 +381,7 @@
   <java-symbol type="bool" name="config_wifi_revert_country_code_on_cellular_loss" />
   <java-symbol type="integer" name="config_wifi_logger_ring_buffer_default_size_limit_kb" />
   <java-symbol type="integer" name="config_wifi_logger_ring_buffer_verbose_size_limit_kb" />
+  <java-symbol type="array" name="config_wifi_fatal_firmware_alert_error_code_list" />
   <java-symbol type="bool" name="config_wifi_turn_off_during_emergency_call" />
   <java-symbol type="bool" name="config_supportMicNearUltrasound" />
   <java-symbol type="bool" name="config_supportSpeakerNearUltrasound" />
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index efebfc2..a7f8933 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -16,11 +16,15 @@
 
 package com.android.systemui.statusbar.car;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.car.drivingstate.CarDrivingStateEvent;
 import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.Log;
 import android.view.GestureDetector;
@@ -55,6 +59,7 @@
 import com.android.systemui.qs.car.CarQSFragment;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.statusbar.FlingAnimationUtils;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.car.hvac.HvacController;
 import com.android.systemui.statusbar.car.hvac.TemperatureView;
@@ -74,6 +79,15 @@
 public class CarStatusBar extends StatusBar implements
         CarBatteryController.BatteryViewHandler {
     private static final String TAG = "CarStatusBar";
+    // used to calculate how fast to open or close the window
+    private static final float DEFAULT_FLING_VELOCITY = 0;
+    // max time a fling animation takes
+    private static final float FLING_ANIMATION_MAX_TIME = 0.5f;
+    // acceleration rate for the fling animation
+    private static final float FLING_SPEED_UP_FACTOR = 0.6f;
+
+    private float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
+    private float mClosingVelocity = DEFAULT_FLING_VELOCITY;
 
     private TaskStackListenerImpl mTaskStackListener;
 
@@ -100,6 +114,7 @@
     private boolean mDeviceIsProvisioned = true;
     private HvacController mHvacController;
     private DrivingStateHelper mDrivingStateHelper;
+    private static FlingAnimationUtils sFlingAnimationUtils;
     private SwitchToGuestTimer mSwitchToGuestTimer;
 
     // The container for the notifications.
@@ -113,6 +128,27 @@
     // it's open.
     private View.OnTouchListener mNavBarNotificationTouchListener;
 
+    // Percentage from top of the screen after which the notification shade will open. This value
+    // will be used while opening the notification shade.
+    private int mSettleOpenPercentage;
+    // Percentage from top of the screen below which the notification shade will close. This
+    // value will be used while closing the notification shade.
+    private int mSettleClosePercentage;
+    // Percentage of notification shade open from top of the screen.
+    private int mPercentageFromBottom;
+    // If notification shade is animation to close or to open.
+    private boolean mIsNotificationAnimating;
+
+    // Tracks when the notification shade is being scrolled. This refers to the glass pane being
+    // scrolled not the recycler view.
+    private boolean mIsTracking;
+    private float mFirstTouchDownOnGlassPane;
+
+    // If the notification card inside the recycler view is being swiped.
+    private boolean mIsNotificationCardSwiping;
+    // If notification shade is being swiped vertically to close.
+    private boolean mIsSwipingVerticallyToClose;
+
     @Override
     public void start() {
         // get the provisioned state before calling the parent class since it's that flow that
@@ -125,6 +161,12 @@
         mActivityManagerWrapper.registerTaskStackListener(mTaskStackListener);
 
         mNotificationPanel.setScrollingEnabled(true);
+        mSettleOpenPercentage = mContext.getResources().getInteger(
+                R.integer.notification_settle_open_percentage);
+        mSettleClosePercentage = mContext.getResources().getInteger(
+                R.integer.notification_settle_close_percentage);
+        sFlingAnimationUtils = new FlingAnimationUtils(mContext,
+                FLING_ANIMATION_MAX_TIME, FLING_SPEED_UP_FACTOR);
 
         createBatteryController();
         mCarBatteryController.startListening();
@@ -313,14 +355,46 @@
                     }
                 });
         mNavBarNotificationTouchListener =
-                (v, event) -> navBarCloseNotificationGestureDetector.onTouchEvent(event);
+                (v, event) -> {
+                    boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event);
+                    if (consumed) {
+                        return true;
+                    }
+                    if (event.getActionMasked() == MotionEvent.ACTION_UP
+                            && mNotificationView.getVisibility() == View.VISIBLE) {
+                        if (mSettleClosePercentage < mPercentageFromBottom) {
+                            animateNotificationPanel(
+                                    DEFAULT_FLING_VELOCITY, false);
+                        } else {
+                            animateNotificationPanel(DEFAULT_FLING_VELOCITY,
+                                    true);
+                        }
+                    }
+                    return true;
+                };
 
         // The following are the ui elements that the user would call the status bar.
         // This will set the status bar so it they can make call backs.
         CarNavigationBarView topBar = mStatusBarWindow.findViewById(R.id.car_top_bar);
         topBar.setStatusBar(this);
-        topBar.setStatusBarWindowTouchListener((v1, event1) ->
-                openGestureDetector.onTouchEvent(event1));
+        topBar.setStatusBarWindowTouchListener((v1, event1) -> {
+
+                    boolean consumed = openGestureDetector.onTouchEvent(event1);
+                    if (consumed) {
+                        return true;
+                    }
+                    if (event1.getActionMasked() == MotionEvent.ACTION_UP
+                            && mNotificationView.getVisibility() == View.VISIBLE) {
+                        if (mSettleOpenPercentage > mPercentageFromBottom) {
+                            animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
+                        } else {
+                            animateNotificationPanel(
+                                    DEFAULT_FLING_VELOCITY, false);
+                        }
+                    }
+                    return true;
+                }
+        );
 
         NotificationClickHandlerFactory clickHandlerFactory = new NotificationClickHandlerFactory(
                 mBarService,
@@ -347,7 +421,10 @@
                 mNotificationListAtBottomAtTimeOfTouch = false;
             }
             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                mFirstTouchDownOnGlassPane = event.getRawX();
                 mNotificationListAtBottomAtTimeOfTouch = mNotificationListAtBottom;
+                // Reset the tracker when there is a touch down on the glass pane.
+                mIsTracking = false;
                 // Pass the down event to gesture detector so that it knows where the touch event
                 // started.
                 closeGestureDetector.onTouchEvent(event);
@@ -364,23 +441,56 @@
                     return;
                 }
                 mNotificationListAtBottom = false;
+                mIsSwipingVerticallyToClose = false;
                 mNotificationListAtBottomAtTimeOfTouch = false;
             }
         });
         mNotificationList.setOnTouchListener(new View.OnTouchListener() {
             @Override
             public boolean onTouch(View v, MotionEvent event) {
+                mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX())
+                        > SWIPE_MAX_OFF_PATH;
+                if (mNotificationListAtBottomAtTimeOfTouch && mNotificationListAtBottom) {
+                    // We need to save the state here as if notification card is swiping we will
+                    // change the mNotificationListAtBottomAtTimeOfTouch. This is to protect
+                    // closing the notification shade while the notification card is being swiped.
+                    mIsSwipingVerticallyToClose = true;
+                }
+
+                // If the card is swiping we should not allow the notification shade to close.
+                // Hence setting mNotificationListAtBottomAtTimeOfTouch to false will stop that
+                // for us. We are also checking for mIsTracking because while swiping the
+                // notification shade to close if the user goes a bit horizontal while swiping
+                // upwards then also this should close.
+                if (mIsNotificationCardSwiping && !mIsTracking) {
+                    mNotificationListAtBottomAtTimeOfTouch = false;
+                }
+
                 boolean handled = false;
                 if (mNotificationListAtBottomAtTimeOfTouch && mNotificationListAtBottom) {
                     handled = closeGestureDetector.onTouchEvent(event);
                 }
+                boolean isTracking = mIsTracking;
+                Rect rect = mNotificationList.getClipBounds();
+                float clippedHeight = rect.bottom;
+                if (!handled && event.getActionMasked() == MotionEvent.ACTION_UP
+                        && mIsSwipingVerticallyToClose) {
+                    if (mSettleClosePercentage < mPercentageFromBottom && isTracking) {
+                        animateNotificationPanel(DEFAULT_FLING_VELOCITY, false);
+                    } else if (clippedHeight != mNotificationView.getHeight() && isTracking) {
+                        // this can be caused when user is at the end of the list and trying to
+                        // fling to top of the list by scrolling down.
+                        animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
+                    }
+                }
+
                 // Updating the mNotificationListAtBottomAtTimeOfTouch state has to be done after
                 // the event has been passed to the closeGestureDetector above, such that the
                 // closeGestureDetector sees the up event before the state has changed.
                 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
                     mNotificationListAtBottomAtTimeOfTouch = false;
                 }
-                return handled;
+                return handled || isTracking;
             }
         });
 
@@ -401,7 +511,9 @@
         mNotificationList.scrollToPosition(0);
         mStatusBarWindowController.setPanelVisible(true);
         mNotificationView.setVisibility(View.VISIBLE);
-        // let the status bar know that the panel is open
+
+        animateNotificationPanel(mOpeningVelocity, false);
+
         setPanelExpanded(true);
     }
 
@@ -415,12 +527,66 @@
         mStatusBarWindowController.setStatusBarFocusable(false);
         mStatusBarWindow.cancelExpandHelper();
         mStatusBarView.collapsePanel(true /* animate */, delayed, speedUpFactor);
-        mStatusBarWindowController.setPanelVisible(false);
-        mNotificationView.setVisibility(View.INVISIBLE);
-        // let the status bar know that the panel is cloased
+
+        animateNotificationPanel(mClosingVelocity, true);
+
+        if (!mIsTracking) {
+            mStatusBarWindowController.setPanelVisible(false);
+            mNotificationView.setVisibility(View.INVISIBLE);
+        }
+
         setPanelExpanded(false);
     }
 
+    /**
+     * Animates the notification shade from one position to other. This is used to either open or
+     * close the notification shade completely with a velocity. If the animation is to close the
+     * notification shade this method also makes the view invisible after animation ends.
+     */
+    private void animateNotificationPanel(float velocity, boolean isClosing) {
+        Rect rect = mNotificationList.getClipBounds();
+        if (rect == null) {
+            return;
+        }
+        float from = rect.bottom;
+        float to = 0;
+        if (!isClosing) {
+            to = mNotificationView.getHeight();
+        }
+        if (mIsNotificationAnimating) {
+            return;
+        }
+        mIsNotificationAnimating = true;
+        mIsTracking = true;
+        ValueAnimator animator = ValueAnimator.ofFloat(from, to);
+        animator.addUpdateListener(
+                animation -> {
+                    float animatedValue = (Float) animation.getAnimatedValue();
+                    setNotificationViewClipBounds((int) animatedValue);
+                });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                mIsNotificationAnimating = false;
+                mIsTracking = false;
+                mOpeningVelocity = DEFAULT_FLING_VELOCITY;
+                mClosingVelocity = DEFAULT_FLING_VELOCITY;
+                if (isClosing) {
+                    mStatusBarWindowController.setPanelVisible(false);
+                    mNotificationView.setVisibility(View.INVISIBLE);
+                    mNotificationList.setClipBounds(null);
+                    // let the status bar know that the panel is closed
+                    setPanelExpanded(false);
+                } else {
+                    // let the status bar know that the panel is open
+                    setPanelExpanded(true);
+                }
+            }
+        });
+        sFlingAnimationUtils.apply(animator, from, to, Math.abs(velocity));
+        animator.start();
+    }
 
     @Override
     protected QS createDefaultQSFragment() {
@@ -576,7 +742,7 @@
 
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        //When executing dump() funciton simultaneously, we need to serialize them
+        //When executing dump() function simultaneously, we need to serialize them
         //to get mStackScroller's position correctly.
         synchronized (mQueueLock) {
             pw.println("  mStackScroller: " + viewInfo(mStackScroller));
@@ -760,38 +926,80 @@
     }
 
     /** Returns true if the current user makes it through the setup wizard, false otherwise. */
-    public boolean getIsUserSetup(){
+    private boolean getIsUserSetup() {
         return mUserSetup;
     }
 
+    private void setNotificationViewClipBounds(int height) {
+        Rect clipBounds = new Rect();
+        clipBounds.set(0, 0, mNotificationView.getWidth(), height);
+        // Sets the clip region on the notification list view.
+        mNotificationList.setClipBounds(clipBounds);
 
-    // TODO: add settle down/up logic
+        if (mNotificationView.getHeight() > 0) {
+            // Calculates the alpha value for the background based on how much of the notification
+            // shade is visible to the user. When the notification shade is completely open then
+            // alpha value will be 1.
+            float alpha = (float) height / mNotificationView.getHeight();
+            Drawable background = mNotificationView.getBackground();
+
+            background.setAlpha((int) (alpha * 255));
+        }
+    }
+
+    private void calculatePercentageFromBottom(float height) {
+        if (mNotificationView.getHeight() > 0) {
+            mPercentageFromBottom = (int) Math.abs(
+                    height / mNotificationView.getHeight() * 100);
+        }
+    }
+
     private static final int SWIPE_UP_MIN_DISTANCE = 75;
     private static final int SWIPE_DOWN_MIN_DISTANCE = 25;
     private static final int SWIPE_MAX_OFF_PATH = 75;
     private static final int SWIPE_THRESHOLD_VELOCITY = 200;
+
     // Only responsible for open hooks. Since once the panel opens it covers all elements
     // there is no need to merge with close.
     private abstract class OpenNotificationGestureListener extends
             GestureDetector.SimpleOnGestureListener {
 
         @Override
+        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
+                float distanceY) {
+
+            if (mNotificationView.getVisibility() == View.INVISIBLE) {
+                // when the on-scroll is called for the first time to open.
+                mNotificationList.scrollToPosition(0);
+            }
+            mStatusBarWindowController.setPanelVisible(true);
+            mNotificationView.setVisibility(View.VISIBLE);
+
+            // clips the view for the notification shade when the user scrolls to open.
+            setNotificationViewClipBounds((int) event2.getRawY());
+
+            // Initially the scroll starts with height being zero. This checks protects from divide
+            // by zero error.
+            calculatePercentageFromBottom(event2.getRawY());
+
+            mIsTracking = true;
+            return true;
+        }
+
+
+        @Override
         public boolean onFling(MotionEvent event1, MotionEvent event2,
                 float velocityX, float velocityY) {
-            if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
-                    || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
-                // swipe was not vertical or was not fast enough
-                return false;
-            }
-            boolean isDown = velocityY > 0;
-            float distanceDelta = Math.abs(event1.getY() - event2.getY());
-            if (isDown && distanceDelta > SWIPE_DOWN_MIN_DISTANCE) {
+            if (velocityY > SWIPE_THRESHOLD_VELOCITY) {
+                mOpeningVelocity = velocityY;
                 openNotification();
                 return true;
             }
+            animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
 
             return false;
         }
+
         protected abstract void openNotification();
     }
 
@@ -800,35 +1008,84 @@
             GestureDetector.SimpleOnGestureListener {
 
         @Override
+        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
+                float distanceY) {
+            if (!mNotificationListAtBottomAtTimeOfTouch) {
+                return false;
+            }
+            float actualNotificationHeight =
+                    mNotificationView.getHeight() - (event1.getRawY() - event2.getRawY());
+            if (actualNotificationHeight > mNotificationView.getHeight()) {
+                actualNotificationHeight = mNotificationView.getHeight();
+            }
+            if (mNotificationView.getHeight() > 0) {
+                mPercentageFromBottom = (int) Math.abs(
+                        actualNotificationHeight / mNotificationView.getHeight() * 100);
+                boolean isUp = distanceY > 0;
+
+                // This check is to figure out if onScroll was called while swiping the card at
+                // bottom of the list. At that time we should not allow notification shade to
+                // close. We are also checking for the upwards swipe gesture here because it is
+                // possible if a user is closing the notification shade and while swiping starts
+                // to open again but does not fling. At that time we should allow the
+                // notification shade to close fully or else it would stuck in between.
+                if (Math.abs(mNotificationView.getHeight() - actualNotificationHeight)
+                        > SWIPE_DOWN_MIN_DISTANCE && isUp) {
+                    setNotificationViewClipBounds((int) actualNotificationHeight);
+                    mIsTracking = true;
+                } else if (!isUp) {
+                    setNotificationViewClipBounds((int) actualNotificationHeight);
+                }
+            }
+            // if we return true the the items in RV won't be scrollable.
+            return false;
+        }
+
+
+        @Override
         public boolean onFling(MotionEvent event1, MotionEvent event2,
                 float velocityX, float velocityY) {
+
             if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
                     || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
                 // swipe was not vertical or was not fast enough
                 return false;
             }
             boolean isUp = velocityY < 0;
-            float distanceDelta = Math.abs(event1.getY() - event2.getY());
-            if (isUp && distanceDelta > SWIPE_UP_MIN_DISTANCE) {
+            if (isUp) {
                 close();
                 return true;
+            } else {
+                // we should close the shade
+                animateNotificationPanel(velocityY, false);
             }
             return false;
         }
+
         protected abstract void close();
     }
 
-    // to be installed on the nav bars
+    // To be installed on the nav bars.
     private abstract class NavBarCloseNotificationGestureListener extends
             CloseNotificationGestureListener {
         @Override
         public boolean onSingleTapUp(MotionEvent e) {
+            mClosingVelocity = DEFAULT_FLING_VELOCITY;
             close();
             return super.onSingleTapUp(e);
         }
 
         @Override
+        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
+                float distanceY) {
+            calculatePercentageFromBottom(event2.getRawY());
+            setNotificationViewClipBounds((int) event2.getRawY());
+            return true;
+        }
+
+        @Override
         public void onLongPress(MotionEvent e) {
+            mClosingVelocity = DEFAULT_FLING_VELOCITY;
             close();
             super.onLongPress(e);
         }
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynandroid/BootCompletedReceiver.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/BootCompletedReceiver.java
similarity index 100%
rename from packages/DynamicSystemInstallationService/src/com/android/dynandroid/BootCompletedReceiver.java
rename to packages/DynamicSystemInstallationService/src/com/android/dynsystem/BootCompletedReceiver.java
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynandroid/DynamicSystemInstallationService.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
similarity index 100%
rename from packages/DynamicSystemInstallationService/src/com/android/dynandroid/DynamicSystemInstallationService.java
rename to packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynandroid/InstallationAsyncTask.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java
similarity index 100%
rename from packages/DynamicSystemInstallationService/src/com/android/dynandroid/InstallationAsyncTask.java
rename to packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynandroid/VerificationActivity.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/VerificationActivity.java
similarity index 100%
rename from packages/DynamicSystemInstallationService/src/com/android/dynandroid/VerificationActivity.java
rename to packages/DynamicSystemInstallationService/src/com/android/dynsystem/VerificationActivity.java
diff --git a/packages/NetworkStack/src/android/net/util/NetworkStackUtils.java b/packages/NetworkStack/src/android/net/util/NetworkStackUtils.java
index 8226787..97e8186 100644
--- a/packages/NetworkStack/src/android/net/util/NetworkStackUtils.java
+++ b/packages/NetworkStack/src/android/net/util/NetworkStackUtils.java
@@ -35,6 +35,34 @@
     // TODO: Refer to DeviceConfig definition.
     public static final String NAMESPACE_CONNECTIVITY = "connectivity";
 
+    /**
+     * A list of captive portal detection specifications used in addition to the fallback URLs.
+     * Each spec has the format url@@/@@statusCodeRegex@@/@@contentRegex. Specs are separated
+     * by "@@,@@".
+     */
+    public static final String CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS =
+            "captive_portal_fallback_probe_specs";
+
+    /**
+     * A comma separated list of URLs used for captive portal detection in addition to the
+     * fallback HTTP url associated with the CAPTIVE_PORTAL_FALLBACK_URL settings.
+     */
+    public static final String CAPTIVE_PORTAL_OTHER_FALLBACK_URLS =
+            "captive_portal_other_fallback_urls";
+
+    /**
+     * Which User-Agent string to use in the header of the captive portal detection probes.
+     * The User-Agent field is unset when this setting has no value (HttpUrlConnection default).
+     */
+    public static final String CAPTIVE_PORTAL_USER_AGENT = "captive_portal_user_agent";
+
+    /**
+     * Whether to use HTTPS for network validation. This is enabled by default and the setting
+     * needs to be set to 0 to disable it. This setting is a misnomer because captive portals
+     * don't actually use HTTPS, but it's consistent with the other settings.
+     */
+    public static final String CAPTIVE_PORTAL_USE_HTTPS = "captive_portal_use_https";
+
     static {
         System.loadLibrary("networkstackutilsjni");
     }
diff --git a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
index 27d4203..093235e 100644
--- a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
@@ -43,6 +43,10 @@
 import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_MIN_EVALUATE_TIME_MS;
 import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_VALID_DNS_TIME_THRESHOLD_MS;
 import static android.net.util.DataStallUtils.DEFAULT_DNS_LOG_SIZE;
+import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS;
+import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS;
+import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USER_AGENT;
+import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USE_HTTPS;
 import static android.net.util.NetworkStackUtils.NAMESPACE_CONNECTIVITY;
 import static android.net.util.NetworkStackUtils.isEmpty;
 
@@ -1171,7 +1175,8 @@
     }
 
     private boolean getUseHttpsValidation() {
-        return mDependencies.getSetting(mContext, Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
+        return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
+                CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
     }
 
     private String getCaptivePortalServerHttpsUrl() {
@@ -1224,8 +1229,8 @@
 
             final URL[] settingProviderUrls;
             if (!TextUtils.isEmpty(firstUrl)) {
-                final String otherUrls = mDependencies.getSetting(mContext,
-                        Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS, "");
+                final String otherUrls = mDependencies.getDeviceConfigProperty(
+                        NAMESPACE_CONNECTIVITY, CAPTIVE_PORTAL_OTHER_FALLBACK_URLS, "");
                 // otherUrls may be empty, but .split() ignores trailing empty strings
                 final String separator = ",";
                 final String[] urls = (firstUrl + separator + otherUrls).split(separator);
@@ -1245,8 +1250,9 @@
 
     private CaptivePortalProbeSpec[] makeCaptivePortalFallbackProbeSpecs() {
         try {
-            final String settingsValue = mDependencies.getSetting(
-                    mContext, Settings.Global.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS, null);
+            final String settingsValue = mDependencies.getDeviceConfigProperty(
+                    NAMESPACE_CONNECTIVITY, CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS, null);
+
             final CaptivePortalProbeSpec[] emptySpecs = new CaptivePortalProbeSpec[0];
             final CaptivePortalProbeSpec[] providerValue = TextUtils.isEmpty(settingsValue)
                     ? emptySpecs
@@ -1340,8 +1346,8 @@
     }
 
     private String getCaptivePortalUserAgent() {
-        return mDependencies.getSetting(mContext,
-                Settings.Global.CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT);
+        return mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
+                CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT);
     }
 
     private URL nextFallbackUrl() {
diff --git a/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
index 910bdc7..594f2ca 100644
--- a/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -26,6 +26,9 @@
 import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_MIN_EVALUATE_INTERVAL;
 import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_VALID_DNS_TIME_THRESHOLD;
 import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_DNS;
+import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS;
+import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS;
+import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USE_HTTPS;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -160,7 +163,7 @@
         when(mDependencies.getRandom()).thenReturn(mRandom);
         when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_MODE), anyInt()))
                 .thenReturn(Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT);
-        when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_USE_HTTPS),
+        when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CAPTIVE_PORTAL_USE_HTTPS),
                 anyInt())).thenReturn(1);
         when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTP_URL), any()))
                 .thenReturn(TEST_HTTP_URL);
@@ -683,13 +686,13 @@
     }
 
     private void setOtherFallbackUrls(String urls) {
-        when(mDependencies.getSetting(any(),
-                eq(Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS), any())).thenReturn(urls);
+        when(mDependencies.getDeviceConfigProperty(any(),
+                eq(CAPTIVE_PORTAL_OTHER_FALLBACK_URLS), any())).thenReturn(urls);
     }
 
     private void setFallbackSpecs(String specs) {
-        when(mDependencies.getSetting(any(),
-                eq(Settings.Global.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS), any())).thenReturn(specs);
+        when(mDependencies.getDeviceConfigProperty(any(),
+                eq(CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS), any())).thenReturn(specs);
     }
 
     private void setCaptivePortalMode(int mode) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
index 4e906bc..4673992 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
@@ -39,7 +39,6 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Predicate;
 
 /**
  * Build/Install/Run:
@@ -47,208 +46,222 @@
  */
 @MediumTest
 @Presubmit
-public class PersisterQueueTests implements PersisterQueue.Listener {
+public class PersisterQueueTests {
     private static final long INTER_WRITE_DELAY_MS = 50;
     private static final long PRE_TASK_DELAY_MS = 300;
-    // We allow at most 1s more than the expected timeout.
-    private static final long TIMEOUT_ALLOWANCE = 100;
-
-    private static final Predicate<MatchingTestItem> TEST_ITEM_PREDICATE = item -> item.mMatching;
-
-    private AtomicInteger mItemCount;
-    private CountDownLatch mSetUpLatch;
-    private volatile CountDownLatch mLatch;
-    private List<Boolean> mProbablyDoneResults;
+    // We allow at most 0.2s more than the expected timeout.
+    private static final long TIMEOUT_ALLOWANCE = 200;
 
     private final PersisterQueue mTarget =
             new PersisterQueue(INTER_WRITE_DELAY_MS, PRE_TASK_DELAY_MS);
 
+    private TestPersisterQueueListener mListener;
+    private TestWriteQueueItemFactory mFactory;
+
     @Before
     public void setUp() throws Exception {
-        mItemCount = new AtomicInteger(0);
-        mProbablyDoneResults = new ArrayList<>();
-        mSetUpLatch = new CountDownLatch(1);
+        mListener = new TestPersisterQueueListener();
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
+        mTarget.addListener(mListener);
 
-        mTarget.addListener(this);
+        mFactory = new TestWriteQueueItemFactory();
+
         mTarget.startPersisting();
 
         assertTrue("Target didn't call callback on start up.",
-                mSetUpLatch.await(TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
+                mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
     }
 
     @After
     public void tearDown() throws Exception {
         mTarget.stopPersisting();
-        mTarget.removeListener(this);
+        mTarget.removeListener(mListener);
     }
 
     @Test
     public void testCallCallbackOnStartUp() {
         // The onPreProcessItem() must be called on start up.
-        assertEquals(1, mProbablyDoneResults.size());
+        assertEquals(1, mListener.mProbablyDoneResults.size());
         // The last one must be called with probably done being true.
-        assertTrue("The last probablyDone must be true.", mProbablyDoneResults.get(0));
+        assertTrue("The last probablyDone must be true.", mListener.mProbablyDoneResults.get(0));
     }
 
     @Test
     public void testProcessOneItem() throws Exception {
-        mLatch = new CountDownLatch(1);
+        mFactory.setExpectedProcessedItemNumber(1);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
 
         final long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(new TestItem(), false);
-        assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process item.", 1, mItemCount.get());
+        mTarget.addItem(mFactory.createItem(), false);
+        assertTrue("Target didn't process item enough times.",
+                mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
         final long processDuration = SystemClock.uptimeMillis() - dispatchTime;
         assertTrue("Target didn't wait enough time before processing item. duration: "
                         + processDuration + "ms pretask delay: " + PRE_TASK_DELAY_MS + "ms",
                 processDuration >= PRE_TASK_DELAY_MS);
 
+        assertTrue("Target didn't call callback enough times.",
+                mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
         // Once before processing this item, once after that.
-        assertEquals(2, mProbablyDoneResults.size());
+        assertEquals(2, mListener.mProbablyDoneResults.size());
         // The last one must be called with probably done being true.
-        assertTrue("The last probablyDone must be true.", mProbablyDoneResults.get(1));
+        assertTrue("The last probablyDone must be true.", mListener.mProbablyDoneResults.get(1));
     }
 
     @Test
     public void testProcessOneItem_Flush() throws Exception {
-        mLatch = new CountDownLatch(1);
+        mFactory.setExpectedProcessedItemNumber(1);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
 
         final long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(new TestItem(), true);
-        assertTrue("Target didn't call callback enough times.",
-                mLatch.await(TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process item.", 1, mItemCount.get());
+        mTarget.addItem(mFactory.createItem(), true);
+        assertTrue("Target didn't process item enough times.",
+                mFactory.waitForAllExpectedItemsProcessed(TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
         final long processDuration = SystemClock.uptimeMillis() - dispatchTime;
         assertTrue("Target didn't process item immediately when flushing. duration: "
                         + processDuration + "ms pretask delay: "
                         + PRE_TASK_DELAY_MS + "ms",
                 processDuration < PRE_TASK_DELAY_MS);
 
+        assertTrue("Target didn't call callback enough times.",
+                mFactory.waitForAllExpectedItemsProcessed(TIMEOUT_ALLOWANCE));
         // Once before processing this item, once after that.
-        assertEquals(2, mProbablyDoneResults.size());
+        assertEquals(2, mListener.mProbablyDoneResults.size());
         // The last one must be called with probably done being true.
-        assertTrue("The last probablyDone must be true.", mProbablyDoneResults.get(1));
+        assertTrue("The last probablyDone must be true.", mListener.mProbablyDoneResults.get(1));
     }
 
     @Test
     public void testProcessTwoItems() throws Exception {
-        mLatch = new CountDownLatch(2);
+        mFactory.setExpectedProcessedItemNumber(2);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(2);
 
         final long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(new TestItem(), false);
-        mTarget.addItem(new TestItem(), false);
+        mTarget.addItem(mFactory.createItem(), false);
+        mTarget.addItem(mFactory.createItem(), false);
         assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS + TIMEOUT_ALLOWANCE,
-                        TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process all items.", 2, mItemCount.get());
+                mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS
+                        + TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process all items.", 2, mFactory.getTotalProcessedItemCount());
         final long processDuration = SystemClock.uptimeMillis() - dispatchTime;
         assertTrue("Target didn't wait enough time before processing item. duration: "
                         + processDuration + "ms pretask delay: " + PRE_TASK_DELAY_MS
                         + "ms inter write delay: " + INTER_WRITE_DELAY_MS + "ms",
                 processDuration >= PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS);
-
+        assertTrue("Target didn't call the onPreProcess callback enough times",
+                mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
         // Once before processing this item, once after that.
-        assertEquals(3, mProbablyDoneResults.size());
+        assertEquals(3, mListener.mProbablyDoneResults.size());
         // The first one must be called with probably done being false.
-        assertFalse("The first probablyDone must be false.", mProbablyDoneResults.get(1));
+        assertFalse("The first probablyDone must be false.", mListener.mProbablyDoneResults.get(1));
         // The last one must be called with probably done being true.
-        assertTrue("The last probablyDone must be true.", mProbablyDoneResults.get(2));
+        assertTrue("The last probablyDone must be true.", mListener.mProbablyDoneResults.get(2));
     }
 
     @Test
     @FlakyTest(bugId = 128526085)
     public void testProcessTwoItems_OneAfterAnother() throws Exception {
         // First item
-        mLatch = new CountDownLatch(1);
+        mFactory.setExpectedProcessedItemNumber(1);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(new TestItem(), false);
-        assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
+        mTarget.addItem(mFactory.createItem(), false);
+        assertTrue("Target didn't process item enough times.",
+                mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
         long processDuration = SystemClock.uptimeMillis() - dispatchTime;
         assertTrue("Target didn't wait enough time before processing item."
                         + processDuration + "ms pretask delay: "
                         + PRE_TASK_DELAY_MS + "ms",
                 processDuration >= PRE_TASK_DELAY_MS);
-        assertEquals("Target didn't process item.", 1, mItemCount.get());
+        assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
+        assertTrue("Target didn't call callback enough times.",
+                mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
 
         // Second item
-        mLatch = new CountDownLatch(1);
+        mFactory.setExpectedProcessedItemNumber(1);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         dispatchTime = SystemClock.uptimeMillis();
         // Synchronize on the instance to make sure we schedule the item after it starts to wait for
         // task indefinitely.
         synchronized (mTarget) {
-            mTarget.addItem(new TestItem(), false);
+            mTarget.addItem(mFactory.createItem(), false);
         }
-        assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process all items.", 2, mItemCount.get());
+        assertTrue("Target didn't process item enough times.",
+                mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process all items.", 2, mFactory.getTotalProcessedItemCount());
         processDuration = SystemClock.uptimeMillis() - dispatchTime;
         assertTrue("Target didn't wait enough time before processing item. Process time: "
                         + processDuration + "ms pre task delay: "
                         + PRE_TASK_DELAY_MS + "ms",
                 processDuration >= PRE_TASK_DELAY_MS);
 
+        assertTrue("Target didn't call callback enough times.",
+                mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
         // Once before processing this item, once after that.
-        assertEquals(3, mProbablyDoneResults.size());
+        assertEquals(3, mListener.mProbablyDoneResults.size());
         // The last one must be called with probably done being true.
-        assertTrue("The last probablyDone must be true.", mProbablyDoneResults.get(2));
+        assertTrue("The last probablyDone must be true.", mListener.mProbablyDoneResults.get(2));
     }
 
     @Test
     public void testFindLastItemNotReturnDifferentType() {
         synchronized (mTarget) {
-            mTarget.addItem(new TestItem(), false);
-            assertNull(mTarget.findLastItem(TEST_ITEM_PREDICATE, MatchingTestItem.class));
+            mTarget.addItem(mFactory.createItem(), false);
+            assertNull(mTarget.findLastItem(TestItem::shouldKeepOnFilter,
+                    FilterableTestItem.class));
         }
     }
 
     @Test
     public void testFindLastItemNotReturnMismatchItem() {
         synchronized (mTarget) {
-            mTarget.addItem(new MatchingTestItem(false), false);
-            assertNull(mTarget.findLastItem(TEST_ITEM_PREDICATE, MatchingTestItem.class));
+            mTarget.addItem(mFactory.createFilterableItem(false), false);
+            assertNull(mTarget.findLastItem(TestItem::shouldKeepOnFilter,
+                    FilterableTestItem.class));
         }
     }
 
     @Test
     public void testFindLastItemReturnMatchedItem() {
         synchronized (mTarget) {
-            final MatchingTestItem item = new MatchingTestItem(true);
+            final FilterableTestItem item = mFactory.createFilterableItem(true);
             mTarget.addItem(item, false);
-            assertSame(item, mTarget.findLastItem(TEST_ITEM_PREDICATE, MatchingTestItem.class));
+            assertSame(item, mTarget.findLastItem(TestItem::shouldKeepOnFilter,
+                    FilterableTestItem.class));
         }
     }
 
     @Test
     public void testRemoveItemsNotRemoveDifferentType() throws Exception {
-        mLatch = new CountDownLatch(1);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         synchronized (mTarget) {
-            mTarget.addItem(new TestItem(), false);
-            mTarget.removeItems(TEST_ITEM_PREDICATE, MatchingTestItem.class);
+            mTarget.addItem(mFactory.createItem(), false);
+            mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
         }
         assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process item.", 1, mItemCount.get());
+                mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
     }
 
     @Test
     public void testRemoveItemsNotRemoveMismatchedItem() throws Exception {
-        mLatch = new CountDownLatch(1);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         synchronized (mTarget) {
-            mTarget.addItem(new MatchingTestItem(false), false);
-            mTarget.removeItems(TEST_ITEM_PREDICATE, MatchingTestItem.class);
+            mTarget.addItem(mFactory.createFilterableItem(false), false);
+            mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
         }
         assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process item.", 1, mItemCount.get());
+                mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
     }
 
     @Test
     public void testUpdateLastOrAddItemUpdatesMatchedItem() throws Exception {
-        mLatch = new CountDownLatch(1);
-        final MatchingTestItem scheduledItem = new MatchingTestItem(true);
-        final MatchingTestItem expected = new MatchingTestItem(true);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
+        final FilterableTestItem scheduledItem = mFactory.createFilterableItem(true);
+        final FilterableTestItem expected = mFactory.createFilterableItem(true);
         synchronized (mTarget) {
             mTarget.addItem(scheduledItem, false);
             mTarget.updateLastOrAddItem(expected, false);
@@ -256,15 +269,15 @@
 
         assertSame(expected, scheduledItem.mUpdateFromItem);
         assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process item.", 1, mItemCount.get());
+                mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
     }
 
     @Test
     public void testUpdateLastOrAddItemUpdatesAddItemWhenNoMatch() throws Exception {
-        mLatch = new CountDownLatch(2);
-        final MatchingTestItem scheduledItem = new MatchingTestItem(false);
-        final MatchingTestItem expected = new MatchingTestItem(true);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(2);
+        final FilterableTestItem scheduledItem = mFactory.createFilterableItem(false);
+        final FilterableTestItem expected = mFactory.createFilterableItem(true);
         synchronized (mTarget) {
             mTarget.addItem(scheduledItem, false);
             mTarget.updateLastOrAddItem(expected, false);
@@ -272,73 +285,132 @@
 
         assertNull(scheduledItem.mUpdateFromItem);
         assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS + TIMEOUT_ALLOWANCE,
-                        TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process item.", 2, mItemCount.get());
+                mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS
+                        + TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process item.", 2, mFactory.getTotalProcessedItemCount());
     }
 
     @Test
     public void testRemoveItemsRemoveMatchedItem() throws Exception {
-        mLatch = new CountDownLatch(1);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         synchronized (mTarget) {
-            mTarget.addItem(new TestItem(), false);
-            mTarget.addItem(new MatchingTestItem(true), false);
-            mTarget.removeItems(TEST_ITEM_PREDICATE, MatchingTestItem.class);
+            mTarget.addItem(mFactory.createItem(), false);
+            mTarget.addItem(mFactory.createFilterableItem(true), false);
+            mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
         }
         assertTrue("Target didn't call callback enough times.",
-                mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
-        assertEquals("Target didn't process item.", 1, mItemCount.get());
+                mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
     }
 
     @Test
     public void testFlushWaitSynchronously() {
         final long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(new TestItem(), false);
-        mTarget.addItem(new TestItem(), false);
+        mTarget.addItem(mFactory.createItem(), false);
+        mTarget.addItem(mFactory.createItem(), false);
         mTarget.flush();
         assertEquals("Flush should wait until all items are processed before return.",
-                2, mItemCount.get());
+                2, mFactory.getTotalProcessedItemCount());
         final long processTime = SystemClock.uptimeMillis() - dispatchTime;
         assertWithMessage("Flush should trigger immediate flush without delays. processTime: "
                 + processTime).that(processTime).isLessThan(TIMEOUT_ALLOWANCE);
     }
 
-    @Override
-    public void onPreProcessItem(boolean queueEmpty) {
-        mProbablyDoneResults.add(queueEmpty);
+    private static class TestWriteQueueItemFactory {
+        private final AtomicInteger mItemCount = new AtomicInteger(0);;
+        private CountDownLatch mLatch;
 
-        final CountDownLatch latch = mLatch;
-        if (latch != null) {
-            latch.countDown();
+        int getTotalProcessedItemCount() {
+            return mItemCount.get();
         }
 
-        mSetUpLatch.countDown();
+        void setExpectedProcessedItemNumber(int countDown) {
+            mLatch = new CountDownLatch(countDown);
+        }
+
+        boolean waitForAllExpectedItemsProcessed(long timeoutInMilliseconds)
+                throws InterruptedException {
+            return mLatch.await(timeoutInMilliseconds, TimeUnit.MILLISECONDS);
+        }
+
+        TestItem createItem() {
+            return new TestItem(mItemCount, mLatch);
+        }
+
+        FilterableTestItem createFilterableItem(boolean shouldKeepOnFilter) {
+            return new FilterableTestItem(shouldKeepOnFilter, mItemCount, mLatch);
+        }
     }
 
-    private class TestItem<T extends TestItem<T>> implements PersisterQueue.WriteQueueItem<T> {
+    private static class TestItem<T extends TestItem<T>>
+            implements PersisterQueue.WriteQueueItem<T> {
+        private AtomicInteger mItemCount;
+        private CountDownLatch mLatch;
+
+        TestItem(AtomicInteger itemCount, CountDownLatch latch) {
+            mItemCount = itemCount;
+            mLatch = latch;
+        }
+
         @Override
         public void process() {
             mItemCount.getAndIncrement();
+            if (mLatch != null) {
+                // Count down the latch at the last step is necessary, as it's a kind of lock to the
+                // next assert in many test cases.
+                mLatch.countDown();
+            }
+        }
+
+        boolean shouldKeepOnFilter() {
+            return true;
         }
     }
 
-    private class MatchingTestItem extends TestItem<MatchingTestItem> {
-        private boolean mMatching;
+    private static class FilterableTestItem extends TestItem<FilterableTestItem> {
+        private boolean mShouldKeepOnFilter;
 
-        private MatchingTestItem mUpdateFromItem;
+        private FilterableTestItem mUpdateFromItem;
 
-        private MatchingTestItem(boolean matching) {
-            mMatching = matching;
+        private FilterableTestItem(boolean shouldKeepOnFilter, AtomicInteger mItemCount,
+                CountDownLatch mLatch) {
+            super(mItemCount, mLatch);
+            mShouldKeepOnFilter = shouldKeepOnFilter;
         }
 
         @Override
-        public boolean matches(MatchingTestItem item) {
-            return item.mMatching;
+        public boolean matches(FilterableTestItem item) {
+            return item.mShouldKeepOnFilter;
         }
 
         @Override
-        public void updateFrom(MatchingTestItem item) {
+        public void updateFrom(FilterableTestItem item) {
             mUpdateFromItem = item;
         }
+
+        @Override
+        boolean shouldKeepOnFilter() {
+            return mShouldKeepOnFilter;
+        }
+    }
+
+    private class TestPersisterQueueListener implements PersisterQueue.Listener {
+        CountDownLatch mCallbackLatch;
+        final List<Boolean> mProbablyDoneResults = new ArrayList<>();
+
+        @Override
+        public void onPreProcessItem(boolean queueEmpty) {
+            mProbablyDoneResults.add(queueEmpty);
+            mCallbackLatch.countDown();
+        }
+
+        void setExpectedOnPreProcessItemCallbackTimes(int countDown) {
+            mCallbackLatch = new CountDownLatch(countDown);
+        }
+
+        boolean waitForAllExpectedCallbackDone(long timeoutInMilliseconds)
+                throws InterruptedException {
+            return mCallbackLatch.await(timeoutInMilliseconds, TimeUnit.MILLISECONDS);
+        }
     }
 }