Updated OverlayPanelViewController to allow for showing overlay panel
from the bottom navigation bar.

Added Top and Bottom NotificationPanelViewMediators that can be used to
change the direction that the notification panel should go in.

Bug: 145827692
Test: Manual
Change-Id: I813415711f826a70cdbf4bd16e9b4f425e81e0de
diff --git a/packages/CarSystemUI/res/values/config.xml b/packages/CarSystemUI/res/values/config.xml
index eb1d9d0..728d57d 100644
--- a/packages/CarSystemUI/res/values/config.xml
+++ b/packages/CarSystemUI/res/values/config.xml
@@ -72,11 +72,21 @@
 
     <!-- Car System UI's OverlayViewsMediator-->
     <string-array name="config_carSystemUIOverlayViewsMediators" translatable="false">
-        <item>com.android.systemui.car.notification.NotificationPanelViewMediator</item>
+        <item>@string/config_notificationPanelViewMediator</item>
         <item>com.android.systemui.car.keyguard.CarKeyguardViewMediator</item>
         <item>com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator</item>
     </string-array>
 
+    <!--
+        Car SystemUI's notification mediator. Replace with other notification mediators to have
+        the notification panel show from another system bar. The system bar should be enabled to
+        use the mediator with that system bar.
+        Example: config_enableBottomNavigationBar=true
+                 config_notificationPanelViewMediator=
+                    com.android.systemui.car.notification.BottomNotificationPanelViewMediator -->
+    <string name="config_notificationPanelViewMediator" translatable="false">
+        com.android.systemui.car.notification.TopNotificationPanelViewMediator</string>
+
     <!-- List of package names that are allowed sources of app installation. -->
     <string-array name="config_allowedAppInstallSources" translatable="false">
         <item>com.android.vending</item>
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/BottomNotificationPanelViewMediator.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/BottomNotificationPanelViewMediator.java
new file mode 100644
index 0000000..6d140ca
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/BottomNotificationPanelViewMediator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 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.car.notification;
+
+import com.android.systemui.car.CarDeviceProvisionedController;
+import com.android.systemui.car.navigationbar.CarNavigationBarController;
+import com.android.systemui.car.window.OverlayPanelViewController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Implementation of NotificationPanelViewMediator that sets the notification panel to be opened
+ * from the top navigation bar.
+ */
+@Singleton
+public class BottomNotificationPanelViewMediator extends NotificationPanelViewMediator {
+
+    @Inject
+    public BottomNotificationPanelViewMediator(
+            CarNavigationBarController carNavigationBarController,
+            NotificationPanelViewController notificationPanelViewController,
+
+            PowerManagerHelper powerManagerHelper,
+
+            CarDeviceProvisionedController carDeviceProvisionedController,
+            ConfigurationController configurationController
+    ) {
+        super(carNavigationBarController,
+                notificationPanelViewController,
+                powerManagerHelper,
+                carDeviceProvisionedController,
+                configurationController);
+        notificationPanelViewController.setOverlayDirection(
+                OverlayPanelViewController.OVERLAY_FROM_BOTTOM_BAR);
+    }
+
+    @Override
+    public void registerListeners() {
+        super.registerListeners();
+        getCarNavigationBarController().registerBottomBarTouchListener(
+                getNotificationPanelViewController().getDragOpenTouchListener());
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java
index a17a0e9..cb9539a 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java
@@ -83,9 +83,9 @@
     private NotificationViewController mNotificationViewController;
 
     private boolean mIsTracking;
-    private boolean mNotificationListAtBottom;
+    private boolean mNotificationListAtEnd;
     private float mFirstTouchDownOnGlassPane;
-    private boolean mNotificationListAtBottomAtTimeOfTouch;
+    private boolean mNotificationListAtEndAtTimeOfTouch;
     private boolean mIsSwipingVerticallyToClose;
     private boolean mIsNotificationCardSwiping;
 
@@ -233,11 +233,11 @@
         // This allows us to initialize gesture listeners and detect when to close the notifications
         glassPane.setOnTouchListener((v, event) -> {
             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-                mNotificationListAtBottomAtTimeOfTouch = false;
+                mNotificationListAtEndAtTimeOfTouch = false;
             }
             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                 mFirstTouchDownOnGlassPane = event.getRawX();
-                mNotificationListAtBottomAtTimeOfTouch = mNotificationListAtBottom;
+                mNotificationListAtEndAtTimeOfTouch = mNotificationListAtEnd;
                 // 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
@@ -251,34 +251,34 @@
             @Override
             public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                 super.onScrolled(recyclerView, dx, dy);
-                // Check if we can scroll vertically downwards.
-                if (!mNotificationList.canScrollVertically(/* direction= */ 1)) {
-                    mNotificationListAtBottom = true;
+                // Check if we can scroll vertically in the animation direction.
+                if (!mNotificationList.canScrollVertically(mAnimateDirection)) {
+                    mNotificationListAtEnd = true;
                     return;
                 }
-                mNotificationListAtBottom = false;
+                mNotificationListAtEnd = false;
                 mIsSwipingVerticallyToClose = false;
-                mNotificationListAtBottomAtTimeOfTouch = false;
+                mNotificationListAtEndAtTimeOfTouch = false;
             }
         });
 
         mNotificationList.setOnTouchListener((v, event) -> {
             mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX())
                     > SWIPE_MAX_OFF_PATH;
-            if (mNotificationListAtBottomAtTimeOfTouch && mNotificationListAtBottom) {
+            if (mNotificationListAtEndAtTimeOfTouch && mNotificationListAtEnd) {
                 // We need to save the state here as if notification card is swiping we will
-                // change the mNotificationListAtBottomAtTimeOfTouch. This is to protect
+                // change the mNotificationListAtEndAtTimeOfTouch. 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
+            // Hence setting mNotificationListAtEndAtTimeOfTouch 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;
+                mNotificationListAtEndAtTimeOfTouch = false;
             }
 
             boolean handled = closeGestureDetector.onTouchEvent(event);
@@ -290,7 +290,7 @@
             }
             if (!handled && event.getActionMasked() == MotionEvent.ACTION_UP
                     && mIsSwipingVerticallyToClose) {
-                if (getSettleClosePercentage() < getPercentageFromBottom() && isTracking) {
+                if (getSettleClosePercentage() < getPercentageFromEndingEdge() && isTracking) {
                     animatePanel(DEFAULT_FLING_VELOCITY, false);
                 } else if (clippedHeight != getLayout().getHeight() && isTracking) {
                     // this can be caused when user is at the end of the list and trying to
@@ -299,11 +299,11 @@
                 }
             }
 
-            // Updating the mNotificationListAtBottomAtTimeOfTouch state has to be done after
+            // Updating the mNotificationListAtEndAtTimeOfTouch 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;
+                mNotificationListAtEndAtTimeOfTouch = false;
             }
             return handled || isTracking;
         });
