Merge "Add KeystoreKeyEventReported atom for keystore logging." into rvc-dev
diff --git a/cmds/statsd/tests/FieldValue_test.cpp b/cmds/statsd/tests/FieldValue_test.cpp
index 23f8ca4..a21eb9b 100644
--- a/cmds/statsd/tests/FieldValue_test.cpp
+++ b/cmds/statsd/tests/FieldValue_test.cpp
@@ -33,6 +33,12 @@
 namespace os {
 namespace statsd {
 
+// These constants must be kept in sync with those in StatsDimensionsValue.java.
+const static int STATS_DIMENSIONS_VALUE_STRING_TYPE = 2;
+const static int STATS_DIMENSIONS_VALUE_INT_TYPE = 3;
+const static int STATS_DIMENSIONS_VALUE_FLOAT_TYPE = 6;
+const static int STATS_DIMENSIONS_VALUE_TUPLE_TYPE = 7;
+
 namespace {
 void makeLogEvent(LogEvent* logEvent, const int32_t atomId, const int64_t timestamp,
                   const vector<int>& attributionUids, const vector<string>& attributionTags,
@@ -291,34 +297,76 @@
     }
 }
 
-//TODO(b/149050405) Update this test for StatsDimensionValueParcel
-//TEST(AtomMatcherTest, TestSubscriberDimensionWrite) {
-//    HashableDimensionKey dim;
-//
-//    int pos1[] = {1, 1, 1};
-//    int pos2[] = {1, 1, 2};
-//    int pos3[] = {1, 1, 3};
-//    int pos4[] = {2, 0, 0};
-//
-//    Field field1(10, pos1, 2);
-//    Field field2(10, pos2, 2);
-//    Field field3(10, pos3, 2);
-//    Field field4(10, pos4, 0);
-//
-//    Value value1((int32_t)10025);
-//    Value value2("tag");
-//    Value value3((int32_t)987654);
-//    Value value4((int32_t)99999);
-//
-//    dim.addValue(FieldValue(field1, value1));
-//    dim.addValue(FieldValue(field2, value2));
-//    dim.addValue(FieldValue(field3, value3));
-//    dim.addValue(FieldValue(field4, value4));
-//
-//    SubscriberReporter::getStatsDimensionsValue(dim);
-//    // TODO(b/110562792): can't test anything here because StatsDimensionsValue class doesn't
-//    // have any read api.
-//}
+void checkAttributionNodeInDimensionsValueParcel(StatsDimensionsValueParcel& attributionNodeParcel,
+                                                 int32_t nodeDepthInAttributionChain,
+                                                 int32_t uid, string tag) {
+    EXPECT_EQ(attributionNodeParcel.field, nodeDepthInAttributionChain /*position at depth 1*/);
+    ASSERT_EQ(attributionNodeParcel.valueType, STATS_DIMENSIONS_VALUE_TUPLE_TYPE);
+    ASSERT_EQ(attributionNodeParcel.tupleValue.size(), 2);
+
+    StatsDimensionsValueParcel uidParcel = attributionNodeParcel.tupleValue[0];
+    EXPECT_EQ(uidParcel.field, 1 /*position at depth 2*/);
+    EXPECT_EQ(uidParcel.valueType, STATS_DIMENSIONS_VALUE_INT_TYPE);
+    EXPECT_EQ(uidParcel.intValue, uid);
+
+    StatsDimensionsValueParcel tagParcel = attributionNodeParcel.tupleValue[1];
+    EXPECT_EQ(tagParcel.field, 2 /*position at depth 2*/);
+    EXPECT_EQ(tagParcel.valueType, STATS_DIMENSIONS_VALUE_STRING_TYPE);
+    EXPECT_EQ(tagParcel.stringValue, tag);
+}
+
+// Test conversion of a HashableDimensionKey into a StatsDimensionValueParcel
+TEST(AtomMatcherTest, TestSubscriberDimensionWrite) {
+    int atomId = 10;
+    // First four fields form an attribution chain
+    int pos1[] = {1, 1, 1};
+    int pos2[] = {1, 1, 2};
+    int pos3[] = {1, 2, 1};
+    int pos4[] = {1, 2, 2};
+    int pos5[] = {2, 1, 1};
+
+    Field field1(atomId, pos1, /*depth=*/2);
+    Field field2(atomId, pos2, /*depth=*/2);
+    Field field3(atomId, pos3, /*depth=*/2);
+    Field field4(atomId, pos4, /*depth=*/2);
+    Field field5(atomId, pos5, /*depth=*/0);
+
+    Value value1((int32_t)1);
+    Value value2("string2");
+    Value value3((int32_t)3);
+    Value value4("string4");
+    Value value5((float)5.0);
+
+    HashableDimensionKey dimensionKey;
+    dimensionKey.addValue(FieldValue(field1, value1));
+    dimensionKey.addValue(FieldValue(field2, value2));
+    dimensionKey.addValue(FieldValue(field3, value3));
+    dimensionKey.addValue(FieldValue(field4, value4));
+    dimensionKey.addValue(FieldValue(field5, value5));
+
+    StatsDimensionsValueParcel rootParcel = dimensionKey.toStatsDimensionsValueParcel();
+    EXPECT_EQ(rootParcel.field, atomId);
+    ASSERT_EQ(rootParcel.valueType, STATS_DIMENSIONS_VALUE_TUPLE_TYPE);
+    ASSERT_EQ(rootParcel.tupleValue.size(), 2);
+
+    // Check that attribution chain is populated correctly
+    StatsDimensionsValueParcel attributionChainParcel = rootParcel.tupleValue[0];
+    EXPECT_EQ(attributionChainParcel.field, 1 /*position at depth 0*/);
+    ASSERT_EQ(attributionChainParcel.valueType, STATS_DIMENSIONS_VALUE_TUPLE_TYPE);
+    ASSERT_EQ(attributionChainParcel.tupleValue.size(), 2);
+    checkAttributionNodeInDimensionsValueParcel(attributionChainParcel.tupleValue[0],
+                                                /*nodeDepthInAttributionChain=*/1,
+                                                value1.int_value, value2.str_value);
+    checkAttributionNodeInDimensionsValueParcel(attributionChainParcel.tupleValue[1],
+                                                /*nodeDepthInAttributionChain=*/2,
+                                                value3.int_value, value4.str_value);
+
+    // Check that the float is populated correctly
+    StatsDimensionsValueParcel floatParcel = rootParcel.tupleValue[1];
+    EXPECT_EQ(floatParcel.field, 2 /*position at depth 0*/);
+    EXPECT_EQ(floatParcel.valueType, STATS_DIMENSIONS_VALUE_FLOAT_TYPE);
+    EXPECT_EQ(floatParcel.floatValue, value5.float_value);
+}
 
 TEST(AtomMatcherTest, TestWriteDimensionToProto) {
     HashableDimensionKey dim;
diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java
index a3fd60e..004f844 100644
--- a/core/java/android/net/NetworkCapabilities.java
+++ b/core/java/android/net/NetworkCapabilities.java
@@ -900,9 +900,17 @@
      * <p>For NetworkCapability instances being sent from ConnectivityService, this value MUST be
      * reset to Process.INVALID_UID unless all the following conditions are met:
      *
+     * <p>The caller is the network owner, AND one of the following sets of requirements is met:
+     *
      * <ol>
-     *   <li>The destination app is the network owner
-     *   <li>The destination app has the ACCESS_FINE_LOCATION permission granted
+     *   <li>The described Network is a VPN
+     * </ol>
+     *
+     * <p>OR:
+     *
+     * <ol>
+     *   <li>The calling app is the network owner
+     *   <li>The calling app has the ACCESS_FINE_LOCATION permission granted
      *   <li>The user's location toggle is on
      * </ol>
      *
@@ -928,7 +936,16 @@
     /**
      * Retrieves the UID of the app that owns this network.
      *
-     * <p>For user privacy reasons, this field will only be populated if:
+     * <p>For user privacy reasons, this field will only be populated if the following conditions
+     * are met:
+     *
+     * <p>The caller is the network owner, AND one of the following sets of requirements is met:
+     *
+     * <ol>
+     *   <li>The described Network is a VPN
+     * </ol>
+     *
+     * <p>OR:
      *
      * <ol>
      *   <li>The calling app is the network owner
@@ -936,8 +953,8 @@
      *   <li>The user's location toggle is on
      * </ol>
      *
-     * Instances of NetworkCapabilities sent to apps without the appropriate permissions will
-     * have this field cleared out.
+     * Instances of NetworkCapabilities sent to apps without the appropriate permissions will have
+     * this field cleared out.
      */
     public int getOwnerUid() {
         return mOwnerUid;
diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java
index e550f85..9876076 100644
--- a/core/java/android/net/RouteInfo.java
+++ b/core/java/android/net/RouteInfo.java
@@ -26,7 +26,6 @@
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.util.Pair;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -554,15 +553,45 @@
     }
 
     /**
-     * A helper class that contains the destination and the gateway in a {@code RouteInfo},
-     * used by {@link ConnectivityService#updateRoutes} or
+     * A helper class that contains the destination, the gateway and the interface in a
+     * {@code RouteInfo}, used by {@link ConnectivityService#updateRoutes} or
      * {@link LinkProperties#addRoute} to calculate the list to be updated.
+     * {@code RouteInfo} objects with different interfaces are treated as different routes because
+     * *usually* on Android different interfaces use different routing tables, and moving a route
+     * to a new routing table never constitutes an update, but is always a remove and an add.
      *
      * @hide
      */
-    public static class RouteKey extends Pair<IpPrefix, InetAddress> {
-        RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway) {
-            super(destination, gateway);
+    public static class RouteKey {
+        @NonNull private final IpPrefix mDestination;
+        @Nullable private final InetAddress mGateway;
+        @Nullable private final String mInterface;
+
+        RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway,
+                @Nullable String iface) {
+            mDestination = destination;
+            mGateway = gateway;
+            mInterface = iface;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof RouteKey)) {
+                return false;
+            }
+            RouteKey p = (RouteKey) o;
+            // No need to do anything special for scoped addresses. Inet6Address#equals does not
+            // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel)
+            // and the kernel ignore scoped addresses both in the prefix and in the nexthop and only
+            // look at RTA_OIF.
+            return Objects.equals(p.mDestination, mDestination)
+                    && Objects.equals(p.mGateway, mGateway)
+                    && Objects.equals(p.mInterface, mInterface);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mDestination, mGateway, mInterface);
         }
     }
 
