Merge "Fix createConfirmDeviceCredentialIntent documentation" into qt-dev
diff --git a/core/java/android/app/role/RoleControllerManager.java b/core/java/android/app/role/RoleControllerManager.java
index 9186b3d..668dbd4 100644
--- a/core/java/android/app/role/RoleControllerManager.java
+++ b/core/java/android/app/role/RoleControllerManager.java
@@ -22,6 +22,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemService;
 import android.annotation.UserIdInt;
+import android.app.ActivityThread;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -93,7 +94,7 @@
             int userId = context.getUserId();
             RemoteService remoteService = sRemoteServices.get(userId);
             if (remoteService == null) {
-                remoteService = new RemoteService(context.getApplicationContext(),
+                remoteService = new RemoteService(ActivityThread.currentApplication(),
                         remoteServiceComponentName, handler, userId);
                 sRemoteServices.put(userId, remoteService);
             }
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 00238bf..3dd510c 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3015,7 +3015,8 @@
      *      specify an explicit component name.
      * @param flags Operation options for the binding as per {@link #bindService}.
      * @param instanceName Unique identifier for the service instance.  Each unique
-     *      name here will result in a different service instance being created.
+     *      name here will result in a different service instance being created.  Identifiers
+     *      must only contain ASCII letters, digits, underscores, and periods.
      * @return Returns success of binding as per {@link #bindService}.
      * @param executor Callbacks on ServiceConnection will be called on executor.
      *      Must use same instance for the same instance of ServiceConnection.
@@ -3023,6 +3024,7 @@
      *      This must be a valid ServiceConnection object; it must not be null.
      *
      * @throws SecurityException If the caller does not have permission to access the service
+     * @throws IllegalArgumentException If the instanceName is invalid.
      *
      * @see #bindService
      */
diff --git a/core/java/android/view/DisplayAddress.java b/core/java/android/view/DisplayAddress.java
index 1360815..c8b7e25e 100644
--- a/core/java/android/view/DisplayAddress.java
+++ b/core/java/android/view/DisplayAddress.java
@@ -32,13 +32,12 @@
      * A physical display ID is stable if the display can be identified using EDID information.
      *
      * @param physicalDisplayId A physical display ID.
-     * @return The {@link Physical} address, or {@code null} if the ID is not stable.
+     * @return The {@link Physical} address.
      * @see SurfaceControl#getPhysicalDisplayIds
      */
-    @Nullable
+    @NonNull
     public static Physical fromPhysicalDisplayId(long physicalDisplayId) {
-        final Physical address = new Physical(physicalDisplayId);
-        return address.getModel() == 0 ? null : address;
+        return new Physical(physicalDisplayId);
     }
 
     /**
@@ -59,9 +58,12 @@
      * of a display. The port, located in the least significant byte, uniquely identifies a physical
      * connector on the device for display output like eDP or HDMI. The model, located in the upper
      * bits, uniquely identifies a display model across manufacturers by encoding EDID information.
+     * While the port is always stable, the model may not be available if EDID identification is not
+     * supported by the platform, in which case the address is not unique.
      */
     public static final class Physical extends DisplayAddress {
-        private static final int PHYSICAL_DISPLAY_ID_MODEL_SHIFT = 8;
+        private static final long UNKNOWN_MODEL = 0;
+        private static final int MODEL_SHIFT = 8;
         private static final int PORT_MASK = 0xFF;
 
         private final long mPhysicalDisplayId;
@@ -75,9 +77,13 @@
 
         /**
          * Model identifier unique across manufacturers.
+         *
+         * @return The model ID, or {@code null} if the model cannot be identified.
          */
-        public long getModel() {
-            return mPhysicalDisplayId >>> PHYSICAL_DISPLAY_ID_MODEL_SHIFT;
+        @Nullable
+        public Long getModel() {
+            final long model = mPhysicalDisplayId >>> MODEL_SHIFT;
+            return model == UNKNOWN_MODEL ? null : model;
         }
 
         @Override
@@ -88,11 +94,15 @@
 
         @Override
         public String toString() {
-            return new StringBuilder("{")
-                    .append("port=").append(getPort() & PORT_MASK)
-                    .append(", model=0x").append(Long.toHexString(getModel()))
-                    .append("}")
-                    .toString();
+            final StringBuilder builder = new StringBuilder("{")
+                    .append("port=").append(getPort() & PORT_MASK);
+
+            final Long model = getModel();
+            if (model != null) {
+                builder.append(", model=0x").append(Long.toHexString(model));
+            }
+
+            return builder.append("}").toString();
         }
 
         @Override
@@ -109,7 +119,7 @@
             mPhysicalDisplayId = physicalDisplayId;
         }
 
-        public static final @android.annotation.NonNull Parcelable.Creator<Physical> CREATOR =
+        public static final @NonNull Parcelable.Creator<Physical> CREATOR =
                 new Parcelable.Creator<Physical>() {
                     @Override
                     public Physical createFromParcel(Parcel in) {
@@ -153,7 +163,7 @@
             mMacAddress = macAddress;
         }
 
-        public static final @android.annotation.NonNull Parcelable.Creator<Network> CREATOR =
+        public static final @NonNull Parcelable.Creator<Network> CREATOR =
                 new Parcelable.Creator<Network>() {
                     @Override
                     public Network createFromParcel(Parcel in) {
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index dbbe1b4..485add9 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -35,6 +35,11 @@
         <permission name="android.permission.CRYPT_KEEPER"/>
     </privapp-permissions>
 
+    <privapp-permissions package="com.android.captiveportallogin">
+        <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
+        <permission name="android.permission.NETWORK_BYPASS_PRIVATE_DNS"/>
+    </privapp-permissions>
+
     <privapp-permissions package="com.android.cellbroadcastreceiver">
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
         <permission name="android.permission.MANAGE_USERS"/>
@@ -213,6 +218,7 @@
         <permission name="android.permission.LOCAL_MAC_ADDRESS"/>
         <permission name="android.permission.MANAGE_SUBSCRIPTION_PLANS"/>
         <permission name="android.permission.MANAGE_USB"/>
+        <permission name="android.permission.NETWORK_BYPASS_PRIVATE_DNS"/>
         <permission name="android.permission.PACKET_KEEPALIVE_OFFLOAD"/>
         <permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/>
         <permission name="android.permission.READ_PRECISE_PHONE_STATE"/>
diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java
index b2ebfa9..ab80b3a 100644
--- a/media/java/android/media/AudioPlaybackConfiguration.java
+++ b/media/java/android/media/AudioPlaybackConfiguration.java
@@ -16,6 +16,9 @@
 
 package android.media;
 
+import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_ALL;
+import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
@@ -235,6 +238,9 @@
                 .setUsage(in.mPlayerAttr.getUsage())
                 .setContentType(in.mPlayerAttr.getContentType())
                 .setFlags(in.mPlayerAttr.getFlags())
+                .setAllowedCapturePolicy(
+                        in.mPlayerAttr.getAllowedCapturePolicy() == ALLOW_CAPTURE_BY_ALL
+                        ? ALLOW_CAPTURE_BY_ALL : ALLOW_CAPTURE_BY_NONE)
                 .build();
         // anonymized data
         anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN;
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index c6c2fdd..c09bd79 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -89,15 +89,33 @@
  <h4>Raw Audio Buffers</h4>
  <p>
  Raw audio buffers contain entire frames of PCM audio data, which is one sample for each channel
- in channel order. Each sample is a {@linkplain AudioFormat#ENCODING_PCM_16BIT 16-bit signed
- integer in native byte order}.
+ in channel order. Each PCM audio sample is either a 16 bit signed integer or a float,
+ in native byte order.
+ Raw audio buffers in the float PCM encoding are only possible
+ if the MediaFormat's {@linkplain MediaFormat#KEY_PCM_ENCODING}
+ is set to {@linkplain AudioFormat#ENCODING_PCM_FLOAT} during MediaCodec
+ {@link #configure configure(&hellip;)}
+ and confirmed by {@link #getOutputFormat} for decoders
+ or {@link #getInputFormat} for encoders.
+ A sample method to check for float PCM in the MediaFormat is as follows:
 
  <pre class=prettyprint>
+ static boolean isPcmFloat(MediaFormat format) {
+   return format.getInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
+       == AudioFormat.ENCODING_PCM_FLOAT;
+ }</pre>
+
+ In order to extract, in a short array,
+ one channel of a buffer containing 16 bit signed integer audio data,
+ the following code may be used:
+
+ <pre class=prettyprint>
+ // Assumes the buffer PCM encoding is 16 bit.
  short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
    MediaFormat format = codec.getOutputFormat(bufferId);
    ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
-   int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+   int numChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
    if (channelIx &lt; 0 || channelIx &gt;= numChannels) {
      return null;
    }
diff --git a/packages/CaptivePortalLogin/AndroidManifest.xml b/packages/CaptivePortalLogin/AndroidManifest.xml
index 355bdd8..9add247 100644
--- a/packages/CaptivePortalLogin/AndroidManifest.xml
+++ b/packages/CaptivePortalLogin/AndroidManifest.xml
@@ -26,6 +26,7 @@
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
     <uses-permission android:name="android.permission.NETWORK_BYPASS_PRIVATE_DNS" />
     <uses-permission android:name="android.permission.MAINLINE_NETWORK_STACK" />
 
diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING
index 42aa47c..5d7b9bb 100644
--- a/packages/PackageInstaller/TEST_MAPPING
+++ b/packages/PackageInstaller/TEST_MAPPING
@@ -1,5 +1,5 @@
 {
-  "presubmit": [
+  "postsubmit": [
     {
       "name": "CtsPackageInstallTestCases",
       "options": [
diff --git a/packages/SystemUI/res-keyguard/values-sw320dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw320dp/dimens.xml
deleted file mode 100644
index 91ca5c5..0000000
--- a/packages/SystemUI/res-keyguard/values-sw320dp/dimens.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* //device/apps/common/assets/res/any/dimens.xml
-**
-** Copyright 2013, 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.
-*/
--->
-<resources>
-
-    <!-- Height of the sliding KeyguardSecurityContainer
-         (includes 2x keyguard_security_view_top_margin) -->
-    <dimen name="keyguard_security_height">395dp</dimen>
-
-</resources>
diff --git a/packages/SystemUI/res-keyguard/values-sw360dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw360dp/dimens.xml
deleted file mode 100644
index d7c9975..0000000
--- a/packages/SystemUI/res-keyguard/values-sw360dp/dimens.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* //device/apps/common/assets/res/any/dimens.xml
-**
-** Copyright 2013, 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.
-*/
--->
-<resources>
-
-    <!-- Height of the sliding KeyguardSecurityContainer (includes 2x
-         keyguard_security_view_top_margin) -->
-    <dimen name="keyguard_security_height">450dp</dimen>
-</resources>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index d67c98a..d8f543f 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -27,11 +27,11 @@
 
     <!-- Height of the sliding KeyguardSecurityContainer
          (includes 2x keyguard_security_view_top_margin) -->
-    <dimen name="keyguard_security_height">450dp</dimen>
+    <dimen name="keyguard_security_height">420dp</dimen>
 
     <!-- Max Height of the sliding KeyguardSecurityContainer
          (includes 2x keyguard_security_view_top_margin) -->
-    <dimen name="keyguard_security_max_height">505dp</dimen>
+    <dimen name="keyguard_security_max_height">450dp</dimen>
 
     <!-- Margin around the various security views -->
     <dimen name="keyguard_security_view_top_margin">8dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index df6bc20..a0c021a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -41,10 +41,16 @@
 
     <!-- Size of the nav bar edge panels, should be greater to the
          edge sensitivity + the drag threshold -->
-    <dimen name="navigation_edge_panel_width">52dp</dimen>
-    <dimen name="navigation_edge_panel_height">52dp</dimen>
+    <dimen name="navigation_edge_panel_width">76dp</dimen>
+    <!-- Padding at the end of the navigation panel to allow the arrow not to be clipped off -->
+    <dimen name="navigation_edge_panel_padding">24dp</dimen>
+    <dimen name="navigation_edge_panel_height">84dp</dimen>
     <!-- The threshold to drag to trigger the edge action -->
     <dimen name="navigation_edge_action_drag_threshold">16dp</dimen>
+    <!-- The minimum display position of the arrow on the screen -->
+    <dimen name="navigation_edge_arrow_min_y">64dp</dimen>
+    <!-- The amount by which the arrow is shifted to avoid the finger-->
+    <dimen name="navigation_edge_finger_offset">48dp</dimen>
 
     <!-- Luminance threshold to determine black/white contrast for the navigation affordances -->
     <item name="navigation_luminance_threshold" type="dimen" format="float">0.5</item>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
index 814fec3..0a6885c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
@@ -27,6 +27,7 @@
 import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.TypedValue;
 import android.view.View;
 import android.widget.TextView;
 
@@ -120,6 +121,15 @@
     }
 
     @Override
+    public void onDensityOrFontScaleChanged() {
+        TypedArray array = mContext.obtainStyledAttributes(R.style.Keyguard_TextView, new int[] {
+                android.R.attr.textSize
+        });
+        setTextSize(TypedValue.COMPLEX_UNIT_PX, array.getDimensionPixelSize(0, 0));
+        array.recycle();
+    }
+
+    @Override
     public void setMessage(CharSequence msg) {
         if (!TextUtils.isEmpty(msg)) {
             securityMessageChanged(msg);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
index 9b3f05e..4638c40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
@@ -48,6 +48,7 @@
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
@@ -126,6 +127,12 @@
     private final float mTouchSlop;
     // Minimum distance to move so that is can be considerd as a back swipe
     private final float mSwipeThreshold;
+    // The threshold where the touch needs to be at most, such that the arrow is displayed above the
+    // finger, otherwise it will be below
+    private final int mMinArrowPosition;
+    // The amount by which the arrow is shifted to avoid the finger
+    private final int mFingerOffset;
+
 
     private final int mNavBarHeight;
 
@@ -147,6 +154,8 @@
 
     private NavigationBarEdgePanel mEdgePanel;
     private WindowManager.LayoutParams mEdgePanelLp;
+    private final Rect mSamplingRect = new Rect();
+    private RegionSamplingHelper mRegionSamplingHelper;
 
     public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService) {
         final Resources res = context.getResources();
@@ -163,6 +172,9 @@
         mSwipeThreshold = res.getDimension(R.dimen.navigation_edge_action_drag_threshold);
 
         mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height);
+        mMinArrowPosition = res.getDimensionPixelSize(
+                R.dimen.navigation_edge_arrow_min_y);
+        mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset);
     }
 
     /**
@@ -208,6 +220,8 @@
         if (mEdgePanel != null) {
             mWm.removeView(mEdgePanel);
             mEdgePanel = null;
+            mRegionSamplingHelper.stop();
+            mRegionSamplingHelper = null;
         }
 
         if (!mIsEnabled) {
@@ -261,6 +275,18 @@
             mEdgePanelLp.windowAnimations = 0;
             mEdgePanel.setLayoutParams(mEdgePanelLp);
             mWm.addView(mEdgePanel, mEdgePanelLp);
+            mRegionSamplingHelper = new RegionSamplingHelper(mEdgePanel,
+                    new RegionSamplingHelper.SamplingCallback() {
+                        @Override
+                        public void onRegionDarknessChanged(boolean isRegionDark) {
+                            mEdgePanel.setIsDark(!isRegionDark, true /* animate */);
+                        }
+
+                        @Override
+                        public Rect getSampledRegion(View sampledView) {
+                            return mSamplingRect;
+                        }
+                    });
         }
     }
 
@@ -291,7 +317,7 @@
             // Verify if this is in within the touch region and we aren't in immersive mode, and
             // either the bouncer is showing or the notification panel is hidden
             int stateFlags = mOverviewProxyService.getSystemUiStateFlags();
-            mIsOnLeftEdge = ev.getX() < mEdgeWidth;
+            mIsOnLeftEdge = ev.getX() <= mEdgeWidth;
             mAllowGesture = (stateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
                     && ((stateFlags & SYSUI_STATE_BOUNCER_SHOWING) == SYSUI_STATE_BOUNCER_SHOWING
                             || (stateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0)
@@ -301,14 +327,13 @@
                         ? (Gravity.LEFT | Gravity.TOP)
                         : (Gravity.RIGHT | Gravity.TOP);
                 mEdgePanel.setIsLeftPanel(mIsOnLeftEdge);
-                mEdgePanelLp.y = MathUtils.constrain(
-                        (int) (ev.getY() - mEdgePanelLp.height / 2),
-                        0, mDisplaySize.y);
+                mEdgePanel.handleTouch(ev);
+                updateEdgePanelPosition(ev.getY());
                 mWm.updateViewLayout(mEdgePanel, mEdgePanelLp);
+                mRegionSamplingHelper.start(mSamplingRect);
 
                 mDownPoint.set(ev.getX(), ev.getY());
                 mThresholdCrossed = false;
-                mEdgePanel.handleTouch(ev);
             }
         } else if (mAllowGesture) {
             if (!mThresholdCrossed && ev.getAction() == MotionEvent.ACTION_MOVE) {
@@ -333,12 +358,9 @@
             // forward touch
             mEdgePanel.handleTouch(ev);
 
-            if (ev.getAction() == MotionEvent.ACTION_UP) {
-                float xDiff = ev.getX() - mDownPoint.x;
-                boolean exceedsThreshold = mIsOnLeftEdge
-                        ? (xDiff > mSwipeThreshold) : (-xDiff > mSwipeThreshold);
-                boolean performAction = exceedsThreshold
-                        && Math.abs(xDiff) > Math.abs(ev.getY() - mDownPoint.y);
+            boolean isUp = ev.getAction() == MotionEvent.ACTION_UP;
+            if (isUp) {
+                boolean performAction = mEdgePanel.shouldTriggerBack();
                 if (performAction) {
                     // Perform back
                     sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
@@ -347,9 +369,32 @@
                 mOverviewProxyService.notifyBackAction(performAction, (int) mDownPoint.x,
                         (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge);
             }
+            if (isUp || ev.getAction() == MotionEvent.ACTION_CANCEL) {
+                mRegionSamplingHelper.stop();
+            } else {
+                updateSamplingRect();
+                mRegionSamplingHelper.updateSamplingRect();
+            }
         }
     }
 
+    private void updateEdgePanelPosition(float touchY) {
+        float position = touchY - mFingerOffset;
+        position = Math.max(position, mMinArrowPosition);
+        position = (position - mEdgePanelLp.height / 2.0f);
+        mEdgePanelLp.y = MathUtils.constrain((int) position, 0, mDisplaySize.y);
+        updateSamplingRect();
+    }
+
+    private void updateSamplingRect() {
+        int top = mEdgePanelLp.y;
+        int left = mIsOnLeftEdge ? 0 : mDisplaySize.x - mEdgePanelLp.width;
+        int right = left + mEdgePanelLp.width;
+        int bottom = top + mEdgePanelLp.height;
+        mSamplingRect.set(left, top, right, bottom);
+        mEdgePanel.adjustRectToBoundingBox(mSamplingRect);
+    }
+
     @Override
     public void onDisplayAdded(int displayId) { }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java
index 86b5344..79c7ab1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java
@@ -16,93 +16,221 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.content.Context;
-import android.graphics.Canvas;
+import android.content.res.Configuration;
+import android.graphics.Canvas;;
 import android.graphics.Paint;
-import android.os.SystemClock;
+import android.graphics.Path;
+import android.graphics.Rect;
 import android.os.VibrationEffect;
-import android.util.FloatProperty;
 import android.util.MathUtils;
-import android.view.HapticFeedbackConstants;
+import android.view.ContextThemeWrapper;
 import android.view.MotionEvent;
+import android.view.VelocityTracker;
 import android.view.View;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
 
+import com.android.settingslib.Utils;
 import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.VibratorHelper;
 
+import androidx.core.graphics.ColorUtils;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
 public class NavigationBarEdgePanel extends View {
 
-    // TODO: read from resources once drawing is finalized.
-    private static final boolean SHOW_PROTECTION_STROKE = true;
-    private static final int PROTECTION_COLOR = 0xffc0c0c0;
-    private static final int STROKE_COLOR = 0xffe5e5e5;
-    private static final int PROTECTION_WIDTH_PX = 4;
-    private static final int BASE_EXTENT = 32;
-    private static final int ARROW_HEIGHT_DP = 32;
-    private static final int POINT_EXTENT_DP = 8;
-    private static final int ARROW_THICKNESS_DP = 4;
-    private static final float TRACK_LENGTH_MULTIPLIER = 1.5f;
-    private static final float START_POINTING_RATIO = 0.3f;
-    private static final float POINTEDNESS_BEFORE_SNAP_RATIO = 0.4f;
-    private static final int ANIM_DURATION_MS = 150;
-    private static final long HAPTIC_TIMEOUT_MS = 200;
+    private static final long COLOR_ANIMATION_DURATION_MS = 100;
+    private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 140;
+    private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100;
+
+    /**
+     * The size of the protection of the arrow in px. Only used if this is not background protected
+     */
+    private static final int PROTECTION_WIDTH_PX = 2;
+
+    /**
+     * The basic translation in dp where the arrow resides
+     */
+    private static final int BASE_TRANSLATION_DP = 32;
+
+    /**
+     * The length of the arrow leg measured from the center to the end
+     */
+    private static final int ARROW_LENGTH_DP = 18;
+
+    /**
+     * The angle measured from the xAxis, where the leg is when the arrow rests
+     */
+    private static final int ARROW_ANGLE_WHEN_EXTENDED_DEGREES = 56;
+
+    /**
+     * The angle that is added per 1000 px speed to the angle of the leg
+     */
+    private static final int ARROW_ANGLE_ADDED_PER_1000_SPEED = 8;
+
+    /**
+     * The maximum angle offset allowed due to speed
+     */
+    private static final int ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES = 4;
+
+    /**
+     * The thickness of the arrow. Adjusted to match the home handle (approximately)
+     */
+    private static final float ARROW_THICKNESS_DP = 2.5f;
+
+    /**
+     * The amount of rubber banding we do for the horizontal translation beyond the base translation
+     */
+    private static final int RUBBER_BAND_AMOUNT = 10;
+
+    /**
+     * The interpolator used to rubberband
+     */
+    private static final Interpolator RUBBER_BAND_INTERPOLATOR
+            = new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT, 1.0f, 1.0f, 1.0f);
+
+    /**
+     * The amount of rubber banding we do for the translation before base translation
+     */
+    private static final int RUBBER_BAND_AMOUNT_APPEAR = 4;
+
+    /**
+     * The interpolator used to rubberband the appearing of the arrow.
+     */
+    private static final Interpolator RUBBER_BAND_INTERPOLATOR_APPEAR
+            = new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT_APPEAR, 1.0f, 1.0f, 1.0f);
 
     private final VibratorHelper mVibratorHelper;
 
+    /**
+     * The paint the arrow is drawn with
+     */
     private final Paint mPaint = new Paint();
-    private final Paint mProtectionPaint = new Paint();
-
-    private final ObjectAnimator mEndAnimator;
-    private final ObjectAnimator mLegAnimator;
+    /**
+     * The paint the arrow protection is drawn with
+     */
+    private final Paint mProtectionPaint;
 
     private final float mDensity;
-    private final float mBaseExtent;
-    private final float mPointExtent;
-    private final float mHeight;
-    private final float mStrokeThickness;
+    private final float mBaseTranslation;
+    private final float mArrowLength;
+    private final float mArrowThickness;
+
+    /**
+     * The minimum delta needed in movement for the arrow to change direction / stop triggering back
+     */
+    private final float mMinDeltaForSwitch;
 
     private final float mSwipeThreshold;
+    private final Path mArrowPath = new Path();
 
+    private final SpringAnimation mAngleAnimation;
+    private final SpringAnimation mTranslationAnimation;
+    private final SpringAnimation mVerticalTranslationAnimation;
+    private final SpringForce mAngleAppearForce;
+    private final SpringForce mAngleDisappearForce;
+    private final ValueAnimator mArrowColorAnimator;
+    private final ValueAnimator mArrowDisappearAnimation;
+    private final SpringForce mRegularTranslationSpring;
+    private final SpringForce mTriggerBackSpring;
+
+    private VelocityTracker mVelocityTracker;
+    private boolean mIsDark = false;
+    private boolean mShowProtection = false;
+    private int mProtectionColorLight;
+    private int mArrowPaddingEnd;
+    private int mArrowColorLight;
+    private int mProtectionColorDark;
+    private int mArrowColorDark;
+    private int mProtectionColor;
+    private int mArrowColor;
+
+    /**
+     * True if the panel is currently on the left of the screen
+     */
     private boolean mIsLeftPanel;
 
     private float mStartX;
+    private float mStartY;
+    private float mCurrentAngle;
+    /**
+     * The current translation of the arrow
+     */
+    private float mCurrentTranslation;
+    /**
+     * Where the arrow will be in the resting position.
+     */
+    private float mDesiredTranslation;
 
     private boolean mDragSlopPassed;
-    private long mLastSlopHapticTime;
-    private boolean mGestureDetected;
     private boolean mArrowsPointLeft;
-    private float mGestureLength;
-    private float mLegProgress;
-    private float mDragProgress;
+    private float mMaxTranslation;
+    private boolean mTriggerBack;
+    private float mPreviousTouchTranslation;
+    private float mTotalTouchDelta;
+    private float mVerticalTranslation;
+    private float mDesiredVerticalTranslation;
+    private float mDesiredAngle;
+    private float mAngleOffset;
+    private int mArrowStartColor;
+    private int mCurrentArrowColor;
+    private float mDisappearAmount;
 
-    // How much the "legs" of the back arrow have proceeded from being a line to an arrow.
-    private static final FloatProperty<NavigationBarEdgePanel> LEG_PROGRESS =
-            new FloatProperty<NavigationBarEdgePanel>("legProgress") {
+    private DynamicAnimation.OnAnimationEndListener mSetGoneEndListener
+            = new DynamicAnimation.OnAnimationEndListener() {
+        @Override
+        public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
+                float velocity) {
+            animation.removeEndListener(this);
+            if (!canceled) {
+                setVisibility(GONE);
+            }
+        }
+    };
+    private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_ANGLE =
+            new FloatPropertyCompat<NavigationBarEdgePanel>("currentAngle") {
         @Override
         public void setValue(NavigationBarEdgePanel object, float value) {
-            object.setLegProgress(value);
+            object.setCurrentAngle(value);
         }
 
         @Override
-        public Float get(NavigationBarEdgePanel object) {
-            return object.getLegProgress();
+        public float getValue(NavigationBarEdgePanel object) {
+            return object.getCurrentAngle();
         }
     };
 
-    // How far across the view the arrow should be drawn.
-    private static final FloatProperty<NavigationBarEdgePanel> DRAG_PROGRESS =
-            new FloatProperty<NavigationBarEdgePanel>("dragProgress") {
+    private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_TRANSLATION =
+            new FloatPropertyCompat<NavigationBarEdgePanel>("currentTranslation") {
 
                 @Override
                 public void setValue(NavigationBarEdgePanel object, float value) {
-                    object.setDragProgress(value);
+                    object.setCurrentTranslation(value);
                 }
 
                 @Override
-                public Float get(NavigationBarEdgePanel object) {
-                    return object.getDragProgress();
+                public float getValue(NavigationBarEdgePanel object) {
+                    return object.getCurrentTranslation();
+                }
+            };
+    private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_VERTICAL_TRANSLATION =
+            new FloatPropertyCompat<NavigationBarEdgePanel>("verticalTranslation") {
+
+                @Override
+                public void setValue(NavigationBarEdgePanel object, float value) {
+                    object.setVerticalTranslation(value);
+                }
+
+                @Override
+                public float getValue(NavigationBarEdgePanel object) {
+                    return object.getVerticalTranslation();
                 }
             };
 
@@ -111,62 +239,200 @@
 
         mVibratorHelper = Dependency.get(VibratorHelper.class);
 
-        mEndAnimator = ObjectAnimator.ofFloat(this, DRAG_PROGRESS, 1f);
-        mEndAnimator.setAutoCancel(true);
-        mEndAnimator.setDuration(ANIM_DURATION_MS);
-
-        mLegAnimator = ObjectAnimator.ofFloat(this, LEG_PROGRESS, 1f);
-        mLegAnimator.setAutoCancel(true);
-        mLegAnimator.setDuration(ANIM_DURATION_MS);
-
         mDensity = context.getResources().getDisplayMetrics().density;
 
-        mBaseExtent = dp(BASE_EXTENT);
-        mHeight = dp(ARROW_HEIGHT_DP);
-        mPointExtent = dp(POINT_EXTENT_DP);
-        mStrokeThickness = dp(ARROW_THICKNESS_DP);
+        mBaseTranslation = dp(BASE_TRANSLATION_DP);
+        mArrowLength = dp(ARROW_LENGTH_DP);
+        mArrowThickness = dp(ARROW_THICKNESS_DP);
+        mMinDeltaForSwitch = dp(32);
 
-        mPaint.setStrokeWidth(mStrokeThickness);
+        mPaint.setStrokeWidth(mArrowThickness);
         mPaint.setStrokeCap(Paint.Cap.ROUND);
-        mPaint.setColor(STROKE_COLOR);
         mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeJoin(Paint.Join.ROUND);
 
-        mProtectionPaint.setStrokeWidth(mStrokeThickness + PROTECTION_WIDTH_PX);
-        mProtectionPaint.setStrokeCap(Paint.Cap.ROUND);
-        mProtectionPaint.setColor(PROTECTION_COLOR);
-        mProtectionPaint.setAntiAlias(true);
+        mArrowColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mArrowColorAnimator.setDuration(COLOR_ANIMATION_DURATION_MS);
+        mArrowColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                int newColor = ColorUtils.blendARGB(mArrowStartColor, mArrowColor,
+                        animation.getAnimatedFraction());
+                setCurrentArrowColor(newColor);
+            }
+        });
 
-        // Both panels arrow point the same way
-        mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
+        mArrowDisappearAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mArrowDisappearAnimation.setDuration(DISAPPEAR_ARROW_ANIMATION_DURATION_MS);
+        mArrowDisappearAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mArrowDisappearAnimation.addUpdateListener(animation -> {
+            mDisappearAmount = (float) animation.getAnimatedValue();
+            invalidate();
+        });
+
+        mAngleAnimation =
+                new SpringAnimation(this, CURRENT_ANGLE);
+        mAngleAppearForce = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_LOW)
+                .setDampingRatio(0.4f)
+                .setFinalPosition(ARROW_ANGLE_WHEN_EXTENDED_DEGREES);
+        mAngleDisappearForce = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+                .setFinalPosition(90);
+        mAngleAnimation.setSpring(mAngleAppearForce).setMaxValue(90);
+
+        mTranslationAnimation =
+                new SpringAnimation(this, CURRENT_TRANSLATION);
+        mRegularTranslationSpring = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mTriggerBackSpring = new SpringForce()
+                .setStiffness(450)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mTranslationAnimation.setSpring(mRegularTranslationSpring);
+        mVerticalTranslationAnimation =
+                new SpringAnimation(this, CURRENT_VERTICAL_TRANSLATION);
+        mVerticalTranslationAnimation.setSpring(
+                new SpringForce()
+                        .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+
+        mProtectionPaint = new Paint(mPaint);
+        mProtectionPaint.setStrokeWidth(mArrowThickness + PROTECTION_WIDTH_PX);
+        loadDimens();
+
+        loadColors(context);
+        updateArrowDirection();
 
         mSwipeThreshold = context.getResources()
                 .getDimension(R.dimen.navigation_edge_action_drag_threshold);
         setVisibility(GONE);
     }
 
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    public boolean shouldTriggerBack() {
+        return mTriggerBack;
+    }
+
+    public void setIsDark(boolean isDark, boolean animate) {
+        mIsDark = isDark;
+        updateIsDark(animate);
+    }
+
+    public void setShowProtection(boolean showProtection) {
+        mShowProtection = showProtection;
+        invalidate();
+    }
+
     public void setIsLeftPanel(boolean isLeftPanel) {
         mIsLeftPanel = isLeftPanel;
     }
 
-    @Override
-    protected void onDraw(Canvas canvas) {
-        float edgeOffset = mBaseExtent * mDragProgress - mStrokeThickness;
-        float animatedOffset = mPointExtent * mLegProgress;
-        canvas.save();
-        canvas.translate(
-                mIsLeftPanel ? edgeOffset : getWidth() - edgeOffset,
-                (getHeight() - mHeight) * 0.5f);
+    /**
+     * Adjust the rect to conform the the actual visible bounding box of the arrow.
+     *
+     * @param samplingRect the existing bounding box in screen coordinates, to be modified
+     */
+    public void adjustRectToBoundingBox(Rect samplingRect) {
+        float translation = mDesiredTranslation;
+        if (!mTriggerBack) {
+            // Let's take the resting position and bounds as the sampling rect, since we are not
+            // visible right now
+            translation = mBaseTranslation;
+            if (mIsLeftPanel && mArrowsPointLeft
+                    || (!mIsLeftPanel && !mArrowsPointLeft)) {
+                // If we're on the left we should move less, because the arrow is facing the other
+                // direction
+                translation -= getStaticArrowWidth();
+            }
+        }
+        float left = translation - mArrowThickness / 2.0f;
+        left = mIsLeftPanel ? left : samplingRect.width() - left;
 
-        float outsideX = mArrowsPointLeft ? animatedOffset : 0;
-        float middleX = mArrowsPointLeft ? 0 : animatedOffset;
-
-        if (SHOW_PROTECTION_STROKE) {
-            canvas.drawLine(outsideX, 0, middleX, mHeight * 0.5f, mProtectionPaint);
-            canvas.drawLine(middleX, mHeight * 0.5f, outsideX, mHeight, mProtectionPaint);
+        // Let's calculate the position of the end based on the angle
+        float width = getStaticArrowWidth();
+        float height = polarToCartY(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength * 2.0f;
+        if (!mArrowsPointLeft) {
+            left -= width;
         }
 
-        canvas.drawLine(outsideX, 0, middleX, mHeight * 0.5f, mPaint);
-        canvas.drawLine(middleX, mHeight * 0.5f, outsideX, mHeight, mPaint);
+        float top = (getHeight() * 0.5f) + mDesiredVerticalTranslation - height / 2.0f;
+        samplingRect.offset((int) left, (int) top);
+        samplingRect.set(samplingRect.left, samplingRect.top,
+                (int) (samplingRect.left + width),
+                (int) (samplingRect.top + height));
+    }
+
+    /**
+     * Updates the UI based on the motion events passed in device co-ordinates
+     */
+    public void handleTouch(MotionEvent event) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(event);
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN : {
+                mDragSlopPassed = false;
+                resetOnDown();
+                mStartX = event.getX();
+                mStartY = event.getY();
+                setVisibility(VISIBLE);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                handleMoveEvent(event);
+                break;
+            }
+            // Fall through
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL: {
+                if (mTriggerBack) {
+                    triggerBackAnimation();
+                } else {
+                    if (mTranslationAnimation.isRunning()) {
+                        mTranslationAnimation.addEndListener(mSetGoneEndListener);
+                    } else {
+                        setVisibility(GONE);
+                    }
+                }
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+            }
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updateArrowDirection();
+        loadDimens();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f;
+        canvas.save();
+        canvas.translate(
+                mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition,
+                (getHeight() * 0.5f) + mVerticalTranslation);
+
+        // Let's calculate the position of the end based on the angle
+        float x = (polarToCartX(mCurrentAngle) * mArrowLength);
+        float y = (polarToCartY(mCurrentAngle) * mArrowLength);
+        Path arrowPath = calculatePath(x,y);
+        if (mShowProtection) {
+            canvas.drawPath(arrowPath, mProtectionPaint);
+        }
+
+        canvas.drawPath(arrowPath, mPaint);
         canvas.restore();
     }
 
@@ -175,107 +441,297 @@
         super.onLayout(changed, left, top, right, bottom);
 
         // TODO: read the gesture length from the nav controller.
-        mGestureLength = getWidth();
+        mMaxTranslation = getWidth() - mArrowPaddingEnd;
     }
 
-    private void setLegProgress(float progress) {
-        mLegProgress = progress;
+    private void loadDimens() {
+        mArrowPaddingEnd = getContext().getResources().getDimensionPixelSize(
+                R.dimen.navigation_edge_panel_padding);
+    }
+
+    private void updateArrowDirection() {
+        // Both panels arrow point the same way
+        mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
         invalidate();
     }
 
-    private float getLegProgress() {
-        return mLegProgress;
+    private void loadColors(Context context) {
+        final int dualToneDarkTheme = Utils.getThemeAttr(context, R.attr.darkIconTheme);
+        final int dualToneLightTheme = Utils.getThemeAttr(context, R.attr.lightIconTheme);
+        Context lightContext = new ContextThemeWrapper(context, dualToneLightTheme);
+        Context darkContext = new ContextThemeWrapper(context, dualToneDarkTheme);
+        mArrowColorLight = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor);
+        mArrowColorDark = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor);
+        mProtectionColorDark = mArrowColorLight;
+        mProtectionColorLight = mArrowColorDark;
+        updateIsDark(false /* animate */);
     }
 
-    private void setDragProgress(float dragProgress) {
-        mDragProgress = dragProgress;
-        invalidate();
-    }
-
-    private float getDragProgress() {
-        return mDragProgress;
-    }
-
-    private void hide() {
-        animate().alpha(0f).setDuration(ANIM_DURATION_MS)
-                .withEndAction(() -> setVisibility(GONE));
-    }
-
-    /**
-     * Updates the UI based on the motion events passed in device co-ordinates
-     */
-    public void handleTouch(MotionEvent event) {
-        switch (event.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN : {
-                mDragSlopPassed = false;
-                mEndAnimator.cancel();
-                mLegAnimator.cancel();
-                animate().cancel();
-                setLegProgress(0f);
-                setDragProgress(0f);
-                mStartX = event.getX();
-                setVisibility(VISIBLE);
-                break;
-            }
-            case MotionEvent.ACTION_MOVE: {
-                handleNewSwipePoint(event.getX());
-                break;
-            }
-            // Fall through
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL: {
-                hide();
-                break;
-            }
+    private void updateIsDark(boolean animate) {
+        // TODO: Maybe animate protection as well
+        mProtectionColor = mIsDark ? mProtectionColorDark : mProtectionColorLight;
+        mProtectionPaint.setColor(mProtectionColor);
+        mArrowColor = mIsDark ? mArrowColorDark : mArrowColorLight;
+        mArrowColorAnimator.cancel();
+        if (!animate) {
+            setCurrentArrowColor(mArrowColor);
+        } else {
+            mArrowStartColor = mCurrentArrowColor;
+            mArrowColorAnimator.start();
         }
     }
 
-    private void handleNewSwipePoint(float x) {
-        float dist = MathUtils.abs(x - mStartX);
+    private void setCurrentArrowColor(int color) {
+        mCurrentArrowColor = color;
+        mPaint.setColor(color);
+        invalidate();
+    }
+
+    private float getStaticArrowWidth() {
+        return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength;
+    }
+
+    private float polarToCartX(float angleInDegrees) {
+        return (float) Math.cos(Math.toRadians(angleInDegrees));
+    }
+
+    private float polarToCartY(float angleInDegrees) {
+        return (float) Math.sin(Math.toRadians(angleInDegrees));
+    }
+
+    private Path calculatePath(float x, float y) {
+        if (!mArrowsPointLeft) {
+            x = -x;
+        }
+        float extent = 1.0f - mDisappearAmount;
+        x = x * extent;
+        y = y * extent;
+        mArrowPath.reset();
+        mArrowPath.moveTo(x, y);
+        mArrowPath.lineTo(0, 0);
+        mArrowPath.lineTo(x, -y);
+        return mArrowPath;
+    }
+
+    private float getCurrentAngle() {
+        return mCurrentAngle;
+    }
+
+    private float getCurrentTranslation() {
+        return mCurrentTranslation;
+    }
+
+    private void triggerBackAnimation() {
+
+        mVelocityTracker.computeCurrentVelocity(1000);
+        // Only do the extra translation if we're not already flinging
+        boolean doExtraTranslation = Math.abs(mVelocityTracker.getXVelocity()) < 1000;
+        if (doExtraTranslation) {
+            setDesiredTranslation(mDesiredTranslation + dp(16), true /* animate */);
+        }
+
+        // Let's also snap the angle a bit
+        if (mAngleOffset < -4) {
+            mAngleOffset = Math.max(-16, mAngleOffset - 16);
+            updateAngle(true /* animated */);
+        }
+
+        // Finally, after the translation, animate back and disappear the arrow
+        Runnable translationEnd = () -> {
+            setTriggerBack(false /* false */, true /* animate */);
+            mTranslationAnimation.setSpring(mTriggerBackSpring);
+            setDesiredTranslation(0, true /* animated */);
+            animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS)
+                    .withEndAction(() -> setVisibility(GONE));
+            mArrowDisappearAnimation.start();
+        };
+        if (mTranslationAnimation.isRunning()) {
+            mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() {
+                @Override
+                public void onAnimationEnd(DynamicAnimation animation, boolean canceled,
+                        float value,
+                        float velocity) {
+                    animation.removeEndListener(this);
+                    if (!canceled) {
+                        translationEnd.run();
+                    }
+                }
+            });
+        } else {
+            translationEnd.run();
+        }
+
+    }
+
+    private void resetOnDown() {
+        animate().cancel();
+        mAngleAnimation.cancel();
+        mTranslationAnimation.cancel();
+        mVerticalTranslationAnimation.cancel();
+        mArrowDisappearAnimation.cancel();
+        mAngleOffset = 0;
+        mTranslationAnimation.setSpring(mRegularTranslationSpring);
+        // Reset the arrow to the side
+        setTriggerBack(false /* triggerBack */, false /* animated */);
+        setDesiredTranslation(0, false /* animated */);
+        setCurrentTranslation(0);
+        mPreviousTouchTranslation = 0;
+        mTotalTouchDelta = 0;
+        setDesiredVerticalTransition(0, false /* animated */);
+    }
+
+    private void handleMoveEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        float touchTranslation = MathUtils.abs(x - mStartX);
+        float yOffset = y - mStartY;
+        float delta = touchTranslation - mPreviousTouchTranslation;
+        if (Math.abs(delta) > 0) {
+            if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) {
+                mTotalTouchDelta += delta;
+            } else {
+                mTotalTouchDelta = delta;
+            }
+        }
+        mPreviousTouchTranslation = touchTranslation;
 
         // Apply a haptic on drag slop passed
-        if (!mDragSlopPassed && dist > mSwipeThreshold) {
+        if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) {
             mDragSlopPassed = true;
             mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
-            mLastSlopHapticTime = SystemClock.uptimeMillis();
+
+            // Let's show the arrow and animate it in!
+            mDisappearAmount = 0.0f;
             setAlpha(1f);
+            // And animate it go to back by default!
+            setTriggerBack(true /* triggerBack */, true /* animated */);
         }
 
-        setDragProgress(MathUtils.constrainedMap(
-                0, 1.0f,
-                0, mGestureLength * TRACK_LENGTH_MULTIPLIER,
-                dist));
-
-        if (dist < mGestureLength) {
-            float calculatedLegProgress = MathUtils.constrainedMap(
-                    0f, POINTEDNESS_BEFORE_SNAP_RATIO,
-                    mGestureLength * START_POINTING_RATIO, mGestureLength,
-                    dist);
-
-            // Blend animated value with drag calculated value, allow the gesture to continue
-            // while the animation is playing with jump cuts in the animation.
-            setLegProgress(MathUtils.lerp(calculatedLegProgress, mLegProgress, mDragProgress));
-
-            if (mGestureDetected) {
-                mGestureDetected = false;
-
-                mLegAnimator.setFloatValues(POINTEDNESS_BEFORE_SNAP_RATIO);
-                mLegAnimator.start();
-            }
+        // Let's make sure we only go to the baseextend and apply rubberbanding afterwards
+        if (touchTranslation > mBaseTranslation) {
+            float diff = touchTranslation - mBaseTranslation;
+            float progress = MathUtils.saturate(diff / (mBaseTranslation * RUBBER_BAND_AMOUNT));
+            progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+                    * (mMaxTranslation - mBaseTranslation);
+            touchTranslation = mBaseTranslation + progress;
         } else {
-            if (!mGestureDetected) {
-                // Prevent another haptic if it was just used
-                if (SystemClock.uptimeMillis() - mLastSlopHapticTime > HAPTIC_TIMEOUT_MS) {
-                    performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
-                }
-                mGestureDetected = true;
+            float diff = mBaseTranslation - touchTranslation;
+            float progress = MathUtils.saturate(diff / mBaseTranslation);
+            progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress)
+                    * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR);
+            touchTranslation = mBaseTranslation - progress;
+        }
+        // By default we just assume the current direction is kept
+        boolean triggerBack = mTriggerBack;
 
-                mLegAnimator.setFloatValues(1f);
-                mLegAnimator.start();
+        //  First lets see if we had continuous motion in one direction for a while
+        if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) {
+            triggerBack = mTotalTouchDelta > 0;
+        }
+
+        // Then, let's see if our velocity tells us to change direction
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float xVelocity = mVelocityTracker.getXVelocity();
+        float yVelocity = mVelocityTracker.getYVelocity();
+        float velocity = MathUtils.mag(xVelocity, yVelocity);
+        mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED,
+                ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity);
+        if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) {
+            mAngleOffset *= -1;
+        }
+
+        // Last if the direction in Y is bigger than X * 2 we also abort
+        if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) {
+            triggerBack = false;
+        }
+        setTriggerBack(triggerBack, true /* animated */);
+
+        if (!mTriggerBack) {
+            touchTranslation = 0;
+        } else if (mIsLeftPanel && mArrowsPointLeft
+                || (!mIsLeftPanel && !mArrowsPointLeft)) {
+            // If we're on the left we should move less, because the arrow is facing the other
+            // direction
+            touchTranslation -= getStaticArrowWidth();
+        }
+        setDesiredTranslation(touchTranslation, true /* animated */);
+        updateAngle(true /* animated */);
+
+        float maxYOffset = getHeight() / 2.0f - mArrowLength;
+        float progress = MathUtils.constrain(
+                Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT),
+                0, 1);
+        float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+                * maxYOffset * Math.signum(yOffset);
+        setDesiredVerticalTransition(verticalTranslation, true /* animated */);
+    }
+
+    private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) {
+        if (mDesiredVerticalTranslation != verticalTranslation) {
+            mDesiredVerticalTranslation = verticalTranslation;
+            if (!animated) {
+                setVerticalTranslation(verticalTranslation);
+            } else {
+                mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation);
+            }
+            invalidate();
+        }
+    }
+
+    private void setVerticalTranslation(float verticalTranslation) {
+        mVerticalTranslation = verticalTranslation;
+        invalidate();
+    }
+
+    private float getVerticalTranslation() {
+        return mVerticalTranslation;
+    }
+
+    private void setDesiredTranslation(float desiredTranslation, boolean animated) {
+        if (mDesiredTranslation != desiredTranslation) {
+            mDesiredTranslation = desiredTranslation;
+            if (!animated) {
+                setCurrentTranslation(desiredTranslation);
+            } else {
+                mTranslationAnimation.animateToFinalPosition(desiredTranslation);
             }
         }
     }
 
+    private void setCurrentTranslation(float currentTranslation) {
+        mCurrentTranslation = currentTranslation;
+        invalidate();
+    }
+
+    private void setTriggerBack(boolean triggerBack, boolean animated) {
+        if (mTriggerBack != triggerBack) {
+            mTriggerBack = triggerBack;
+            mAngleAnimation.cancel();
+            updateAngle(animated);
+            // Whenever the trigger back state changes the existing translation animation should be
+            // cancelled
+            mTranslationAnimation.cancel();
+        }
+    }
+
+    private void updateAngle(boolean animated) {
+        float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90;
+        if (newAngle != mDesiredAngle) {
+            if (!animated) {
+                setCurrentAngle(newAngle);
+            } else {
+                mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce);
+                mAngleAnimation.animateToFinalPosition(newAngle);
+            }
+            mDesiredAngle = newAngle;
+        }
+    }
+
+    private void setCurrentAngle(float currentAngle) {
+        mCurrentAngle = currentAngle;
+        invalidate();
+    }
+
     private float dp(float dp) {
         return mDensity * dp;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java
new file mode 100644
index 0000000..d59319e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.IBinder;
+import android.provider.Settings;
+import android.view.CompositionSamplingListener;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import com.android.systemui.R;
+
+/**
+ * A helper class to sample regions on the screen and inspect its luminosity.
+ */
+public class RegionSamplingHelper implements View.OnAttachStateChangeListener,
+        View.OnLayoutChangeListener {
+
+    private final Handler mHandler = new Handler();
+    private final View mSampledView;
+
+    private final CompositionSamplingListener mSamplingListener;
+    private final Runnable mUpdateSamplingListener = this::updateSamplingListener;
+
+    /**
+     * The requested sampling bounds that we want to sample from
+     */
+    private final Rect mSamplingRequestBounds = new Rect();
+
+    /**
+     * The sampling bounds that are currently registered.
+     */
+    private final Rect mRegisteredSamplingBounds = new Rect();
+    private final SamplingCallback mCallback;
+    private boolean mSamplingEnabled = false;
+    private boolean mSamplingListenerRegistered = false;
+
+    private float mLastMedianLuma;
+    private float mCurrentMedianLuma;
+    private boolean mWaitingOnDraw;
+
+    // Passing the threshold of this luminance value will make the button black otherwise white
+    private final float mLuminanceThreshold;
+    private final float mLuminanceChangeThreshold;
+    private boolean mFirstSamplingAfterStart;
+    private SurfaceControl mRegisteredStopLayer = null;
+    private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() {
+        @Override
+        public void onDraw() {
+            // We need to post the remove runnable, since it's not allowed to remove in onDraw
+            mHandler.post(mRemoveDrawRunnable);
+            RegionSamplingHelper.this.onDraw();
+        }
+    };
+    private Runnable mRemoveDrawRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw);
+        }
+    };
+
+    public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback) {
+        mSamplingListener = new CompositionSamplingListener(
+                sampledView.getContext().getMainExecutor()) {
+            @Override
+            public void onSampleCollected(float medianLuma) {
+                if (mSamplingEnabled) {
+                    updateMediaLuma(medianLuma);
+                }
+            }
+        };
+        mSampledView = sampledView;
+        mSampledView.addOnAttachStateChangeListener(this);
+        mSampledView.addOnLayoutChangeListener(this);
+
+        final Resources res = sampledView.getResources();
+        mLuminanceThreshold = res.getFloat(R.dimen.navigation_luminance_threshold);
+        mLuminanceChangeThreshold = res.getFloat(R.dimen.navigation_luminance_change_threshold);
+        mCallback = samplingCallback;
+    }
+
+    private void onDraw() {
+        if (mWaitingOnDraw) {
+            mWaitingOnDraw = false;
+            updateSamplingListener();
+        }
+    }
+
+    void start(Rect initialSamplingBounds) {
+        if (!mCallback.isSamplingEnabled()) {
+            return;
+        }
+        if (initialSamplingBounds != null) {
+            mSamplingRequestBounds.set(initialSamplingBounds);
+        }
+        mSamplingEnabled = true;
+        // make sure we notify once
+        mLastMedianLuma = -1;
+        mFirstSamplingAfterStart = true;
+        updateSamplingListener();
+    }
+
+    void stop() {
+        mSamplingEnabled = false;
+        updateSamplingListener();
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View view) {
+        updateSamplingListener();
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View view) {
+        // isAttachedToWindow is only changed after this call to the listeners, so let's post it
+        // instead
+        postUpdateSamplingListener();
+    }
+
+    @Override
+    public void onLayoutChange(View v, int left, int top, int right, int bottom,
+            int oldLeft, int oldTop, int oldRight, int oldBottom) {
+        updateSamplingRect();
+    }
+
+    private void postUpdateSamplingListener() {
+        mHandler.removeCallbacks(mUpdateSamplingListener);
+        mHandler.post(mUpdateSamplingListener);
+    }
+
+    private void updateSamplingListener() {
+        boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty()
+                && (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart);
+        if (isSamplingEnabled) {
+            SurfaceControl stopLayerControl = mSampledView.getViewRootImpl().getSurfaceControl();
+            if (!stopLayerControl.isValid()) {
+                if (!mWaitingOnDraw) {
+                    mWaitingOnDraw = true;
+                    // The view might be attached but we haven't drawn yet, so wait until the
+                    // next draw to update the listener again with the stop layer, such that our
+                    // own drawing doesn't affect the sampling.
+                    if (mHandler.hasCallbacks(mRemoveDrawRunnable)) {
+                        mHandler.removeCallbacks(mRemoveDrawRunnable);
+                    } else {
+                        mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw);
+                    }
+                }
+                // If there's no valid surface, let's just sample without a stop layer, so we
+                // don't have to delay
+                stopLayerControl = null;
+            }
+            if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds)
+                    || mRegisteredStopLayer != stopLayerControl) {
+                // We only want to reregister if something actually changed
+                unregisterSamplingListener();
+                mSamplingListenerRegistered = true;
+                CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY,
+                        stopLayerControl != null ? stopLayerControl.getHandle() : null,
+                        mSamplingRequestBounds);
+                mRegisteredSamplingBounds.set(mSamplingRequestBounds);
+                mRegisteredStopLayer = stopLayerControl;
+            }
+            mFirstSamplingAfterStart = false;
+        } else {
+            unregisterSamplingListener();
+        }
+    }
+
+    private void unregisterSamplingListener() {
+        if (mSamplingListenerRegistered) {
+            mSamplingListenerRegistered = false;
+            mRegisteredStopLayer = null;
+            mRegisteredSamplingBounds.setEmpty();
+            CompositionSamplingListener.unregister(mSamplingListener);
+        }
+    }
+
+    private void updateMediaLuma(float medianLuma) {
+        mCurrentMedianLuma = medianLuma;
+
+        // If the difference between the new luma and the current luma is larger than threshold
+        // then apply the current luma, this is to prevent small changes causing colors to flicker
+        if (Math.abs(mCurrentMedianLuma - mLastMedianLuma) > mLuminanceChangeThreshold) {
+            mCallback.onRegionDarknessChanged(medianLuma < mLuminanceThreshold /* isRegionDark */);
+            mLastMedianLuma = medianLuma;
+        }
+    }
+
+    public void updateSamplingRect() {
+        Rect sampledRegion = mCallback.getSampledRegion(mSampledView);
+        if (!mSamplingRequestBounds.equals(sampledRegion)) {
+            mSamplingRequestBounds.set(sampledRegion);
+            updateSamplingListener();
+        }
+    }
+
+    public interface SamplingCallback {
+        /**
+         * Called when the darkness of the sampled region changes
+         * @param isRegionDark true if the sampled luminance is below the luminance threshold
+         */
+        void onRegionDarknessChanged(boolean isRegionDark);
+
+        /**
+         * Get the sampled region of interest from the sampled view
+         * @param sampledView The view that this helper is attached to for convenience
+         * @return the region to be sampled in sceen coordinates. Return {@code null} to avoid
+         * sampling in this frame
+         */
+        Rect getSampledRegion(View sampledView);
+
+        /**
+         * @return if sampling should be enabled in the current configuration
+         */
+        default boolean isSamplingEnabled() {
+            return true;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 24f8fc2..76136df 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -828,6 +828,13 @@
                         sb.append(compName);
                         Slog.w(TAG, sb.toString());
                         stopping.add(service);
+
+                        // If the app is under bg restrictions, also make sure that
+                        // any notification is dismissed
+                        if (appRestrictedAnyInBackground(
+                                service.appInfo.uid, service.packageName)) {
+                            cancelForegroundNotificationLocked(service);
+                        }
                     }
                 }
             }