@@ -377,25 +377,31 @@
     }
 
     @Override
-    protected void onScroll(int height) {
+    protected void onScroll(int y) {
         if (mHandleBar != null) {
             ViewGroup.MarginLayoutParams lp =
                     (ViewGroup.MarginLayoutParams) mHandleBar.getLayoutParams();
-            mHandleBar.setTranslationY(height - mHandleBar.getHeight() - lp.bottomMargin);
+            // Adjust handlebar to new pointer position, and a little more depending on the
+            // animate direction so the bar can be seen fully.
+            if (mAnimateDirection > 0) {
+                mHandleBar.setTranslationY(y - mHandleBar.getHeight() - lp.bottomMargin);
+            } else {
+                mHandleBar.setTranslationY(y + mHandleBar.getHeight() + lp.topMargin);
+            }
         }
 
         if (mNotificationView.getHeight() > 0) {
             Drawable background = mNotificationView.getBackground().mutate();
-            background.setAlpha((int) (getBackgroundAlpha(height) * 255));
+            background.setAlpha((int) (getBackgroundAlpha(y) * 255));
             mNotificationView.setBackground(background);
         }
     }
 
     @Override
     protected boolean shouldAllowClosingScroll() {
-        // Unless the notification list is at the bottom, the panel shouldn't be allowed to
+        // Unless the notification list is at the end, the panel shouldn't be allowed to
         // collapse on scroll.
-        return mNotificationListAtBottomAtTimeOfTouch;
+        return mNotificationListAtEndAtTimeOfTouch;
     }
 
     /**
@@ -403,9 +409,11 @@
      * shade is visible to the user. When the notification shade is completely open then
      * alpha value will be 1.
      */
-    private float getBackgroundAlpha(int height) {
-        return mInitialBackgroundAlpha
-                + ((float) height / mNotificationView.getHeight() * mBackgroundAlphaDiff);
+    private float getBackgroundAlpha(int y) {
+        float fractionCovered =
+                ((float) (mAnimateDirection > 0 ? y : mNotificationView.getHeight() - y))
+                        / mNotificationView.getHeight();
+        return mInitialBackgroundAlpha + fractionCovered * mBackgroundAlphaDiff;
     }
 
     /** Sets the unseen count listener. */
@@ -431,13 +439,18 @@
         @Override
         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
                 float distanceY) {
-            calculatePercentageFromBottom(event2.getRawY());
-            // To prevent the jump in the clip bounds while closing the notification shade using
+            calculatePercentageFromEndingEdge(event2.getRawY());
+            // To prevent the jump in the clip bounds while closing the notification panel using
             // the handle bar we should calculate the height using the diff of event1 and event2.
             // This will help the notification shade to clip smoothly as the event2 value changes
             // as event1 value will be fixed.
-            int clipHeight = getLayout().getHeight() - (int) (event1.getRawY() - event2.getRawY());
-            setViewClipBounds(clipHeight);
+            float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
+            float y = mAnimateDirection > 0
+                    ? getLayout().getHeight() - diff
+                    : diff;
+            // Ensure the position is within the overlay panel.
+            y = Math.max(0, Math.min(y, getLayout().getHeight()));
+            setViewClipBounds((int) y);
             return true;
         }
     }
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java
index 24a84d9..8f52638 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java
@@ -19,17 +19,15 @@
 import android.car.hardware.power.CarPowerManager;
 import android.content.res.Configuration;
 