@@ -574,7 +603,7 @@
      */
     @NonNull
     public RouteKey getRouteKey() {
-        return new RouteKey(mDestination, mGateway);
+        return new RouteKey(mDestination, mGateway, mInterface);
     }
 
     /**
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 0a1e3a0..e0b67da 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -1063,6 +1063,10 @@
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
+        ViewPager viewPager = findViewById(R.id.profile_pager);
+        if (shouldShowTabs() && viewPager.isLayoutRtl()) {
+            mMultiProfilePagerAdapter.setupViewPager(viewPager);
+        }
 
         mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation);
         adjustPreviewWidth(newConfig.orientation, null);
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index daacd45..86c13a0 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -1938,7 +1938,7 @@
                 ResolverListAdapter activeListAdapter =
                         mMultiProfilePagerAdapter.getActiveListAdapter();
                 activeListAdapter.notifyDataSetChanged();
-                if (activeListAdapter.getCount() == 0) {
+                if (activeListAdapter.getCount() == 0 && !inactiveListAdapterHasItems()) {
                     // We no longer have any items...  just finish the activity.
                     finish();
                 }
@@ -1948,6 +1948,13 @@
         }
     }
 
+    private boolean inactiveListAdapterHasItems() {
+        if (!shouldShowTabs()) {
+            return false;
+        }
+        return mMultiProfilePagerAdapter.getInactiveListAdapter().getCount() > 0;
+    }
+
     private BroadcastReceiver createWorkProfileStateReceiver() {
         return new BroadcastReceiver() {
             @Override
diff --git a/core/java/com/android/internal/app/ResolverViewPager.java b/core/java/com/android/internal/app/ResolverViewPager.java
index 9cdfc2f..478cc18 100644
--- a/core/java/com/android/internal/app/ResolverViewPager.java
+++ b/core/java/com/android/internal/app/ResolverViewPager.java
@@ -74,12 +74,16 @@
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     }
 
+    /**
+     * Sets whether swiping sideways should happen.
+     * <p>Note that swiping is always disabled for RTL layouts (b/159110029 for context).
+     */
     void setSwipingEnabled(boolean swipingEnabled) {
         mSwipingEnabled = swipingEnabled;
     }
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        return mSwipingEnabled && super.onInterceptTouchEvent(ev);
+        return !isLayoutRtl() && mSwipingEnabled && super.onInterceptTouchEvent(ev);
     }
 }
diff --git a/core/java/com/android/internal/policy/BackdropFrameRenderer.java b/core/java/com/android/internal/policy/BackdropFrameRenderer.java
index 7bfed91..6fe1d81 100644
--- a/core/java/com/android/internal/policy/BackdropFrameRenderer.java
+++ b/core/java/com/android/internal/policy/BackdropFrameRenderer.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.policy;
 
+import android.graphics.Insets;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
 import android.graphics.RenderNode;
@@ -69,16 +70,14 @@
     private ColorDrawable mNavigationBarColor;
     private boolean mOldFullscreen;
     private boolean mFullscreen;
-    private final Rect mOldSystemInsets = new Rect();
-    private final Rect mOldStableInsets = new Rect();
-    private final Rect mSystemInsets = new Rect();
-    private final Rect mStableInsets = new Rect();
+    private final Rect mOldSystemBarInsets = new Rect();
+    private final Rect mSystemBarInsets = new Rect();
     private final Rect mTmpRect = new Rect();
 
     public BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds,
             Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable,
             Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor,
-            boolean fullscreen, Rect systemInsets, Rect stableInsets) {
+            boolean fullscreen, Insets systemBarInsets) {
         setName("ResizeFrame");
 
         mRenderer = renderer;
@@ -95,10 +94,8 @@
         mTargetRect.set(initialBounds);
         mFullscreen = fullscreen;
         mOldFullscreen = fullscreen;
-        mSystemInsets.set(systemInsets);
-        mStableInsets.set(stableInsets);
-        mOldSystemInsets.set(systemInsets);
-        mOldStableInsets.set(stableInsets);
+        mSystemBarInsets.set(systemBarInsets.toRect());
+        mOldSystemBarInsets.set(systemBarInsets.toRect());
 
         // Kick off our draw thread.
         start();
@@ -154,16 +151,13 @@
      *
      * @param newTargetBounds The new target bounds.
      * @param fullscreen Whether the window is currently drawing in fullscreen.
-     * @param systemInsets The current visible system insets for the window.
-     * @param stableInsets The stable insets for the window.
+     * @param systemBarInsets The current visible system insets for the window.
      */
-    public void setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemInsets,
-            Rect stableInsets) {
+    public void setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemBarInsets) {
         synchronized (this) {
             mFullscreen = fullscreen;
             mTargetRect.set(newTargetBounds);
-            mSystemInsets.set(systemInsets);
-            mStableInsets.set(stableInsets);
+            mSystemBarInsets.set(systemBarInsets);
             // Notify of a bounds change.
             pingRenderLocked(false /* drawImmediate */);
         }
@@ -247,14 +241,12 @@
         mNewTargetRect.set(mTargetRect);
         if (!mNewTargetRect.equals(mOldTargetRect)
                 || mOldFullscreen != mFullscreen
-                || !mStableInsets.equals(mOldStableInsets)
-                || !mSystemInsets.equals(mOldSystemInsets)
+                || !mSystemBarInsets.equals(mOldSystemBarInsets)
                 || mReportNextDraw) {
             mOldFullscreen = mFullscreen;
             mOldTargetRect.set(mNewTargetRect);
-            mOldSystemInsets.set(mSystemInsets);
-            mOldStableInsets.set(mStableInsets);
-            redrawLocked(mNewTargetRect, mFullscreen, mSystemInsets, mStableInsets);
+            mOldSystemBarInsets.set(mSystemBarInsets);
+            redrawLocked(mNewTargetRect, mFullscreen);
         }
     }
 