@@ -1232,6 +1239,10 @@
         }
     }
 
+    private boolean appIsTopLocked(int uid) {
+        return mAm.getUidState(uid) <= ActivityManager.PROCESS_STATE_TOP;
+    }
+
     /**
      * @param id Notification ID.  Zero === exit foreground state for the given service.
      */
@@ -1318,8 +1329,11 @@
                         throw new SecurityException("Foreground not allowed as per app op");
                 }
 
-                if (!ignoreForeground &&
-                        appRestrictedAnyInBackground(r.appInfo.uid, r.packageName)) {
+                // Apps that are TOP or effectively similar may call startForeground() on
+                // their services even if they are restricted from doing that while in bg.
+                if (!ignoreForeground
+                        && !appIsTopLocked(r.appInfo.uid)
+                        && appRestrictedAnyInBackground(r.appInfo.uid, r.packageName)) {
                     Slog.w(TAG,
                             "Service.startForeground() not allowed due to bg restriction: service "
                             + r.shortInstanceName);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index dcb3a22..fc5d393 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -13853,6 +13853,18 @@
             throw new IllegalArgumentException("callingPackage cannot be null");
         }
 
+        // Ensure that instanceName, which is caller provided, does not contain
+        // unusual characters.
+        if (instanceName != null) {
+            for (int i = 0; i < instanceName.length(); ++i) {
+                char c = instanceName.charAt(i);
+                if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+                            || (c >= '0' && c <= '9') || c == '_' || c == '.')) {
+                    throw new IllegalArgumentException("Illegal instanceName");
+                }
+            }
+        }
+
         synchronized(this) {
             return mServices.bindServiceLocked(caller, token, service,
                     resolvedType, connection, flags, instanceName, callingPackage, userId);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e2033c6..21a862a 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4816,10 +4816,11 @@
             NotificationRecord oldRecord) {
         Notification notification = r.getNotification();
 
-        // Does the app want to bubble & have permission to bubble?
+        // Does the app want to bubble & is able to bubble
         boolean canBubble = notification.getBubbleMetadata() != null
                 && mPreferencesHelper.areBubblesAllowed(pkg, userId)
-                && r.getChannel().canBubble();
+                && r.getChannel().canBubble()
+                && !mActivityManager.isLowRamDevice();
 
         // Is the app in the foreground?
         final boolean appIsForeground =
diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java
index 7f1b4c0..efd3e1c 100644
--- a/services/core/java/com/android/server/wm/TaskPositioner.java
+++ b/services/core/java/com/android/server/wm/TaskPositioner.java
@@ -450,6 +450,11 @@
 
         // This is a moving or scrolling operation.
         mTask.mStack.getDimBounds(mTmpRect);
+        // If a target window is covered by system bar, there is no way to move it again by touch.
+        // So we exclude them from stack bounds. and then it will be shown inside stable area.
+        Rect stableBounds = new Rect();
+        mDisplayContent.getStableRect(stableBounds);
+        mTmpRect.intersect(stableBounds);
 
         int nX = (int) x;
         int nY = (int) y;
diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java
index 10fb559..d4d157fb 100644
--- a/services/core/java/com/android/server/wm/TaskRecord.java
+++ b/services/core/java/com/android/server/wm/TaskRecord.java
@@ -2209,9 +2209,27 @@
             // by policy, make sure the window remains within parent somewhere
             final float density =
                     ((float) newParentConfig.densityDpi) / DisplayMetrics.DENSITY_DEFAULT;
-            fitWithinBounds(outOverrideBounds, newParentConfig.windowConfiguration.getBounds(),
+            final Rect parentBounds =
+                    new Rect(newParentConfig.windowConfiguration.getBounds());
+            final ActivityDisplay display = mStack.getDisplay();
+            if (display != null && display.mDisplayContent != null) {
+                // If a freeform window moves below system bar, there is no way to move it again
+                // by touch. Because its caption is covered by system bar. So we exclude them
+                // from stack bounds. and then caption will be shown inside stable area.
+                final Rect stableBounds = new Rect();
+                display.mDisplayContent.getStableRect(stableBounds);
+                parentBounds.intersect(stableBounds);
+            }
+
+            fitWithinBounds(outOverrideBounds, parentBounds,
                     (int) (density * WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP),
                     (int) (density * WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP));
+
+            // Prevent to overlap caption with stable insets.
+            final int offsetTop = parentBounds.top - outOverrideBounds.top;
+            if (offsetTop > 0) {
+                outOverrideBounds.offset(0, offsetTop);
+            }
         }
         computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java
index b766822..eb90295 100644
--- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java
+++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java
@@ -21,9 +21,14 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
 
 import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
 import android.util.Pair;
 
 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
@@ -32,9 +37,11 @@
 import org.junit.rules.TestWatcher;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
+import org.mockito.Mockito;
 import org.mockito.quality.Strictness;
 import org.mockito.stubbing.Answer;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -57,6 +64,8 @@
     private StaticMockitoSession mMockitoSession;
     private Map<DeviceConfig.OnPropertyChangedListener, Pair<String, Executor>>
             mOnPropertyChangedListenerMap = new HashMap<>();
+    private Map<DeviceConfig.OnPropertiesChangedListener, Pair<String, Executor>>
+            mOnPropertiesChangedListenerMap = new HashMap<>();
     private Map<String, String> mKeyValueMap = new ConcurrentHashMap<>();
 
     /**
@@ -77,6 +86,18 @@
         doAnswer((Answer<Void>) invocationOnMock -> {
             String namespace = invocationOnMock.getArgument(0);
             Executor executor = invocationOnMock.getArgument(1);
+            DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener =
+                    invocationOnMock.getArgument(2);
+            mOnPropertiesChangedListenerMap.put(
+                    onPropertiesChangedListener, new Pair<>(namespace, executor));
+            return null;
+        }).when(() -> DeviceConfig.addOnPropertiesChangedListener(
+                anyString(), any(Executor.class),
+                any(DeviceConfig.OnPropertiesChangedListener.class)));
+
+        doAnswer((Answer<Void>) invocationOnMock -> {
+            String namespace = invocationOnMock.getArgument(0);
+            Executor executor = invocationOnMock.getArgument(1);
             DeviceConfig.OnPropertyChangedListener onPropertyChangedListener =
                     invocationOnMock.getArgument(2);
             mOnPropertyChangedListenerMap.put(
@@ -91,6 +112,14 @@
                     String name = invocationOnMock.getArgument(1);
                     String value = invocationOnMock.getArgument(2);
                     mKeyValueMap.put(getKey(namespace, name), value);
+                    for (DeviceConfig.OnPropertiesChangedListener listener :
+                            mOnPropertiesChangedListenerMap.keySet()) {
+                        if (namespace.equals(mOnPropertiesChangedListenerMap.get(listener).first)) {
+                            mOnPropertiesChangedListenerMap.get(listener).second.execute(
+                                    () -> listener.onPropertiesChanged(
+                                            getProperties(namespace, name, value)));
+                        }
+                    }
                     for (DeviceConfig.OnPropertyChangedListener listener :
                             mOnPropertyChangedListenerMap.keySet()) {
                         if (namespace.equals(mOnPropertyChangedListenerMap.get(listener).first)) {
@@ -114,12 +143,14 @@
             protected void succeeded(Description description) {
                 mMockitoSession.finishMocking();
                 mOnPropertyChangedListenerMap.clear();
+                mOnPropertiesChangedListenerMap.clear();
             }
 
             @Override
             protected void failed(Throwable e, Description description) {
                 mMockitoSession.finishMocking(e);
                 mOnPropertyChangedListenerMap.clear();
+                mOnPropertiesChangedListenerMap.clear();
             }
         }.apply(base, description);
     }
@@ -128,4 +159,79 @@
         return namespace + "/" + name;
     }
 
+    private Properties getProperties(String namespace, String name, String value) {
+        Properties properties = Mockito.mock(Properties.class);
+        when(properties.getNamespace()).thenReturn(namespace);
+        when(properties.getKeyset()).thenReturn(Collections.singleton(name));
+        when(properties.getBoolean(anyString(), anyBoolean())).thenAnswer(
+                invocation -> {
+                    String key = invocation.getArgument(0);
+                    boolean defaultValue = invocation.getArgument(1);
+                    if (name.equalsIgnoreCase(key) && value != null) {
+                        return Boolean.parseBoolean(value);
+                    } else {
+                        return defaultValue;
+                    }
+                }
+        );
+        when(properties.getFloat(anyString(), anyFloat())).thenAnswer(
+                invocation -> {
+                    String key = invocation.getArgument(0);
+                    float defaultValue = invocation.getArgument(1);
+                    if (name.equalsIgnoreCase(key) && value != null) {
+                        try {
+                            return Float.parseFloat(value);
+                        } catch (NumberFormatException e) {
+                            return defaultValue;
+                        }
+                    } else {
+                        return defaultValue;
+                    }
+                }
+        );
+        when(properties.getInt(anyString(), anyInt())).thenAnswer(
+                invocation -> {
+                    String key = invocation.getArgument(0);
+                    int defaultValue = invocation.getArgument(1);
+                    if (name.equalsIgnoreCase(key) && value != null) {
+                        try {
+                            return Integer.parseInt(value);
+                        } catch (NumberFormatException e) {
+                            return defaultValue;
+                        }
+                    } else {
+                        return defaultValue;
+                    }
+                }
+        );
+        when(properties.getLong(anyString(), anyLong())).thenAnswer(
+                invocation -> {
+                    String key = invocation.getArgument(0);
+                    long defaultValue = invocation.getArgument(1);
+                    if (name.equalsIgnoreCase(key) && value != null) {
+                        try {
+                            return Long.parseLong(value);
+                        } catch (NumberFormatException e) {
+                            return defaultValue;
+                        }
+                    } else {
+                        return defaultValue;
+                    }
+                }
+        );
+        when(properties.getString(anyString(), anyString())).thenAnswer(
+                invocation -> {
+                    String key = invocation.getArgument(0);
+                    String defaultValue = invocation.getArgument(1);
+                    if (name.equalsIgnoreCase(key) && value != null) {
+                        return value;
+                    } else {
+                        return defaultValue;
+                    }
+                }
+        );
+
+        return properties;
+    }
+
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java
index 39b5840..3eb7209 100644
--- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.testables;
 
-import static android.provider.DeviceConfig.OnPropertyChangedListener;
+import static android.provider.DeviceConfig.OnPropertiesChangedListener;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -93,20 +93,22 @@
     public void testListener() throws InterruptedException {
         CountDownLatch countDownLatch = new CountDownLatch(1);
 
-        OnPropertyChangedListener changeListener = (namespace, name, value) -> {
-            assertThat(namespace).isEqualTo(sNamespace);
-            assertThat(name).isEqualTo(sKey);
-            assertThat(value).isEqualTo(sValue);
+        OnPropertiesChangedListener changeListener = (properties) -> {
+            assertThat(properties.getNamespace()).isEqualTo(sNamespace);
+            assertThat(properties.getKeyset().size()).isEqualTo(1);
+            assertThat(properties.getKeyset()).contains(sKey);
+            assertThat(properties.getString(sKey, "bogus_value")).isEqualTo(sValue);
+            assertThat(properties.getString("bogus_key", "bogus_value")).isEqualTo("bogus_value");
             countDownLatch.countDown();
         };
         try {
-            DeviceConfig.addOnPropertyChangedListener(sNamespace,
+            DeviceConfig.addOnPropertiesChangedListener(sNamespace,
                     ActivityThread.currentApplication().getMainExecutor(), changeListener);
             DeviceConfig.setProperty(sNamespace, sKey, sValue, false);
             assertThat(countDownLatch.await(
                     WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
         } finally {
-            DeviceConfig.removeOnPropertyChangedListener(changeListener);
+            DeviceConfig.removeOnPropertiesChangedListener(changeListener);
         }
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 34bb0a8..cbca087 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -5154,4 +5154,41 @@
         assertEquals(1, notifsAfter.length);
         assertEquals((notifsAfter[0].getNotification().flags & FLAG_BUBBLE), 0);
     }
+
+    @Test
+    public void testNotificationBubbles_disabled_lowRamDevice() throws Exception {
+        // Bubbles are allowed!
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(true);
+        when(mPreferencesHelper.getNotificationChannel(
+                anyString(), anyInt(), anyString(), anyBoolean())).thenReturn(
+                mTestNotificationChannel);
+        when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn(
+                mTestNotificationChannel.getImportance());
+
+        // Plain notification that has bubble metadata
+        NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel,
+                null /* tvExtender */, true /* isBubble */);
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag",
+                nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId());
+        waitForIdle();
+
+        // Would be a normal notification because wouldn't have met requirements to bubble
+        StatusBarNotification[] notifsBefore = mBinderService.getActiveNotifications(PKG);
+        assertEquals(1, notifsBefore.length);
+        assertEquals((notifsBefore[0].getNotification().flags & FLAG_BUBBLE), 0);
+
+        // Make the package foreground so that we're allowed to be a bubble
+        when(mActivityManager.getPackageImportance(nr.sbn.getPackageName())).thenReturn(
+                IMPORTANCE_FOREGROUND);
+
+        // And we are low ram
+        when(mActivityManager.isLowRamDevice()).thenReturn(true);
+
+        // We wouldn't be a bubble because the notification didn't meet requirements (low ram)
+        StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
+        assertEquals(1, notifsAfter.length);
+        assertEquals((notifsAfter[0].getNotification().flags & FLAG_BUBBLE), 0);
+
+    }
 }