+import androidx.annotation.CallSuper;
+
 import com.android.systemui.car.CarDeviceProvisionedController;
 import com.android.systemui.car.navigationbar.CarNavigationBarController;
 import com.android.systemui.car.window.OverlayViewMediator;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
 /** The view mediator which attaches the view controller to other elements of the system ui. */
-@Singleton
-public class NotificationPanelViewMediator implements OverlayViewMediator,
+public abstract class NotificationPanelViewMediator implements OverlayViewMediator,
         ConfigurationController.ConfigurationListener {
 
     private final CarNavigationBarController mCarNavigationBarController;
@@ -38,7 +36,6 @@
     private final CarDeviceProvisionedController mCarDeviceProvisionedController;
     private final ConfigurationController mConfigurationController;
 
-    @Inject
     public NotificationPanelViewMediator(
             CarNavigationBarController carNavigationBarController,
             NotificationPanelViewController notificationPanelViewController,
@@ -56,9 +53,10 @@
     }
 
     @Override
+    @CallSuper
     public void registerListeners() {
         mCarNavigationBarController.registerTopBarTouchListener(
-                mNotificationPanelViewController.getDragOpenTouchListener());
+                mNotificationPanelViewController.getDragCloseTouchListener());
         mCarNavigationBarController.registerBottomBarTouchListener(
                 mNotificationPanelViewController.getDragCloseTouchListener());
         mCarNavigationBarController.registerLeftBarTouchListener(
@@ -128,4 +126,12 @@
         mNotificationPanelViewController.reinflate();
         registerListeners();
     }
+
+    protected final CarNavigationBarController getCarNavigationBarController() {
+        return mCarNavigationBarController;
+    }
+
+    protected final NotificationPanelViewController getNotificationPanelViewController() {
+        return mNotificationPanelViewController;
+    }
 }
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/TopNotificationPanelViewMediator.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/TopNotificationPanelViewMediator.java
new file mode 100644
index 0000000..09a4621
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/TopNotificationPanelViewMediator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 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.car.notification;
+
+import com.android.systemui.car.CarDeviceProvisionedController;
+import com.android.systemui.car.navigationbar.CarNavigationBarController;
+import com.android.systemui.car.window.OverlayPanelViewController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Implementation of NotificationPanelViewMediator that sets the notification panel to be opened
+ * from the top navigation bar.
+ */
+@Singleton
+public class TopNotificationPanelViewMediator extends NotificationPanelViewMediator {
+
+    @Inject
+    public TopNotificationPanelViewMediator(
+            CarNavigationBarController carNavigationBarController,
+            NotificationPanelViewController notificationPanelViewController,
+
+            PowerManagerHelper powerManagerHelper,
+
+            CarDeviceProvisionedController carDeviceProvisionedController,
+            ConfigurationController configurationController
+    ) {
+        super(carNavigationBarController,
+                notificationPanelViewController,
+                powerManagerHelper,
+                carDeviceProvisionedController,
+                configurationController);
+        notificationPanelViewController.setOverlayDirection(
+                OverlayPanelViewController.OVERLAY_FROM_TOP_BAR);
+    }
+
+    @Override
+    public void registerListeners() {
+        super.registerListeners();
+        getCarNavigationBarController().registerBottomBarTouchListener(
+                getNotificationPanelViewController().getDragOpenTouchListener());
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java
index 90892d5..0fe9856 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java
@@ -19,6 +19,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.annotation.IntDef;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
@@ -35,13 +36,36 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * The {@link OverlayPanelViewController} provides additional dragging animation capabilities to
  * {@link OverlayViewController}.
  */
 public abstract class OverlayPanelViewController extends OverlayViewController {
 
-    private static final boolean DEBUG = true;
+    /** @hide */
+    @IntDef(flag = true, prefix = { "OVERLAY_" }, value = {
+            OVERLAY_FROM_TOP_BAR,
+            OVERLAY_FROM_BOTTOM_BAR
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface OverlayDirection {}
+
+    /**
+     * Indicates that the overlay panel should be opened from the top bar and expanded by dragging
+     * towards the bottom bar.
+     */
+    public static final int OVERLAY_FROM_TOP_BAR = 0;
+
+    /**
+     * Indicates that the overlay panel should be opened from the bottom bar and expanded by
+     * dragging towards the top bar.
+     */
+    public static final int OVERLAY_FROM_BOTTOM_BAR = 1;
+
+    private static final boolean DEBUG = false;
     private static final String TAG = "OverlayPanelViewController";
 
     // used to calculate how fast to open or close the window
@@ -54,14 +78,18 @@
     protected static final int SWIPE_DOWN_MIN_DISTANCE = 25;
     protected static final int SWIPE_MAX_OFF_PATH = 75;
     protected static final int SWIPE_THRESHOLD_VELOCITY = 200;
+    private static final int POSITIVE_DIRECTION = 1;
+    private static final int NEGATIVE_DIRECTION = -1;
 
     private final FlingAnimationUtils mFlingAnimationUtils;
     private final CarDeviceProvisionedController mCarDeviceProvisionedController;
     private final View.OnTouchListener mDragOpenTouchListener;
     private final View.OnTouchListener mDragCloseTouchListener;
 
+    protected int mAnimateDirection = POSITIVE_DIRECTION;
+
     private final int mSettleClosePercentage;
-    private int mPercentageFromBottom;
+    private int mPercentageFromEndingEdge;
 
     private boolean mPanelVisible;
     private boolean mPanelExpanded;
@@ -91,8 +119,7 @@
         mSettleClosePercentage = resources.getInteger(
                 R.integer.notification_settle_close_percentage);
 
-        // Attached to the top navigation bar (i.e. status bar) to detect pull down of the
-        // notification shade.
+        // Attached to a navigation bar to open the overlay panel
         GestureDetector openGestureDetector = new GestureDetector(context,
                 new OpenGestureListener() {
                     @Override
@@ -101,8 +128,8 @@
                     }
                 });
 
-        // Attached to the NavBars to close the notification shade
-        GestureDetector navBarCloseNotificationGestureDetector = new GestureDetector(context,
+        // Attached to the other navigation bars to close the overlay panel
+        GestureDetector closeGestureDetector = new GestureDetector(context,
                 new SystemBarCloseGestureListener() {
                     @Override
                     protected void close() {
@@ -132,7 +159,7 @@
             if (!isInflated()) {
                 return true;
             }
-            boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event);
+            boolean consumed = closeGestureDetector.onTouchEvent(event);
             if (consumed) {
                 return true;
             }
@@ -141,6 +168,17 @@
         };
     }
 
+    /** Sets the overlay panel animation direction along the x or y axis. */
+    public void setOverlayDirection(@OverlayDirection int direction) {
+        if (direction == OVERLAY_FROM_TOP_BAR) {
+            mAnimateDirection = POSITIVE_DIRECTION;
+        } else if (direction == OVERLAY_FROM_BOTTOM_BAR) {
+            mAnimateDirection = NEGATIVE_DIRECTION;
+        } else {
+            throw new IllegalArgumentException("Direction not supported");
+        }
+    }
+
     /** Toggles the visibility of the panel. */
     public void toggle() {
         if (!isInflated()) {
@@ -207,7 +245,7 @@
     protected void maybeCompleteAnimation(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_UP
                 && isPanelVisible()) {
-            if (mSettleClosePercentage < mPercentageFromBottom) {
+            if (mSettleClosePercentage < mPercentageFromEndingEdge) {
                 animatePanel(DEFAULT_FLING_VELOCITY, false);
             } else {
                 animatePanel(DEFAULT_FLING_VELOCITY, true);
@@ -221,16 +259,15 @@
      * panel this method also makes the view invisible after animation ends.
      */
     protected void animatePanel(float velocity, boolean isClosing) {
-        float to = 0;
-        if (!isClosing) {
-            to = getLayout().getHeight();
-        }
+        float to = getEndPosition(isClosing);
 
         Rect rect = getLayout().getClipBounds();
-        if (rect != null && rect.bottom != to) {
-            float from = rect.bottom;
-            animate(from, to, velocity, isClosing);
-            return;
+        if (rect != null) {
+            float from = getCurrentStartPosition(rect);
+            if (from != to) {
+                animate(from, to, velocity, isClosing);
+                return;
+            }
         }
 
         // We will only be here if the shade is being opened programmatically or via button when
@@ -242,12 +279,32 @@
                     public void onGlobalLayout() {
                         ViewTreeObserver obs = getLayout().getViewTreeObserver();
                         obs.removeOnGlobalLayoutListener(this);
-                        float to = getLayout().getHeight();
-                        animate(/* from= */ 0, to, velocity, isClosing);
+                        animate(
+                                getDefaultStartPosition(),
+                                getEndPosition(/* isClosing= */ false),
+                                velocity,
+                                isClosing
+                        );
                     }
                 });
     }
 
+    /* Returns the start position if the user has not started swiping. */
+    private int getDefaultStartPosition() {
+        return mAnimateDirection > 0 ? 0 : getLayout().getHeight();
+    }
+
+    /** Returns the start position if we are in the middle of swiping. */
+    private int getCurrentStartPosition(Rect clipBounds) {
+        return mAnimateDirection > 0 ? clipBounds.bottom : clipBounds.top;
+    }
+
+    private int getEndPosition(boolean isClosing) {
+        return (mAnimateDirection > 0 && !isClosing) || (mAnimateDirection == -1 && isClosing)
+                ? getLayout().getHeight()
+                : 0;
+    }
+
     private void animate(float from, float to, float velocity, boolean isClosing) {
         if (mIsAnimating) {
             return;
@@ -356,25 +413,44 @@
      * Misc
      * ***************************************************************************************** */
 
-    protected void calculatePercentageFromBottom(float height) {
+    /**
+     * Given the position of the pointer dragging the panel, return the percentage of its closeness
+     * to the ending edge.
+     */
+    protected void calculatePercentageFromEndingEdge(float y) {
         if (getLayout().getHeight() > 0) {
-            mPercentageFromBottom = (int) Math.abs(
-                    height / getLayout().getHeight() * 100);
+            float height = getVisiblePanelHeight(y);
+            mPercentageFromEndingEdge = (int) Math.abs(height / getLayout().getHeight() * 100);
         }
     }
 
-    protected void setViewClipBounds(int height) {
-        if (height > getLayout().getHeight()) {
-            height = getLayout().getHeight();
-        }
+    private float getVisiblePanelHeight(float y) {
+        return mAnimateDirection > 0 ? y : getLayout().getHeight() - y;
+    }
+
+    /** Sets the boundaries of the overlay panel that can be seen based on pointer position. */
+    protected void setViewClipBounds(int y) {
+        // Bound the pointer position to be within the overlay panel.
+        y = Math.max(0, Math.min(y, getLayout().getHeight()));
         Rect clipBounds = new Rect();
-        clipBounds.set(0, 0, getLayout().getWidth(), height);
+        int top, bottom;
+        if (mAnimateDirection > 0) {
+            top = 0;
+            bottom = y;
+        } else {
+            top = y;
+            bottom = getLayout().getHeight();
+        }
+        clipBounds.set(0, top, getLayout().getWidth(), bottom);
         getLayout().setClipBounds(clipBounds);
-        onScroll(height);
+        onScroll(y);
     }
 
-    /** Called while scrolling. */
-    protected abstract void onScroll(int height);
+    /**
+     * Called while scrolling, this passes the position of the clip boundary that is currently
+     * changing.
+     */
+    protected abstract void onScroll(int y);
 
     /* ***************************************************************************************** *
      * Getters
@@ -406,8 +482,8 @@
     }
 
     /** Returns the percentage of the panel that is open from the bottom. */
-    protected final int getPercentageFromBottom() {
-        return mPercentageFromBottom;
+    protected final int getPercentageFromEndingEdge() {
+        return mPercentageFromEndingEdge;
     }
 
     /** Returns the percentage at which we've determined whether to open or close the panel. */
@@ -443,7 +519,7 @@
 
             // Initially the scroll starts with height being zero. This checks protects from divide
             // by zero error.
-            calculatePercentageFromBottom(event2.getRawY());
+            calculatePercentageFromEndingEdge(event2.getRawY());
 
             mIsTracking = true;
             return true;
@@ -453,7 +529,7 @@
         @Override
         public boolean onFling(MotionEvent event1, MotionEvent event2,
                 float velocityX, float velocityY) {
-            if (velocityY > SWIPE_THRESHOLD_VELOCITY) {
+            if (mAnimateDirection * velocityY > SWIPE_THRESHOLD_VELOCITY) {
                 mOpeningVelocity = velocityY;
                 open();
                 return true;
@@ -483,19 +559,14 @@
         @Override
         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
                 float distanceY) {
-            // should not clip while scroll to the bottom of the list.
             if (!shouldAllowClosingScroll()) {
                 return false;
             }
-            float actualNotificationHeight =
-                    getLayout().getHeight() - (event1.getRawY() - event2.getRawY());
-            if (actualNotificationHeight > getLayout().getHeight()) {
-                actualNotificationHeight = getLayout().getHeight();
-            }
+            float y = getYPositionOfPanelEndingEdge(event1, event2);
             if (getLayout().getHeight() > 0) {
-                mPercentageFromBottom = (int) Math.abs(
-                        actualNotificationHeight / getLayout().getHeight() * 100);
-                boolean isUp = distanceY > 0;
+                mPercentageFromEndingEdge = (int) Math.abs(
+                        y / getLayout().getHeight() * 100);
+                boolean isInClosingDirection = mAnimateDirection * 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
@@ -503,23 +574,37 @@
                 // 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(getLayout().getHeight() - actualNotificationHeight)
-                        > SWIPE_DOWN_MIN_DISTANCE && isUp) {
-                    setViewClipBounds((int) actualNotificationHeight);
+                if (Math.abs(getLayout().getHeight() - y)
+                        > SWIPE_DOWN_MIN_DISTANCE && isInClosingDirection) {
+                    setViewClipBounds((int) y);
                     mIsTracking = true;
-                } else if (!isUp) {
-                    setViewClipBounds((int) actualNotificationHeight);
+                } else if (!isInClosingDirection) {
+                    setViewClipBounds((int) y);
                 }
             }
             // if we return true the items in RV won't be scrollable.
             return false;
         }
 
+        /**
+         * To prevent the jump in the clip bounds while closing the panel we should calculate the y
+         * position using the diff of event1 and event2. This will help the panel clip smoothly as
+         * the event2 value changes while event1 value will be fixed.
+         * @param event1 MotionEvent that contains the position of where the event2 started.
+         * @param event2 MotionEvent that contains the position of where the user has scrolled to
+         *               on the screen.
+         */
+        private float getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2) {
+            float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
+            float y = mAnimateDirection > 0 ? getLayout().getHeight() - diff : diff;
+            y = Math.max(0, Math.min(y, getLayout().getHeight()));
+            return y;
+        }
 
         @Override
         public boolean onFling(MotionEvent event1, MotionEvent event2,
                 float velocityX, float velocityY) {
-            // should not fling if the touch does not start when view is at the bottom of the list.
+            // should not fling if the touch does not start when view is at the end of the list.
             if (!shouldAllowClosingScroll()) {
                 return false;
             }
@@ -528,8 +613,8 @@
                 // swipe was not vertical or was not fast enough
                 return false;
             }
-            boolean isUp = velocityY < 0;
-            if (isUp) {
+            boolean isInClosingDirection = mAnimateDirection * velocityY < 0;
+            if (isInClosingDirection) {
                 close();
                 return true;
             } else {
@@ -555,7 +640,7 @@
         @Override
         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
                 float distanceY) {
-            calculatePercentageFromBottom(event2.getRawY());
+            calculatePercentageFromEndingEdge(event2.getRawY());
             setViewClipBounds((int) event2.getRawY());
             return true;
         }
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayWindowModule.java b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayWindowModule.java
index c46b287..e1918ce 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayWindowModule.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayWindowModule.java
@@ -17,7 +17,8 @@
 package com.android.systemui.car.window;
 
 import com.android.systemui.car.keyguard.CarKeyguardViewMediator;
-import com.android.systemui.car.notification.NotificationPanelViewMediator;
+import com.android.systemui.car.notification.BottomNotificationPanelViewMediator;
+import com.android.systemui.car.notification.TopNotificationPanelViewMediator;
 import com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator;
 
 import dagger.Binds;
@@ -31,12 +32,19 @@
 @Module
 public abstract class OverlayWindowModule {
 
-    /** Injects NotificationPanelViewMediator. */
+    /** Injects TopNotificationPanelViewMediator. */
     @Binds
     @IntoMap
-    @ClassKey(NotificationPanelViewMediator.class)
-    public abstract OverlayViewMediator bindNotificationPanelViewMediator(
-            NotificationPanelViewMediator notificationPanelViewMediator);
+    @ClassKey(TopNotificationPanelViewMediator.class)
+    public abstract OverlayViewMediator bindTopNotificationPanelViewMediator(
+            TopNotificationPanelViewMediator topNotificationPanelViewMediator);
+
+    /** Injects BottomNotificationPanelViewMediator. */
+    @Binds
+    @IntoMap
+    @ClassKey(BottomNotificationPanelViewMediator.class)
+    public abstract OverlayViewMediator bindBottomNotificationPanelViewMediator(
+            BottomNotificationPanelViewMediator bottomNotificationPanelViewMediator);
 
     /** Inject into CarKeyguardViewMediator. */
     @Binds
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java
index 70f1d25..8d705a8 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java
@@ -18,13 +18,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.animation.Animator;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.LayoutInflater;
@@ -42,6 +45,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -173,6 +177,51 @@
     }
 
     @Test
+    public void animateCollapsePanel_withOverlayFromTopBar_collapsesTowardsTopBar() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        // Mock a panel that has layout size 50 and where the panel is opened.
+        int size = 50;
+        mockPanelWithSize(size);
+        mOverlayPanelViewController.getLayout().setClipBounds(
+                new Rect(0, 0, size, size));
+        mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
+        mOverlayPanelViewController.setPanelExpanded(true);
+        mOverlayPanelViewController.setPanelVisible(true);
+        mOverlayPanelViewController.setOverlayDirection(
+                OverlayPanelViewController.OVERLAY_FROM_TOP_BAR);
+
+        mOverlayPanelViewController.animateCollapsePanel();
+
+        ArgumentCaptor<Float> endValueCaptor = ArgumentCaptor.forClass(Float.class);
+        verify(mFlingAnimationUtils).apply(
+                any(Animator.class), anyFloat(), endValueCaptor.capture(), anyFloat());
+        assertThat(endValueCaptor.getValue().intValue()).isEqualTo(0);
+    }
+
+    @Test
+    public void animateCollapsePanel_withOverlayFromBottomBar_collapsesTowardsBottomBar() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        // Mock a panel that has layout size 50 and where the panel is opened.
+        int size = 50;
+        mockPanelWithSize(size);
+        mOverlayPanelViewController.getLayout().setClipBounds(
+                new Rect(0, 0, size, size));
+        mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
+        mOverlayPanelViewController.setPanelExpanded(true);
+        mOverlayPanelViewController.setPanelVisible(true);
+        mOverlayPanelViewController.setOverlayDirection(
+                OverlayPanelViewController.OVERLAY_FROM_BOTTOM_BAR);
+
+        mOverlayPanelViewController.animateCollapsePanel();
+
+        ArgumentCaptor<Float> endValueCaptor = ArgumentCaptor.forClass(Float.class);
+        verify(mFlingAnimationUtils).apply(
+                any(Animator.class), anyFloat(), endValueCaptor.capture(), anyFloat());
+        assertThat(endValueCaptor.getValue().intValue()).isEqualTo(
+                mOverlayPanelViewController.getLayout().getHeight());
+    }
+
+    @Test
     public void animateCollapsePanel_removesWindowFocus() {
         mOverlayPanelViewController.inflate(mBaseLayout);
         mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
@@ -219,6 +268,49 @@
     }
 
     @Test
+    public void animateExpandPanel_withOverlayFromTopBar_expandsToBottom() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        // Mock a panel that has layout size 50 and where the panel is not opened.
+        int size = 50;
+        mockPanelWithSize(size);
+        mOverlayPanelViewController.getLayout().setClipBounds(
+                new Rect(0, 0, size, 0));
+        mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
+        when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
+        mOverlayPanelViewController.setOverlayDirection(
+                OverlayPanelViewController.OVERLAY_FROM_TOP_BAR);
+
+        mOverlayPanelViewController.animateExpandPanel();
+
+        ArgumentCaptor<Float> endValueCaptor = ArgumentCaptor.forClass(Float.class);
+        verify(mFlingAnimationUtils).apply(
+                any(Animator.class), anyFloat(), endValueCaptor.capture(), anyFloat());
+        assertThat(endValueCaptor.getValue().intValue()).isEqualTo(
+                mOverlayPanelViewController.getLayout().getHeight());
+    }
+
+    @Test
+    public void animateExpandPanel_withOverlayFromBottomBar_expandsToTop() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        // Mock a panel that has layout size 50 and where the panel is not opened.
+        int size = 50;
+        mockPanelWithSize(size);
+        mOverlayPanelViewController.getLayout().setClipBounds(
+                new Rect(0, size, size, size));
+        mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
+        when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
+        mOverlayPanelViewController.setOverlayDirection(
+                OverlayPanelViewController.OVERLAY_FROM_BOTTOM_BAR);
+
+        mOverlayPanelViewController.animateExpandPanel();
+
+        ArgumentCaptor<Float> endValueCaptor = ArgumentCaptor.forClass(Float.class);
+        verify(mFlingAnimationUtils).apply(
+                any(Animator.class), anyFloat(), endValueCaptor.capture(), anyFloat());
+        assertThat(endValueCaptor.getValue().intValue()).isEqualTo(0);
+    }
+
+    @Test
     public void animateExpandPanel_setsPanelVisible() {
         mOverlayPanelViewController.inflate(mBaseLayout);
         mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
@@ -330,6 +422,10 @@
         verify(mOverlayViewGlobalStateController).inflateView(mOverlayPanelViewController);
     }
 
+    private void mockPanelWithSize(int size) {
+        mOverlayPanelViewController.getLayout().setLeftTopRightBottom(0, 0, size, size);
+    }
+
     private static class TestOverlayPanelViewController extends OverlayPanelViewController {
 
         private boolean mShouldAnimateCollapsePanel;