@@ -304,11 +296,8 @@
      *
      * @param newBounds The window bounds which needs to be drawn.
      * @param fullscreen Whether the window is currently drawing in fullscreen.
-     * @param systemInsets The current visible system insets for the window.
-     * @param stableInsets The stable insets for the window.
      */
-    private void redrawLocked(Rect newBounds, boolean fullscreen, Rect systemInsets,
-            Rect stableInsets) {
+    private void redrawLocked(Rect newBounds, boolean fullscreen) {
 
         // While a configuration change is taking place the view hierarchy might become
         // inaccessible. For that case we remember the previous metrics to avoid flashes.
@@ -355,7 +344,7 @@
         }
         mFrameAndBackdropNode.endRecording();
 
-        drawColorViews(left, top, width, height, fullscreen, systemInsets, stableInsets);
+        drawColorViews(left, top, width, height, fullscreen);
 
         // We need to render the node explicitly
         mRenderer.drawRenderNode(mFrameAndBackdropNode);
@@ -363,14 +352,13 @@
         reportDrawIfNeeded();
     }
 
-    private void drawColorViews(int left, int top, int width, int height,
-            boolean fullscreen, Rect systemInsets, Rect stableInsets) {
+    private void drawColorViews(int left, int top, int width, int height, boolean fullscreen) {
         if (mSystemBarBackgroundNode == null) {
             return;
         }
         RecordingCanvas canvas = mSystemBarBackgroundNode.beginRecording(width, height);
         mSystemBarBackgroundNode.setLeftTopRightBottom(left, top, left + width, top + height);
-        final int topInset = DecorView.getColorViewTopInset(mStableInsets.top, mSystemInsets.top);
+        final int topInset = mSystemBarInsets.top;
         if (mStatusBarColor != null) {
             mStatusBarColor.setBounds(0, 0, left + width, topInset);
             mStatusBarColor.draw(canvas);
@@ -380,7 +368,7 @@
         // don't want the navigation bar background be moving around when resizing in docked mode.
         // However, we need it for the transitions into/out of docked mode.
         if (mNavigationBarColor != null && fullscreen) {
-            DecorView.getNavigationBarRect(width, height, stableInsets, systemInsets, mTmpRect, 1f);
+            DecorView.getNavigationBarRect(width, height, mSystemBarInsets, mTmpRect, 1f);
             mNavigationBarColor.setBounds(mTmpRect);
             mNavigationBarColor.draw(canvas);
         }
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index c6135f2..b12c5e9 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -1050,22 +1050,6 @@
         return false;
     }
 
-    public static int getColorViewTopInset(int stableTop, int systemTop) {
-        return Math.min(stableTop, systemTop);
-    }
-
-    public static int getColorViewBottomInset(int stableBottom, int systemBottom) {
-        return Math.min(stableBottom, systemBottom);
-    }
-
-    public static int getColorViewRightInset(int stableRight, int systemRight) {
-        return Math.min(stableRight, systemRight);
-    }
-
-    public static int getColorViewLeftInset(int stableLeft, int systemLeft) {
-        return Math.min(stableLeft, systemLeft);
-    }
-
     public static boolean isNavBarToRightEdge(int bottomInset, int rightInset) {
         return bottomInset == 0 && rightInset > 0;
     }
@@ -1079,14 +1063,11 @@
                 : isNavBarToLeftEdge(bottomInset, leftInset) ? leftInset : bottomInset;
     }
 
-    public static void getNavigationBarRect(int canvasWidth, int canvasHeight, Rect stableInsets,
-            Rect contentInsets, Rect outRect, float scale) {
-        final int bottomInset =
-                (int) (getColorViewBottomInset(stableInsets.bottom, contentInsets.bottom) * scale);
-        final int leftInset =
-                (int) (getColorViewLeftInset(stableInsets.left, contentInsets.left) * scale);
-        final int rightInset =
-                (int) (getColorViewLeftInset(stableInsets.right, contentInsets.right) * scale);
+    public static void getNavigationBarRect(int canvasWidth, int canvasHeight, Rect systemBarInsets,
+            Rect outRect, float scale) {
+        final int bottomInset = (int) (systemBarInsets.bottom * scale);
+        final int leftInset = (int) (systemBarInsets.left * scale);
+        final int rightInset = (int) (systemBarInsets.right * scale);
         final int size = getNavBarSize(bottomInset, rightInset, leftInset);
         if (isNavBarToRightEdge(bottomInset, rightInset)) {
             outRect.set(canvasWidth - size, 0, canvasWidth, canvasHeight);
@@ -1113,31 +1094,30 @@
             mLastWindowFlags = attrs.flags;
 
             if (insets != null) {
-                mLastTopInset = getColorViewTopInset(insets.getStableInsetTop(),
-                        insets.getSystemWindowInsetTop());
-                mLastBottomInset = getColorViewBottomInset(insets.getStableInsetBottom(),
-                        insets.getSystemWindowInsetBottom());
-                mLastRightInset = getColorViewRightInset(insets.getStableInsetRight(),
-                        insets.getSystemWindowInsetRight());
-                mLastLeftInset = getColorViewRightInset(insets.getStableInsetLeft(),
-                        insets.getSystemWindowInsetLeft());
+                final Insets systemBarInsets = insets.getInsets(WindowInsets.Type.systemBars());
+                final Insets stableBarInsets = insets.getInsetsIgnoringVisibility(
+                        WindowInsets.Type.systemBars());
+                mLastTopInset = systemBarInsets.top;
+                mLastBottomInset = systemBarInsets.bottom;
+                mLastRightInset = systemBarInsets.right;
+                mLastLeftInset = systemBarInsets.left;
 
                 // Don't animate if the presence of stable insets has changed, because that
                 // indicates that the window was either just added and received them for the
                 // first time, or the window size or position has changed.
-                boolean hasTopStableInset = insets.getStableInsetTop() != 0;
+                boolean hasTopStableInset = stableBarInsets.top != 0;
                 disallowAnimate |= (hasTopStableInset != mLastHasTopStableInset);
                 mLastHasTopStableInset = hasTopStableInset;
 
-                boolean hasBottomStableInset = insets.getStableInsetBottom() != 0;
+                boolean hasBottomStableInset = stableBarInsets.bottom != 0;
                 disallowAnimate |= (hasBottomStableInset != mLastHasBottomStableInset);
                 mLastHasBottomStableInset = hasBottomStableInset;
 
-                boolean hasRightStableInset = insets.getStableInsetRight() != 0;
+                boolean hasRightStableInset = stableBarInsets.right != 0;
                 disallowAnimate |= (hasRightStableInset != mLastHasRightStableInset);
                 mLastHasRightStableInset = hasRightStableInset;
 
-                boolean hasLeftStableInset = insets.getStableInsetLeft() != 0;
+                boolean hasLeftStableInset = stableBarInsets.left != 0;
                 disallowAnimate |= (hasLeftStableInset != mLastHasLeftStableInset);
                 mLastHasLeftStableInset = hasLeftStableInset;
 
@@ -2296,7 +2276,7 @@
     public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
             Rect stableInsets) {
         if (mBackdropFrameRenderer != null) {
-            mBackdropFrameRenderer.setTargetRect(newBounds, fullscreen, systemInsets, stableInsets);
+            mBackdropFrameRenderer.setTargetRect(newBounds, fullscreen, systemInsets);
         }
     }
 
@@ -2314,11 +2294,12 @@
         final ThreadedRenderer renderer = getThreadedRenderer();
         if (renderer != null) {
             loadBackgroundDrawablesIfNeeded();
+            WindowInsets rootInsets = getRootWindowInsets();
             mBackdropFrameRenderer = new BackdropFrameRenderer(this, renderer,
                     initialBounds, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                     mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
-                    getCurrentColor(mNavigationColorViewState), fullscreen, systemInsets,
-                    stableInsets);
+                    getCurrentColor(mNavigationColorViewState), fullscreen,
+                    rootInsets.getInsets(WindowInsets.Type.systemBars()));
 
             // Get rid of the shadow while we are resizing. Shadow drawing takes considerable time.
             // If we want to get the shadow shown while resizing, we would need to elevate a new
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index b64923f..5d4407b 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -233,13 +233,20 @@
             oldVisibility = mImportanceRingView.getVisibility();
             wasGone = oldVisibility == GONE;
             visibility = !mImportantConversation ? GONE : visibility;
-            isGone = visibility == GONE;
-            if (wasGone != isGone) {
+            boolean isRingGone = visibility == GONE;
+            if (wasGone != isRingGone) {
                 // Keep the badge visibility in sync with the icon. This is necessary in cases
                 // Where the icon is being hidden externally like in group children.
                 mImportanceRingView.animate().cancel();
                 mImportanceRingView.setVisibility(visibility);
             }
+
+            oldVisibility = mConversationIconBadge.getVisibility();
+            wasGone = oldVisibility == GONE;
+            if (wasGone != isGone) {
+                mConversationIconBadge.animate().cancel();
+                mConversationIconBadge.setVisibility(visibility);
+            }
         });
         // When the small icon is gone, hide the rest of the badge
         mIcon.setOnForceHiddenChangedListener((forceHidden) -> {
diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java
index 53272f7..6d940b8 100644
--- a/core/java/com/android/internal/widget/MessagingGroup.java
+++ b/core/java/com/android/internal/widget/MessagingGroup.java
@@ -42,6 +42,7 @@
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
 import android.widget.RemoteViews;
+import android.widget.TextView;
 
 import com.android.internal.R;
 
@@ -612,7 +613,7 @@
         return 0;
     }
 
-    public View getSenderView() {
+    public TextView getSenderView() {
         return mSenderView;
     }
 
@@ -668,6 +669,7 @@
                     singleLine ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
             MarginLayoutParams layoutParams = (MarginLayoutParams) mSenderView.getLayoutParams();
             layoutParams.setMarginEnd(singleLine ? mSenderTextPaddingSingleLine : 0);
+            mSenderView.setSingleLine(singleLine);
             updateMaxDisplayedLines();
             updateClipRect();
             updateSenderVisibility();
diff --git a/core/proto/android/stats/launcher/launcher.proto b/core/proto/android/stats/launcher/launcher.proto
index dbd0e03..fc177d5 100644
--- a/core/proto/android/stats/launcher/launcher.proto
+++ b/core/proto/android/stats/launcher/launcher.proto
@@ -32,10 +32,12 @@
 }
 
 enum LauncherState {
-    BACKGROUND = 0;
-    HOME = 1;
-    OVERVIEW = 2;
-    ALLAPPS = 3;
+    LAUNCHER_STATE_UNSPECIFIED = 0;
+    BACKGROUND = 1;
+    HOME = 2;
+    OVERVIEW = 3;
+    ALLAPPS = 4;
+    UNCHANGED = 5;
 }
 
 message LauncherTarget {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java
index 5ee4693..e0532c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java
@@ -17,9 +17,11 @@
 package com.android.systemui.statusbar.notification;
 
 import android.content.res.Resources;
+import android.text.Layout;
 import android.util.Pools;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.TextView;
 
 import com.android.internal.widget.IMessagingLayout;
 import com.android.internal.widget.MessagingGroup;
@@ -229,6 +231,15 @@
         return result;
     }
 
+    private boolean hasEllipses(TextView textView) {
+        Layout layout = textView.getLayout();
+        return layout != null && layout.getEllipsisCount(layout.getLineCount() - 1) > 0;
+    }
+
+    private boolean needsReflow(TextView own, TextView other) {
+        return hasEllipses(own) != hasEllipses(other);
+    }
+
     /**
      * Transform two groups towards each other.
      *
@@ -238,13 +249,20 @@
             float transformationAmount, boolean to) {
         boolean useLinearTransformation =
                 otherGroup.getIsolatedMessage() == null && !mTransformInfo.isAnimating();
-        transformView(transformationAmount, to, ownGroup.getSenderView(), otherGroup.getSenderView(),
-                true /* sameAsAny */, useLinearTransformation);
+        TextView ownSenderView = ownGroup.getSenderView();
+        TextView otherSenderView = otherGroup.getSenderView();
+        transformView(transformationAmount, to, ownSenderView, otherSenderView,
+                // Normally this would be handled by the TextViewMessageState#sameAs check, but in
+                // this case it doesn't work because our text won't match, due to the appended colon
+                // in the collapsed view.
+                !needsReflow(ownSenderView, otherSenderView),
+                useLinearTransformation);
         int totalAvatarTranslation = transformView(transformationAmount, to, ownGroup.getAvatar(),
                 otherGroup.getAvatar(), true /* sameAsAny */, useLinearTransformation);
         List<MessagingMessage> ownMessages = ownGroup.getMessages();
         List<MessagingMessage> otherMessages = otherGroup.getMessages();
         float previousTranslation = 0;
+        boolean isLastView = true;
         for (int i = 0; i < ownMessages.size(); i++) {
             View child = ownMessages.get(ownMessages.size() - 1 - i).getView();
             if (isGone(child)) {
@@ -278,6 +296,9 @@
                 mMessagingLayout.setMessagingClippingDisabled(true);
             }
             if (otherChild == null) {
+                if (isLastView) {
+                    previousTranslation = ownSenderView.getTranslationY();
+                }
                 child.setTranslationY(previousTranslation);
                 setClippingDeactivated(child, true);
             } else if (ownGroup.getIsolatedMessage() == child || otherIsIsolated) {
@@ -287,6 +308,7 @@
             } else {
                 previousTranslation = child.getTranslationY();
             }
+            isLastView = false;
         }
         ownGroup.updateClipRect();
         return totalAvatarTranslation;
@@ -382,6 +404,9 @@
         if (view.getParent() == null) {
             return true;
         }
+        if (view.getWidth() == 0) {
+            return true;
+        }
         final ViewGroup.LayoutParams lp = view.getLayoutParams();
         if (lp instanceof MessagingLinearLayout.LayoutParams
                 && ((MessagingLinearLayout.LayoutParams) lp).hide) {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 158ed8c..712b413 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -3279,16 +3279,19 @@
                 };
 
         // When the inline suggestion render service is available and the view is focused, there
-        // are 2 cases when augmented autofill should ask IME for inline suggestion request,
+        // are 3 cases when augmented autofill should ask IME for inline suggestion request,
         // because standard autofill flow didn't:
         // 1. the field is augmented autofill only (when standard autofill provider is None or
         // when it returns null response)
         // 2. standard autofill provider doesn't support inline suggestion
+        // 3. we re-entered the autofill session and standard autofill was not re-triggered, this is
+        //    recognized by seeing mExpiredResponse == true
         final RemoteInlineSuggestionRenderService remoteRenderService =
                 mService.getRemoteInlineSuggestionRenderServiceLocked();
         if (remoteRenderService != null
                 && (mForAugmentedAutofillOnly
-                || !isInlineSuggestionsEnabledByAutofillProviderLocked())
+                || !isInlineSuggestionsEnabledByAutofillProviderLocked()
+                || mExpiredResponse)
                 && isViewFocusedLocked(flags)) {
             if (sDebug) Slog.d(TAG, "Create inline request for augmented autofill");
             remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback(
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 2958fd2..36ba610 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -1698,6 +1698,12 @@
             return newNc;
         }
 
+        // Allow VPNs to see ownership of their own VPN networks - not location sensitive.
+        if (nc.hasTransport(TRANSPORT_VPN)) {
+            // Owner UIDs already checked above. No need to re-check.
+            return newNc;
+        }
+
         Binder.withCleanCallingIdentity(
                 () -> {
                     if (!mLocationPermissionChecker.checkLocationPermission(
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index e654af7..1f85d10 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -1106,7 +1106,8 @@
         NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
         networkAgentConfig.allowBypass = mConfig.allowBypass && !mLockdown;
 
-        mNetworkCapabilities.setOwnerUid(Binder.getCallingUid());
+        mNetworkCapabilities.setOwnerUid(mOwnerUID);
+        mNetworkCapabilities.setAdministratorUids(new int[] {mOwnerUID});
         mNetworkCapabilities.setUids(createUserAndRestrictedProfilesRanges(mUserHandle,
                 mConfig.allowedApplications, mConfig.disallowedApplications));
         long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/notification/NotificationChannelLogger.java b/services/core/java/com/android/server/notification/NotificationChannelLogger.java
index a7b1877..5c127c3 100644
--- a/services/core/java/com/android/server/notification/NotificationChannelLogger.java
+++ b/services/core/java/com/android/server/notification/NotificationChannelLogger.java
@@ -99,6 +99,16 @@
     }
 
     /**
+     * Log blocking or unblocking of the entire app's notifications.
+     * @param uid UID of the app.
+     * @param pkg Package name of the app.
+     * @param enabled If true, notifications are now allowed.
+     */
+    default void logAppNotificationsAllowed(int uid, String pkg, boolean enabled) {
+        logAppEvent(NotificationChannelEvent.getBlocked(enabled), uid, pkg);
+    }
+
+    /**
      * Low-level interface for logging events, to be implemented.
      * @param event Event to log.
      * @param channel Notification channel.
@@ -124,6 +134,13 @@
             boolean wasBlocked);
 
     /**
+     * Low-level interface for logging app-as-a-whole events, to be implemented.
+     * @param uid UID of app.
+     * @param pkg Package of app.
+     */
+    void logAppEvent(@NonNull NotificationChannelEvent event, int uid, String pkg);
+
+    /**
      * The UiEvent enums that this class can log.
      */
     enum NotificationChannelEvent implements UiEventLogger.UiEventEnum {
@@ -144,8 +161,11 @@
         @UiEvent(doc = "System created a new conversation (sub-channel in a notification channel)")
         NOTIFICATION_CHANNEL_CONVERSATION_CREATED(272),
         @UiEvent(doc = "System deleted a new conversation (sub-channel in a notification channel)")
-        NOTIFICATION_CHANNEL_CONVERSATION_DELETED(274);
-
+        NOTIFICATION_CHANNEL_CONVERSATION_DELETED(274),
+        @UiEvent(doc = "All notifications for the app were blocked.")
+        APP_NOTIFICATIONS_BLOCKED(557),
+        @UiEvent(doc = "Notifications for the app as a whole were unblocked.")
+        APP_NOTIFICATIONS_UNBLOCKED(558);
 
         private final int mId;
         NotificationChannelEvent(int id) {
@@ -178,6 +198,10 @@
                     ? NotificationChannelEvent.NOTIFICATION_CHANNEL_GROUP_CREATED
                     : NotificationChannelEvent.NOTIFICATION_CHANNEL_GROUP_DELETED;
         }
+
+        public static NotificationChannelEvent getBlocked(boolean enabled) {
+            return enabled ? APP_NOTIFICATIONS_UNBLOCKED : APP_NOTIFICATIONS_BLOCKED;
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/notification/NotificationChannelLoggerImpl.java b/services/core/java/com/android/server/notification/NotificationChannelLoggerImpl.java
index 2f7772e..fd3dd56 100644
--- a/services/core/java/com/android/server/notification/NotificationChannelLoggerImpl.java
+++ b/services/core/java/com/android/server/notification/NotificationChannelLoggerImpl.java
@@ -19,6 +19,8 @@
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
 
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLoggerImpl;
 import com.android.internal.util.FrameworkStatsLog;
 
 /**
@@ -27,6 +29,8 @@
  * should live in the interface so it can be tested.
  */
 public class NotificationChannelLoggerImpl implements NotificationChannelLogger {
+    UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
+
     @Override
     public void logNotificationChannel(NotificationChannelEvent event,
             NotificationChannel channel, int uid, String pkg,
@@ -51,4 +55,9 @@
                 /* int old_importance*/ NotificationChannelLogger.getImportance(wasBlocked),
                 /* int importance*/ NotificationChannelLogger.getImportance(channelGroup));
     }
+
+    @Override
+    public void logAppEvent(NotificationChannelEvent event, int uid, String pkg) {
+        mUiEventLogger.log(event, uid, pkg);
+    }
 }
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index e472e30..afc7557 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -1654,6 +1654,7 @@
         }
         setImportance(packageName, uid,
                 enabled ? DEFAULT_IMPORTANCE : IMPORTANCE_NONE);
+        mNotificationChannelLogger.logAppNotificationsAllowed(uid, packageName, enabled);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index d558839..c24c1e4 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -32,6 +32,7 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
 import static android.os.Build.VERSION_CODES.N;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.util.DisplayMetrics.DENSITY_DEFAULT;
@@ -1526,12 +1527,12 @@
     }
 
     /**
-     * Sets the provided record to {@link mFixedRotationLaunchingApp} if possible to apply fixed
+     * Sets the provided record to {@link #mFixedRotationLaunchingApp} if possible to apply fixed
      * rotation transform to it and indicate that the display may be rotated after it is launched.
      */
     void setFixedRotationLaunchingApp(@NonNull ActivityRecord r, @Surface.Rotation int rotation) {
         final WindowToken prevRotatedLaunchingApp = mFixedRotationLaunchingApp;
-        if (prevRotatedLaunchingApp != null && prevRotatedLaunchingApp == r
+        if (prevRotatedLaunchingApp == r
                 && r.getWindowConfiguration().getRotation() == rotation) {
             // The given launching app and target rotation are the same as the existing ones.
             return;
@@ -5659,6 +5660,16 @@
             }
         }
 
+        /**
+         * Return {@code true} if there is an ongoing animation to the "Recents" activity and this
+         * activity as a fixed orientation so shouldn't be rotated.
+         */
+        boolean isFixedOrientationRecentsAnimating() {
+            return mAnimatingRecents != null
+                    && mAnimatingRecents.getRequestedConfigurationOrientation()
+                    != ORIENTATION_UNDEFINED;
+        }
+
         @Override
         public void onAppTransitionFinishedLocked(IBinder token) {
             final ActivityRecord r = getActivityRecord(token);
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 831491d..f093fd3 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -430,6 +430,15 @@
                         "Deferring rotation, still finishing previous rotation");
                 return false;
             }
+
+            if (mDisplayContent.mFixedRotationTransitionListener
+                    .isFixedOrientationRecentsAnimating()) {
+                // During the recents animation, the closing app might still be considered on top.
+                // In order to ignore its requested orientation to avoid a sensor led rotation (e.g
+                // user rotating the device while the recents animation is running), we ignore
+                // rotation update while the animation is running.
+                return false;
+            }
         }
 
         if (!mService.mDisplayEnabled) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index c749125..6670dbf 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2927,9 +2927,17 @@
         // Don't crop HOME/RECENTS windows to stack bounds. This is because in split-screen
         // they extend past their stack and sysui uses the stack surface to control cropping.
         // TODO(b/158242495): get rid of this when drag/drop can use surface bounds.
-        final boolean isTopHomeOrRecents = (isActivityTypeHome() || isActivityTypeRecents())
-                && getRootTask().getTopMostTask() == this;
-        return isResizeable() && !isTopHomeOrRecents;
+        if (isActivityTypeHome() || isActivityTypeRecents()) {
+            // Make sure this is the top-most non-organizer root task (if not top-most, it means
+            // another translucent task could be above this, so this needs to stay cropped.
+            final Task rootTask = getRootTask();
+            final Task topNonOrgTask =
+                    rootTask.mCreatedByOrganizer ? rootTask.getTopMostTask() : rootTask;
+            if (isDescendantOf(topNonOrgTask)) {
+                return false;
+            }
+        }
+        return isResizeable();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 1a2672b..51cf858 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -28,6 +28,7 @@
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
 import android.graphics.GraphicBuffer;
+import android.graphics.Insets;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.RecordingCanvas;
@@ -37,8 +38,11 @@
 import android.os.Handler;
 import android.util.ArraySet;
 import android.util.Slog;
+import android.view.InsetsSource;
+import android.view.InsetsState;
 import android.view.SurfaceControl;
 import android.view.ThreadedRenderer;
+import android.view.WindowInsets;
 import android.view.WindowManager.LayoutParams;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -475,9 +479,12 @@
         final int color = ColorUtils.setAlphaComponent(
                 task.getTaskDescription().getBackgroundColor(), 255);
         final LayoutParams attrs = mainWindow.getAttrs();
+        final InsetsPolicy insetsPolicy = mainWindow.getDisplayContent().getInsetsPolicy();
+        final InsetsState insetsState = insetsPolicy.getInsetsForDispatch(mainWindow);
+        final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrameLw(), insetsState);
         final SystemBarBackgroundPainter decorPainter = new SystemBarBackgroundPainter(attrs.flags,
                 attrs.privateFlags, attrs.systemUiVisibility, task.getTaskDescription(),
-                mHighResTaskSnapshotScale, mainWindow.getRequestedInsetsState());
+                mHighResTaskSnapshotScale, insetsState);
         final int taskWidth = task.getBounds().width();
         final int taskHeight = task.getBounds().height();
         final int width = (int) (taskWidth * mHighResTaskSnapshotScale);
@@ -488,7 +495,7 @@
         node.setClipToBounds(false);
         final RecordingCanvas c = node.start(width, height);
         c.drawColor(color);
-        decorPainter.setInsets(mainWindow.getContentInsets(), mainWindow.getStableInsets());
+        decorPainter.setInsets(systemBarInsets);
         decorPainter.drawDecors(c, null /* statusBarExcludeFrame */);
         node.end(c);
         final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height);
@@ -593,6 +600,13 @@
         return 0;
     }
 
+    static Rect getSystemBarInsets(Rect frame, InsetsState state) {
+        return state.calculateInsets(frame, null /* ignoringVisibilityState */,
+                false /* isScreenRound */, false /* alwaysConsumeSystemBars */,
+                null /* displayCutout */, 0 /* legacySoftInputMode */, 0 /* legacySystemUiFlags */,
+                null /* typeSideMap */).getInsets(WindowInsets.Type.systemBars()).toRect();
+    }
+
     void dump(PrintWriter pw, String prefix) {
         pw.println(prefix + "mHighResTaskSnapshotScale=" + mHighResTaskSnapshotScale);
         mCache.dump(pw, prefix);
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotSurface.java b/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
index e26f1e1..f1f5762 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
@@ -39,10 +39,9 @@
 
 import static com.android.internal.policy.DecorView.NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES;
 import static com.android.internal.policy.DecorView.STATUS_BAR_COLOR_VIEW_ATTRIBUTES;
-import static com.android.internal.policy.DecorView.getColorViewLeftInset;
-import static com.android.internal.policy.DecorView.getColorViewTopInset;
 import static com.android.internal.policy.DecorView.getNavigationBarRect;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW;
+import static com.android.server.wm.TaskSnapshotController.getSystemBarInsets;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
@@ -131,9 +130,8 @@
     private final IWindowSession mSession;
     private final WindowManagerService mService;
     private final Rect mTaskBounds;
-    private final Rect mStableInsets = new Rect();
-    private final Rect mContentInsets = new Rect();
     private final Rect mFrame = new Rect();
+    private final Rect mSystemBarInsets = new Rect();
     private TaskSnapshot mSnapshot;
     private final RectF mTmpSnapshotSize = new RectF();
     private final RectF mTmpDstFrame = new RectF();
@@ -174,6 +172,7 @@
         final int windowFlags;
         final int windowPrivateFlags;
         final int currentOrientation;
+        final InsetsState insetsState;
         synchronized (service.mGlobalLock) {
             final WindowState mainWindow = activity.findMainWindow();
             final Task task = activity.getTask();
@@ -241,6 +240,10 @@
             taskBounds = new Rect();
             task.getBounds(taskBounds);
             currentOrientation = topFullscreenOpaqueWindow.getConfiguration().orientation;
+
+            final InsetsPolicy insetsPolicy = topFullscreenOpaqueWindow.getDisplayContent()
+                    .getInsetsPolicy();
+            insetsState = insetsPolicy.getInsetsForDispatch(topFullscreenOpaqueWindow);
         }
         try {
             final int res = session.addToDisplay(window, window.mSeq, layoutParams,
@@ -255,8 +258,7 @@
         }
         final TaskSnapshotSurface snapshotSurface = new TaskSnapshotSurface(service, window,
                 surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, sysUiVis,
-                windowFlags, windowPrivateFlags, taskBounds,
-                currentOrientation, topFullscreenOpaqueWindow.getRequestedInsetsState());
+                windowFlags, windowPrivateFlags, taskBounds, currentOrientation, insetsState);
         window.setOuter(snapshotSurface);
         try {
             session.relayout(window, window.mSeq, layoutParams, -1, -1, View.VISIBLE, 0, -1,
@@ -266,7 +268,9 @@
         } catch (RemoteException e) {
             // Local call.
         }
-        snapshotSurface.setFrames(tmpFrame, tmpContentInsets, tmpStableInsets);
+
+        final Rect systemBarInsets = getSystemBarInsets(tmpFrame, insetsState);
+        snapshotSurface.setFrames(tmpFrame, systemBarInsets);
         snapshotSurface.drawSnapshot();
         return snapshotSurface;
     }
@@ -315,13 +319,12 @@
     }
 
     @VisibleForTesting
-    void setFrames(Rect frame, Rect contentInsets, Rect stableInsets) {
+    void setFrames(Rect frame, Rect systemBarInsets) {
         mFrame.set(frame);
-        mContentInsets.set(contentInsets);
-        mStableInsets.set(stableInsets);
+        mSystemBarInsets.set(systemBarInsets);
         mSizeMismatch = (mFrame.width() != mSnapshot.getSnapshot().getWidth()
                 || mFrame.height() != mSnapshot.getSnapshot().getHeight());
-        mSystemBarBackgroundPainter.setInsets(contentInsets, stableInsets);
+        mSystemBarBackgroundPainter.setInsets(systemBarInsets);
     }
 
     private void drawSnapshot() {
@@ -453,9 +456,7 @@
         );
 
         // However, we also need to make space for the navigation bar on the left side.
-        final int colorViewLeftInset = getColorViewLeftInset(mStableInsets.left,
-                mContentInsets.left);
-        frame.offset(colorViewLeftInset, 0);
+        frame.offset(mSystemBarInsets.left, 0);
         return frame;
     }
 
@@ -540,8 +541,6 @@
      */
     static class SystemBarBackgroundPainter {
 
-        private final Rect mContentInsets = new Rect();
-        private final Rect mStableInsets = new Rect();
         private final Paint mStatusBarPaint = new Paint();
         private final Paint mNavigationBarPaint = new Paint();
         private final int mStatusBarColor;
@@ -551,6 +550,7 @@
         private final int mSysUiVis;
         private final float mScale;
         private final InsetsState mInsetsState;
+        private final Rect mSystemBarInsets = new Rect();
 
         SystemBarBackgroundPainter(int windowFlags, int windowPrivateFlags, int sysUiVis,
                 TaskDescription taskDescription, float scale, InsetsState insetsState) {
@@ -576,9 +576,8 @@
             mInsetsState = insetsState;
         }
 
-        void setInsets(Rect contentInsets, Rect stableInsets) {
-            mContentInsets.set(contentInsets);
-            mStableInsets.set(stableInsets);
+        void setInsets(Rect systemBarInsets) {
+            mSystemBarInsets.set(systemBarInsets);
         }
 
         int getStatusBarColorViewHeight() {
@@ -589,7 +588,7 @@
                             mSysUiVis, mStatusBarColor, mWindowFlags, forceBarBackground)
                     : STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible(
                             mInsetsState, mStatusBarColor, mWindowFlags, forceBarBackground)) {
-                return (int) (getColorViewTopInset(mStableInsets.top, mContentInsets.top) * mScale);
+                return (int) (mSystemBarInsets.top * mScale);
             } else {
                 return 0;
             }
@@ -615,8 +614,7 @@
                 int statusBarHeight) {
             if (statusBarHeight > 0 && Color.alpha(mStatusBarColor) != 0
                     && (alreadyDrawnFrame == null || c.getWidth() > alreadyDrawnFrame.right)) {
-                final int rightInset = (int) (DecorView.getColorViewRightInset(mStableInsets.right,
-                        mContentInsets.right) * mScale);
+                final int rightInset = (int) (mSystemBarInsets.right * mScale);
                 final int left = alreadyDrawnFrame != null ? alreadyDrawnFrame.right : 0;
                 c.drawRect(left, 0, c.getWidth() - rightInset, statusBarHeight, mStatusBarPaint);
             }
@@ -625,8 +623,8 @@
         @VisibleForTesting
         void drawNavigationBarBackground(Canvas c) {
             final Rect navigationBarRect = new Rect();
-            getNavigationBarRect(c.getWidth(), c.getHeight(), mStableInsets, mContentInsets,
-                    navigationBarRect, mScale);
+            getNavigationBarRect(c.getWidth(), c.getHeight(), mSystemBarInsets, navigationBarRect,
+                    mScale);
             final boolean visible = isNavigationBarColorViewVisible();
             if (visible && Color.alpha(mNavigationBarColor) != 0 && !navigationBarRect.isEmpty()) {
                 c.drawRect(navigationBarRect, mNavigationBarPaint);
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 6406f0ae..dd08f42 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1032,7 +1032,8 @@
      * @return Whether this child is on top of the window hierarchy.
      */
     boolean isOnTop() {
-        return getParent().getTopChild() == this && getParent().isOnTop();
+        final WindowContainer parent = getParent();
+        return parent != null && parent.getTopChild() == this && parent.isOnTop();
     }
 
     /** Returns the top child container. */
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 8fb9862..5fc519c 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -2339,6 +2339,10 @@
             return false;
         }
 
+        if (inPinnedWindowingMode()) {
+            return false;
+        }
+
         final boolean windowsAreFocusable = mActivityRecord == null || mActivityRecord.windowsAreFocusable();
         if (!windowsAreFocusable) {
             // This window can't be an IME target if the app's windows should not be focusable.
@@ -3412,6 +3416,7 @@
     private void setTouchableRegionCropIfNeeded(InputWindowHandle handle) {
         final Task task = getTask();
         if (task == null || !task.cropWindowsToStackBounds()) {
+            handle.setTouchableRegionCrop(null);
             return;
         }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelLoggerFake.java
index b6ea063..f609306 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelLoggerFake.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelLoggerFake.java
@@ -51,4 +51,9 @@
             NotificationChannelGroup channelGroup, int uid, String pkg, boolean wasBlocked) {
         mCalls.add(new CallRecord(event));
     }
+
+    @Override
+    public void logAppEvent(NotificationChannelEvent event, int uid, String pkg) {
+        mCalls.add(new CallRecord(event));
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 622a203..2e49929 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -2266,6 +2266,14 @@
     }
 
     @Test
+    public void testAppBlockedLogging() {
+        mHelper.setEnabled(PKG_N_MR1, 1020, false);
+        assertEquals(1, mLogger.getCalls().size());
+        assertEquals(
+                NotificationChannelLogger.NotificationChannelEvent.APP_NOTIFICATIONS_BLOCKED,
+                mLogger.get(0).event);
+    }
+    @Test
     public void testXml_statusBarIcons_default() throws Exception {
         String preQXml = "<ranking version=\"1\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 5a952b3..8cf8507 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
@@ -79,6 +80,7 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
 
 import android.annotation.SuppressLint;
 import android.app.ActivityTaskManager;
@@ -1231,11 +1233,29 @@
     }
 
     @Test
+    public void testRecentsNotRotatingWithFixedRotation() {
+        final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation();
+        doCallRealMethod().when(displayRotation).updateRotationUnchecked(anyBoolean());
+        doCallRealMethod().when(displayRotation).updateOrientation(anyInt(), anyBoolean());
+
+        final ActivityRecord recentsActivity = createActivityRecord(mDisplayContent,
+                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_RECENTS);
+        recentsActivity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
+
+        mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity);
+        displayRotation.setRotation((displayRotation.getRotation() + 1) % 4);
+        assertFalse(displayRotation.updateRotationUnchecked(false));
+
+        mDisplayContent.mFixedRotationTransitionListener.onFinishRecentsAnimation(false);
+        assertTrue(displayRotation.updateRotationUnchecked(false));
+    }
+
+    @Test
     public void testRemoteRotation() {
         DisplayContent dc = createNewDisplay();
 
         final DisplayRotation dr = dc.getDisplayRotation();
-        Mockito.doCallRealMethod().when(dr).updateRotationUnchecked(anyBoolean());
+        doCallRealMethod().when(dr).updateRotationUnchecked(anyBoolean());
         Mockito.doReturn(ROTATION_90).when(dr).rotationForOrientation(anyInt(), anyInt());
         final boolean[] continued = new boolean[1];
         // TODO(display-merge): Remove cast
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
index 4907bdc..d6ec788 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
@@ -190,7 +190,7 @@
     public void testCalculateSnapshotFrame() {
         setupSurface(100, 100);
         final Rect insets = new Rect(0, 10, 0, 10);
-        mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+        mSurface.setFrames(new Rect(0, 0, 100, 100), insets);
         assertEquals(new Rect(0, 0, 100, 80),
                 mSurface.calculateSnapshotFrame(new Rect(0, 10, 100, 90)));
     }
@@ -199,7 +199,7 @@
     public void testCalculateSnapshotFrame_navBarLeft() {
         setupSurface(100, 100);
         final Rect insets = new Rect(10, 10, 0, 0);
-        mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+        mSurface.setFrames(new Rect(0, 0, 100, 100), insets);
         assertEquals(new Rect(10, 0, 100, 90),
                 mSurface.calculateSnapshotFrame(new Rect(10, 10, 100, 100)));
     }
@@ -208,7 +208,7 @@
     public void testCalculateSnapshotFrame_waterfall() {
         setupSurface(100, 100, new Rect(5, 10, 5, 10), 0, 0, new Rect(0, 0, 100, 100));
         final Rect insets = new Rect(0, 10, 0, 10);
-        mSurface.setFrames(new Rect(5, 0, 95, 100), insets, insets);
+        mSurface.setFrames(new Rect(5, 0, 95, 100), insets);
         assertEquals(new Rect(0, 0, 90, 90),
                 mSurface.calculateSnapshotFrame(new Rect(5, 0, 95, 90)));
     }
@@ -217,7 +217,7 @@
     public void testDrawStatusBarBackground() {
         setupSurface(100, 100);
         final Rect insets = new Rect(0, 10, 10, 0);
-        mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+        mSurface.setFrames(new Rect(0, 0, 100, 100), insets);
         final Canvas mockCanvas = mock(Canvas.class);
         when(mockCanvas.getWidth()).thenReturn(100);
         when(mockCanvas.getHeight()).thenReturn(100);
@@ -230,7 +230,7 @@
     public void testDrawStatusBarBackground_nullFrame() {
         setupSurface(100, 100);
         final Rect insets = new Rect(0, 10, 10, 0);
-        mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+        mSurface.setFrames(new Rect(0, 0, 100, 100), insets);
         final Canvas mockCanvas = mock(Canvas.class);
         when(mockCanvas.getWidth()).thenReturn(100);
         when(mockCanvas.getHeight()).thenReturn(100);
@@ -243,7 +243,7 @@
     public void testDrawStatusBarBackground_nope() {
         setupSurface(100, 100);
         final Rect insets = new Rect(0, 10, 10, 0);
-        mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+        mSurface.setFrames(new Rect(0, 0, 100, 100), insets);
         final Canvas mockCanvas = mock(Canvas.class);
         when(mockCanvas.getWidth()).thenReturn(100);
         when(mockCanvas.getHeight()).thenReturn(100);
@@ -257,7 +257,7 @@
         final Rect insets = new Rect(0, 10, 0, 10);
         setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
                 new Rect(0, 0, 100, 100));
-        mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+        mSurface.setFrames(new Rect(0, 0, 100, 100), insets);
         final Canvas mockCanvas = mock(Canvas.class);
         when(mockCanvas.getWidth()).thenReturn(100);
         when(mockCanvas.getHeight()).thenReturn(100);
@@ -270,7 +270,7 @@
         final Rect insets = new Rect(10, 10, 0, 0);
         setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
                 new Rect(0, 0, 100, 100));
-        mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+        mSurface.setFrames(new Rect(0, 0, 100, 100), insets);
         final Canvas mockCanvas = mock(Canvas.class);
         when(mockCanvas.getWidth()).thenReturn(100);
         when(mockCanvas.getHeight()).thenReturn(100);
@@ -283,7 +283,7 @@
         final Rect insets = new Rect(0, 10, 10, 0);
         setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
                 new Rect(0, 0, 100, 100));
-        mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+        mSurface.setFrames(new Rect(0, 0, 100, 100), insets);
         final Canvas mockCanvas = mock(Canvas.class);
         when(mockCanvas.getWidth()).thenReturn(100);
         when(mockCanvas.getHeight()).thenReturn(100);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 0d26e44..4a0f48cf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.hardware.camera2.params.OutputConfiguration.ROTATION_90;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
@@ -244,6 +245,12 @@
         appWindow.mAttrs.flags &= ~FLAG_NOT_FOCUSABLE;
         assertTrue(appWindow.canBeImeTarget());
 
+        // Verify PINNED windows can't be IME target.
+        int initialMode = appWindow.mActivityRecord.getWindowingMode();
+        appWindow.mActivityRecord.setWindowingMode(WINDOWING_MODE_PINNED);
+        assertFalse(appWindow.canBeImeTarget());
+        appWindow.mActivityRecord.setWindowingMode(initialMode);
+
         // Make windows invisible
         appWindow.hideLw(false /* doAnimation */);
         imeWindow.hideLw(false /* doAnimation */);
diff --git a/tests/net/common/java/android/net/LinkPropertiesTest.java b/tests/net/common/java/android/net/LinkPropertiesTest.java
index 0fc9be3..6eba62e 100644
--- a/tests/net/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/net/common/java/android/net/LinkPropertiesTest.java
@@ -16,6 +16,8 @@
 
 package android.net;
 
+import static android.net.RouteInfo.RTN_THROW;
+import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 
 import static com.android.testutils.ParcelUtilsKt.assertParcelSane;
@@ -1282,4 +1284,20 @@
         assertTrue(lp.hasIpv6UnreachableDefaultRoute());
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
     }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testRouteAddWithSameKey() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("wlan0");
+        final IpPrefix v6 = new IpPrefix("64:ff9b::/96");
+        lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1280));
+        assertEquals(1, lp.getRoutes().size());
+        lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1500));
+        assertEquals(1, lp.getRoutes().size());
+        final IpPrefix v4 = new IpPrefix("192.0.2.128/25");
+        lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_UNICAST, 1460));
+        assertEquals(2, lp.getRoutes().size());
+        lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460));
+        assertEquals(2, lp.getRoutes().size());
+    }
 }
diff --git a/tests/net/common/java/android/net/RouteInfoTest.java b/tests/net/common/java/android/net/RouteInfoTest.java
index 8204b49..60cac0b 100644
--- a/tests/net/common/java/android/net/RouteInfoTest.java
+++ b/tests/net/common/java/android/net/RouteInfoTest.java
@@ -25,6 +25,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -56,7 +57,7 @@
     private static final int INVALID_ROUTE_TYPE = -1;
 
     private InetAddress Address(String addr) {
-        return InetAddress.parseNumericAddress(addr);
+        return InetAddresses.parseNumericAddress(addr);
     }
 
     private IpPrefix Prefix(String prefix) {
@@ -391,4 +392,43 @@
         r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0");
         assertEquals(0, r.getMtu());
     }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testRouteKey() {
+        RouteInfo.RouteKey k1, k2;
+        // Only prefix, null gateway and null interface
+        k1 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey();
+        k2 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey();
+        assertEquals(k1, k2);
+        assertEquals(k1.hashCode(), k2.hashCode());
+
+        // With prefix, gateway and interface. Type and MTU does not affect RouteKey equality
+        k1 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0",
+                RTN_UNREACHABLE, 1450).getRouteKey();
+        k2 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0",
+                RouteInfo.RTN_UNICAST, 1400).getRouteKey();
+        assertEquals(k1, k2);
+        assertEquals(k1.hashCode(), k2.hashCode());
+
+        // Different scope IDs are ignored by the kernel, so we consider them equal here too.
+        k1 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%1"), "wlan0").getRouteKey();
+        k2 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%2"), "wlan0").getRouteKey();
+        assertEquals(k1, k2);
+        assertEquals(k1.hashCode(), k2.hashCode());
+
+        // Different prefix
+        k1 = new RouteInfo(Prefix("192.0.2.0/24"), null).getRouteKey();
+        k2 = new RouteInfo(Prefix("192.0.3.0/24"), null).getRouteKey();
+        assertNotEquals(k1, k2);
+
+        // Different gateway
+        k1 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), null).getRouteKey();
+        k2 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::2"), null).getRouteKey();
+        assertNotEquals(k1, k2);
+
+        // Different interface
+        k1 = new RouteInfo(Prefix("ff02::1/128"), null, "tun0").getRouteKey();
+        k2 = new RouteInfo(Prefix("ff02::1/128"), null, "tun1").getRouteKey();
+        assertNotEquals(k1, k2);
+    }
 }