Merge "Fix shadow and animation clipping for Global Actions Dialog." into qt-dev
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index f53ac1b..abcccf8 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -364,6 +364,7 @@
         AppsOnExternalStorageInfo apps_on_external_storage_info = 10057;
         FaceSettings face_settings = 10058;
         CoolingDevice cooling_device = 10059;
+        AppOps app_ops = 10060;
     }
 
     // DO NOT USE field numbers above 100,000 in AOSP.
@@ -6357,3 +6358,41 @@
     // Whether GarageMode is entered.
     optional bool is_garage_mode = 1;
 }
+
+/**
+ * Historical app ops data per package.
+ */
+message AppOps {
+    // Uid of the package requesting the op
+    optional int32 uid = 1 [(is_uid) = true];
+
+    // Nmae of the package performing the op
+    optional string package_name = 2;
+
+    // operation id; maps to the OP_* constants in AppOpsManager.java
+    optional int32 op_id = 3;
+
+    // The number of times the op was granted while the app was in the
+    // foreground (only for trusted requests)
+    optional int64 trusted_foreground_granted_count = 4;
+
+    // The number of times the op was granted while the app was in the
+    // background (only for trusted requests)
+    optional int64 trusted_background_granted_count = 5;
+
+    // The number of times the op was rejected while the app was in the
+    // foreground (only for trusted requests)
+    optional int64 trusted_foreground_rejected_count = 6;
+
+    // The number of times the op was rejected while the app was in the
+    // background (only for trusted requests)
+    optional int64 trusted_background_rejected_count = 7;
+
+    // For long-running operations, total duration of the operation
+    // while the app was in the foreground (only for trusted requests)
+    optional int64 trusted_foreground_duration_millis = 8;
+
+    // For long-running operations, total duration of the operation
+    // while the app was in the background (only for trusted requests)
+    optional int64 trusted_background_duration_millis = 9;
+}
diff --git a/cmds/statsd/src/external/StatsPullerManager.cpp b/cmds/statsd/src/external/StatsPullerManager.cpp
index 2ffe18e..914d60d 100644
--- a/cmds/statsd/src/external/StatsPullerManager.cpp
+++ b/cmds/statsd/src/external/StatsPullerManager.cpp
@@ -260,6 +260,9 @@
         // Face Settings
         {android::util::FACE_SETTINGS,
          {.puller = new StatsCompanionServicePuller(android::util::FACE_SETTINGS)}},
+        // App ops
+        {android::util::APP_OPS,
+         {.puller = new StatsCompanionServicePuller(android::util::APP_OPS)}},
 };
 
 StatsPullerManager::StatsPullerManager() : mNextPullTimeNs(NO_ALARM_UPDATE) {
diff --git a/core/java/android/app/JobSchedulerImpl.java b/core/java/android/app/JobSchedulerImpl.java
index 5494e2a..e877018 100644
--- a/core/java/android/app/JobSchedulerImpl.java
+++ b/core/java/android/app/JobSchedulerImpl.java
@@ -83,7 +83,7 @@
     @Override
     public List<JobInfo> getAllPendingJobs() {
         try {
-            return mBinder.getAllPendingJobs();
+            return mBinder.getAllPendingJobs().getList();
         } catch (RemoteException e) {
             return null;
         }
@@ -110,7 +110,7 @@
     @Override
     public List<JobSnapshot> getAllJobSnapshots() {
         try {
-            return mBinder.getAllJobSnapshots();
+            return mBinder.getAllJobSnapshots().getList();
         } catch (RemoteException e) {
             return null;
         }
diff --git a/core/java/android/app/VoiceInteractor.java b/core/java/android/app/VoiceInteractor.java
index 7828573..b37120f 100644
--- a/core/java/android/app/VoiceInteractor.java
+++ b/core/java/android/app/VoiceInteractor.java
@@ -79,10 +79,10 @@
     /** @hide */
     public static final String KEY_KILL_SIGNAL = "key_kill_signal";
 
-    IVoiceInteractor mInteractor;
+    @Nullable IVoiceInteractor mInteractor;
 
-    Context mContext;
-    Activity mActivity;
+    @Nullable Context mContext;
+    @Nullable Activity mActivity;
     boolean mRetaining;
 
     final HandlerCaller mHandlerCaller;
@@ -999,7 +999,9 @@
 
         // destroyed now
         mInteractor = null;
-        mActivity.setVoiceInteractor(null);
+        if (mActivity != null) {
+            mActivity.setVoiceInteractor(null);
+        }
     }
 
     public boolean submitRequest(Request request) {
diff --git a/core/java/android/app/job/IJobScheduler.aidl b/core/java/android/app/job/IJobScheduler.aidl
index 53b33c2..3006f50 100644
--- a/core/java/android/app/job/IJobScheduler.aidl
+++ b/core/java/android/app/job/IJobScheduler.aidl
@@ -19,6 +19,7 @@
 import android.app.job.JobInfo;
 import android.app.job.JobSnapshot;
 import android.app.job.JobWorkItem;
+import android.content.pm.ParceledListSlice;
 
  /**
   * IPC interface that supports the app-facing {@link #JobScheduler} api.
@@ -30,8 +31,8 @@
     int scheduleAsPackage(in JobInfo job, String packageName, int userId, String tag);
     void cancel(int jobId);
     void cancelAll();
-    List<JobInfo> getAllPendingJobs();
+    ParceledListSlice getAllPendingJobs();
     JobInfo getPendingJob(int jobId);
     List<JobInfo> getStartedJobs();
-    List<JobSnapshot> getAllJobSnapshots();
+    ParceledListSlice getAllJobSnapshots();
 }
diff --git a/core/java/android/app/usage/NetworkStatsManager.java b/core/java/android/app/usage/NetworkStatsManager.java
index 59ae334..8e40449 100644
--- a/core/java/android/app/usage/NetworkStatsManager.java
+++ b/core/java/android/app/usage/NetworkStatsManager.java
@@ -278,6 +278,12 @@
             return null;
         }
 
+        return querySummary(template, startTime, endTime);
+    }
+
+    /** @hide */
+    public NetworkStats querySummary(NetworkTemplate template, long startTime,
+            long endTime) throws SecurityException, RemoteException {
         NetworkStats result;
         result = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
         result.startSummaryEnumeration();
@@ -296,6 +302,13 @@
             NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
     }
 
+    /** @hide */
+    public NetworkStats queryDetailsForUid(NetworkTemplate template,
+            long startTime, long endTime, int uid) throws SecurityException {
+        return queryDetailsForUidTagState(template, startTime, endTime, uid,
+                NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
+    }
+
     /**
      * Query network usage statistics details for a given uid and tag.
      *
@@ -340,6 +353,13 @@
         NetworkTemplate template;
         template = createTemplate(networkType, subscriberId);
 
+        return queryDetailsForUidTagState(template, startTime, endTime, uid, tag, state);
+    }
+
+    /** @hide */
+    public NetworkStats queryDetailsForUidTagState(NetworkTemplate template,
+            long startTime, long endTime, int uid, int tag, int state) throws SecurityException {
+
         NetworkStats result;
         try {
             result = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index ee62af5..69c1295 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -248,6 +248,8 @@
     public static final int FLAG_STORAGE_DE = IInstalld.FLAG_STORAGE_DE;
     /** {@hide} */
     public static final int FLAG_STORAGE_CE = IInstalld.FLAG_STORAGE_CE;
+    /** {@hide} */
+    public static final int FLAG_STORAGE_EXTERNAL = IInstalld.FLAG_STORAGE_EXTERNAL;
 
     /** {@hide} */
     public static final int FLAG_FOR_WRITE = 1 << 8;
diff --git a/core/java/android/os/storage/VolumeRecord.java b/core/java/android/os/storage/VolumeRecord.java
index b6ee261..1a794eb 100644
--- a/core/java/android/os/storage/VolumeRecord.java
+++ b/core/java/android/os/storage/VolumeRecord.java
@@ -25,6 +25,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 
+import java.util.Locale;
 import java.util.Objects;
 
 /**
@@ -45,6 +46,7 @@
     public String nickname;
     public int userFlags;
     public long createdMillis;
+    public long lastSeenMillis;
     public long lastTrimMillis;
     public long lastBenchMillis;
 
@@ -61,6 +63,7 @@
         nickname = parcel.readString();
         userFlags = parcel.readInt();
         createdMillis = parcel.readLong();
+        lastSeenMillis = parcel.readLong();
         lastTrimMillis = parcel.readLong();
         lastBenchMillis = parcel.readLong();
     }
@@ -73,6 +76,10 @@
         return fsUuid;
     }
 
+    public String getNormalizedFsUuid() {
+        return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null;
+    }
+
     public String getNickname() {
         return nickname;
     }
@@ -97,6 +104,7 @@
                 DebugUtils.flagsToString(VolumeRecord.class, "USER_FLAG_", userFlags));
         pw.println();
         pw.printPair("createdMillis", TimeUtils.formatForLogging(createdMillis));
+        pw.printPair("lastSeenMillis", TimeUtils.formatForLogging(lastSeenMillis));
         pw.printPair("lastTrimMillis", TimeUtils.formatForLogging(lastTrimMillis));
         pw.printPair("lastBenchMillis", TimeUtils.formatForLogging(lastBenchMillis));
         pw.decreaseIndent();
@@ -155,6 +163,7 @@
         parcel.writeString(nickname);
         parcel.writeInt(userFlags);
         parcel.writeLong(createdMillis);
+        parcel.writeLong(lastSeenMillis);
         parcel.writeLong(lastTrimMillis);
         parcel.writeLong(lastBenchMillis);
     }
diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java
index 1391d43..be20570 100644
--- a/core/java/android/service/appprediction/AppPredictionService.java
+++ b/core/java/android/service/appprediction/AppPredictionService.java
@@ -39,6 +39,7 @@
 import android.os.RemoteException;
 import android.service.appprediction.IPredictionService.Stub;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.util.Slog;
 
 import java.util.ArrayList;
@@ -46,7 +47,7 @@
 import java.util.function.Consumer;
 
 /**
- * TODO(b/111701043): Add java docs
+ * A service used to predict app and shortcut usage.
  *
  * @hide
  */
@@ -58,7 +59,9 @@
 
     /**
      * The {@link Intent} that must be declared as handled by the service.
-     * TODO(b/111701043): Add any docs about permissions the service must hold
+     *
+     * <p>The service must also require the {@link android.permission#MANAGE_APP_PREDICTIONS}
+     * permission.
      *
      * @hide
      */
@@ -145,8 +148,11 @@
     @Override
     @NonNull
     public final IBinder onBind(@NonNull Intent intent) {
-        // TODO(b/111701043): Verify that the action is valid
-        return mInterface.asBinder();
+        if (SERVICE_INTERFACE.equals(intent.getAction())) {
+            return mInterface.asBinder();
+        }
+        Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+        return null;
     }
 
     /**
@@ -180,7 +186,6 @@
 
     /**
      * Called by the client app to request sorting of targets based on prediction rank.
-     * TODO(b/111701043): Implement CancellationSignal so caller can cancel a long running request
      */
     @MainThread
     public abstract void onSortAppTargets(@NonNull AppPredictionSessionId sessionId,
@@ -254,7 +259,6 @@
     /**
      * Called by the client app to request target predictions. This method is only called if there
      * are one or more prediction callbacks registered.
-     * TODO(b/111701043): Add java docs
      *
      * @see #updatePredictions(AppPredictionSessionId, List)
      */
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index d67c884..63e1485 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -2660,6 +2660,9 @@
          */
         @NonNull
         public Transaction merge(@NonNull Transaction other) {
+            if (this == other) {
+                return this;
+            }
             mResizedSurfaces.putAll(other.mResizedSurfaces);
             other.mResizedSurfaces.clear();
             nativeMergeTransaction(mNativeObject, other.mNativeObject);
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 2d292ef..9340b71 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -156,7 +156,9 @@
      * @param src Source to copy insets from
      */
     public WindowInsets(WindowInsets src) {
-        this(src.mTypeInsetsMap, src.mTypeMaxInsetsMap, src.mTypeVisibilityMap, src.mIsRound,
+        this(src.mSystemWindowInsetsConsumed ? null : src.mTypeInsetsMap,
+                src.mStableInsetsConsumed ? null : src.mTypeMaxInsetsMap,
+                src.mTypeVisibilityMap, src.mIsRound,
                 src.mAlwaysConsumeSystemBars, displayCutoutCopyConstructorArgument(src));
     }
 
diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java
index cc2caca..cdb79ab 100644
--- a/core/java/com/android/internal/content/FileSystemProvider.java
+++ b/core/java/com/android/internal/content/FileSystemProvider.java
@@ -247,7 +247,6 @@
             }
             childId = getDocIdForFile(file);
             onDocIdChanged(childId);
-            addFolderToMediaStore(getFileForDocId(childId, true));
         } else {
             try {
                 if (!file.createNewFile()) {
@@ -259,19 +258,11 @@
                 throw new IllegalStateException("Failed to touch " + file + ": " + e);
             }
         }
+        MediaStore.scanFile(getContext(), file);
 
         return childId;
     }
 
-    private void addFolderToMediaStore(@Nullable File visibleFolder) {
-        // visibleFolder is null if we're adding a folder to external thumb drive or SD card.
-        if (visibleFolder != null) {
-            assert (visibleFolder.isDirectory());
-
-            MediaStore.scanFile(getContext(), visibleFolder);
-        }
-    }
-
     @Override
     public String renameDocument(String docId, String displayName) throws FileNotFoundException {
         // Since this provider treats renames as generating a completely new
@@ -293,7 +284,6 @@
         moveInMediaStore(beforeVisibleFile, afterVisibleFile);
 
         if (!TextUtils.equals(docId, afterDocId)) {
-            scanFile(afterVisibleFile);
             return afterDocId;
         } else {
             return null;
diff --git a/core/java/com/android/internal/infra/AbstractRemoteService.java b/core/java/com/android/internal/infra/AbstractRemoteService.java
index 64f8857..3900f16 100644
--- a/core/java/com/android/internal/infra/AbstractRemoteService.java
+++ b/core/java/com/android/internal/infra/AbstractRemoteService.java
@@ -231,6 +231,7 @@
         @SuppressWarnings("unchecked") // TODO(b/117779333): fix this warning
         final S castService = (S) this;
         mVultureCallback.onServiceDied(castService);
+        handleBindFailure();
     }
 
     // Note: we are dumping without a lock held so this is a bit racy but
@@ -406,7 +407,8 @@
             @NonNull BasePendingRequest<S, I> pendingRequest);
 
     /**
-     * Called if {@link Context#bindServiceAsUser} returns {@code false}.
+     * Called if {@link Context#bindServiceAsUser} returns {@code false}, or
+     * if {@link DeathRecipient#binderDied()} is called.
      */
     abstract void handleBindFailure();
 
@@ -431,8 +433,6 @@
             mBinding = false;
 
             if (!mServiceDied) {
-                // TODO(b/126266412): merge these 2 calls?
-                handleBindFailure();
                 handleBinderDied();
             }
         }
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 0cf9500..7de6ca5 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1533,7 +1533,7 @@
     <!-- Message shown during face acquisition when only the left part of the user's face was detected [CHAR LIMIT=50] -->
     <string name="face_acquired_too_left">Move phone to the right.</string>
     <!-- Message shown during face acquisition when the user is not front facing the sensor [CHAR LIMIT=50] -->
-    <string name="face_acquired_poor_gaze">Look at the screen with your eyes open.</string>
+    <string name="face_acquired_poor_gaze">Please look more directly at your device.</string>
     <!-- Message shown during face acquisition when the user is not detected [CHAR LIMIT=50] -->
     <string name="face_acquired_not_detected">Can\u2019t see your face. Look at the phone.</string>
     <!-- Message shown during face acquisition when the device is not steady [CHAR LIMIT=50] -->
diff --git a/data/etc/com.android.dialer.xml b/data/etc/com.android.dialer.xml
index ccdb21f..405279f 100644
--- a/data/etc/com.android.dialer.xml
+++ b/data/etc/com.android.dialer.xml
@@ -24,5 +24,7 @@
         <permission name="android.permission.STOP_APP_SWITCHES"/>
         <permission name="com.android.voicemail.permission.READ_VOICEMAIL"/>
         <permission name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+        <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
+        <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
     </privapp-permissions>
 </permissions>
diff --git a/graphics/java/android/graphics/drawable/GradientDrawable.java b/graphics/java/android/graphics/drawable/GradientDrawable.java
index 11d635e..297153d 100644
--- a/graphics/java/android/graphics/drawable/GradientDrawable.java
+++ b/graphics/java/android/graphics/drawable/GradientDrawable.java
@@ -637,8 +637,7 @@
      * @see #setOrientation(Orientation)
      */
     public Orientation getOrientation() {
-        updateGradientStateOrientation();
-        return mGradientState.mOrientation;
+        return mGradientState.getOrientation();
     }
 
     /**
@@ -654,10 +653,7 @@
      * @see #getOrientation()
      */
     public void setOrientation(Orientation orientation) {
-        // Update the angle here so that subsequent attempts to obtain the orientation
-        // from the angle overwrite previously configured values during inflation
-        mGradientState.mAngle = getAngleFromOrientation(orientation);
-        mGradientState.mOrientation = orientation;
+        mGradientState.setOrientation(orientation);
         mGradientIsDirty = true;
         invalidateSelf();
     }
@@ -1246,76 +1242,6 @@
     }
 
     /**
-     * Update the orientation of the gradient based on the given angle only if the type is
-     * {@link #LINEAR_GRADIENT}
-     */
-    private void updateGradientStateOrientation() {
-        if (mGradientState.mGradient == LINEAR_GRADIENT) {
-            int angle = mGradientState.mAngle;
-            if (angle % 45 != 0) {
-                throw new IllegalArgumentException("Linear gradient requires 'angle' attribute to "
-                     + "be a multiple of 45");
-            }
-
-            Orientation orientation;
-            switch (angle) {
-                case 0:
-                    orientation = Orientation.LEFT_RIGHT;
-                    break;
-                case 45:
-                    orientation = Orientation.BL_TR;
-                    break;
-                case 90:
-                    orientation = Orientation.BOTTOM_TOP;
-                    break;
-                case 135:
-                    orientation = Orientation.BR_TL;
-                    break;
-                case 180:
-                    orientation = Orientation.RIGHT_LEFT;
-                    break;
-                case 225:
-                    orientation = Orientation.TR_BL;
-                    break;
-                case 270:
-                    orientation = Orientation.TOP_BOTTOM;
-                    break;
-                case 315:
-                    orientation = Orientation.TL_BR;
-                    break;
-                default:
-                    // Should not get here as exception is thrown above if angle is not multiple
-                    // of 45 degrees
-                    orientation = Orientation.LEFT_RIGHT;
-                    break;
-            }
-            mGradientState.mOrientation = orientation;
-        }
-    }
-
-    private int getAngleFromOrientation(Orientation orientation) {
-        switch (orientation) {
-            default:
-            case LEFT_RIGHT:
-                return 0;
-            case BL_TR:
-                return 45;
-            case BOTTOM_TOP:
-                return 90;
-            case BR_TL:
-                return 135;
-            case RIGHT_LEFT:
-                return 180;
-            case TR_BL:
-                return 225;
-            case TOP_BOTTOM:
-                return 270;
-            case TL_BR:
-                return 315;
-        }
-    }
-
-    /**
      * This checks mGradientIsDirty, and if it is true, recomputes both our drawing
      * rectangle (mRect) and the gradient itself, since it depends on our
      * rectangle too.
@@ -1344,8 +1270,7 @@
 
                 if (st.mGradient == LINEAR_GRADIENT) {
                     final float level = st.mUseLevel ? getLevel() / 10000.0f : 1.0f;
-                    updateGradientStateOrientation();
-                    switch (st.mOrientation) {
+                    switch (st.getOrientation()) {
                     case TOP_BOTTOM:
                         x0 = r.left;            y0 = r.top;
                         x1 = x0;                y1 = level * r.bottom;
@@ -2056,7 +1981,7 @@
         int[] mAttrPadding;
 
         public GradientState(Orientation orientation, int[] gradientColors) {
-            mOrientation = orientation;
+            setOrientation(orientation);
             setGradientColors(gradientColors);
         }
 
@@ -2259,6 +2184,93 @@
             mCenterY = y;
         }
 
+        public void setOrientation(Orientation orientation) {
+            // Update the angle here so that subsequent attempts to obtain the orientation
+            // from the angle overwrite previously configured values during inflation
+            mAngle = getAngleFromOrientation(orientation);
+            mOrientation = orientation;
+        }
+
+        @NonNull
+        public Orientation getOrientation() {
+            updateGradientStateOrientation();
+            return mOrientation;
+        }
+
+        /**
+         * Update the orientation of the gradient based on the given angle only if the type is
+         * {@link #LINEAR_GRADIENT}
+         */
+        private void updateGradientStateOrientation() {
+            if (mGradient == LINEAR_GRADIENT) {
+                int angle = mAngle;
+                if (angle % 45 != 0) {
+                    throw new IllegalArgumentException("Linear gradient requires 'angle' attribute "
+                            + "to be a multiple of 45");
+                }
+
+                Orientation orientation;
+                switch (angle) {
+                    case 0:
+                        orientation = Orientation.LEFT_RIGHT;
+                        break;
+                    case 45:
+                        orientation = Orientation.BL_TR;
+                        break;
+                    case 90:
+                        orientation = Orientation.BOTTOM_TOP;
+                        break;
+                    case 135:
+                        orientation = Orientation.BR_TL;
+                        break;
+                    case 180:
+                        orientation = Orientation.RIGHT_LEFT;
+                        break;
+                    case 225:
+                        orientation = Orientation.TR_BL;
+                        break;
+                    case 270:
+                        orientation = Orientation.TOP_BOTTOM;
+                        break;
+                    case 315:
+                        orientation = Orientation.TL_BR;
+                        break;
+                    default:
+                        // Should not get here as exception is thrown above if angle is not multiple
+                        // of 45 degrees
+                        orientation = Orientation.LEFT_RIGHT;
+                        break;
+                }
+                mOrientation = orientation;
+            }
+        }
+
+        private int getAngleFromOrientation(@Nullable Orientation orientation) {
+            if (orientation != null) {
+                switch (orientation) {
+                    default:
+                    case LEFT_RIGHT:
+                        return 0;
+                    case BL_TR:
+                        return 45;
+                    case BOTTOM_TOP:
+                        return 90;
+                    case BR_TL:
+                        return 135;
+                    case RIGHT_LEFT:
+                        return 180;
+                    case TR_BL:
+                        return 225;
+                    case TOP_BOTTOM:
+                        return 270;
+                    case TL_BR:
+                        return 315;
+                }
+            } else {
+                return 0;
+            }
+        }
+
         public void setGradientColors(@Nullable int[] colors) {
             mGradientColors = colors;
             mSolidColors = null;
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index 62fd489..5173f63 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -167,8 +167,6 @@
     LOG_ALWAYS_FATAL_IF(physDeviceProperties.apiVersion < VK_MAKE_VERSION(1, 1, 0));
     mDriverVersion = physDeviceProperties.driverVersion;
 
-    mIsQualcomm = physDeviceProperties.vendorID == 20803;
-
     // query to get the initial queue props size
     uint32_t queueCount;
     mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, nullptr);
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
index 31de803..dd3c6d0 100644
--- a/libs/hwui/renderthread/VulkanManager.h
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -179,13 +179,6 @@
     SwapBehavior mSwapBehavior = SwapBehavior::Discard;
     GrVkExtensions mExtensions;
     uint32_t mDriverVersion = 0;
-
-    // TODO: Remove once fix has landed. Temporaryly needed for workaround for setting up AHB
-    // surfaces on Qualcomm. Currently if you don't use VkSwapchain Qualcomm is not setting
-    // reporting that we need to use one of their private vendor usage bits which greatly effects
-    // performance if it is not used.
-    bool mIsQualcomm = false;
-    bool isQualcomm() const { return mIsQualcomm; }
 };
 
 } /* namespace renderthread */
diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp
index df6b9ed..b2cc23e 100644
--- a/libs/hwui/renderthread/VulkanSurface.cpp
+++ b/libs/hwui/renderthread/VulkanSurface.cpp
@@ -297,11 +297,6 @@
     native_window_get_consumer_usage(window, &consumerUsage);
     windowInfo.windowUsageFlags = consumerUsage | hwbUsage.androidHardwareBufferUsage;
 
-    if (vkManager.isQualcomm()) {
-        windowInfo.windowUsageFlags =
-                windowInfo.windowUsageFlags | AHARDWAREBUFFER_USAGE_VENDOR_0;
-    }
-
     /*
      * Now we attempt to modify the window!
      */
diff --git a/media/java/android/media/ThumbnailUtils.java b/media/java/android/media/ThumbnailUtils.java
index 5de56c7..b3c2bb7 100644
--- a/media/java/android/media/ThumbnailUtils.java
+++ b/media/java/android/media/ThumbnailUtils.java
@@ -52,6 +52,7 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.Objects;
 import java.util.function.ToIntFunction;
 
 /**
@@ -369,10 +370,12 @@
             // If we're okay with something larger than native format, just
             // return a frame without up-scaling it
             if (size.getWidth() > width && size.getHeight() > height) {
-                return mmr.getFrameAtTime(duration / 2, OPTION_CLOSEST_SYNC);
+                return Objects.requireNonNull(
+                        mmr.getFrameAtTime(duration / 2, OPTION_CLOSEST_SYNC));
             } else {
-                return mmr.getScaledFrameAtTime(duration / 2, OPTION_CLOSEST_SYNC,
-                        size.getWidth(), size.getHeight());
+                return Objects.requireNonNull(
+                        mmr.getScaledFrameAtTime(duration / 2, OPTION_CLOSEST_SYNC,
+                        size.getWidth(), size.getHeight()));
             }
         } catch (RuntimeException e) {
             throw new IOException("Failed to create thumbnail", e);
diff --git a/packages/CaptivePortalLogin/Android.bp b/packages/CaptivePortalLogin/Android.bp
index a345091..1f6c2ae 100644
--- a/packages/CaptivePortalLogin/Android.bp
+++ b/packages/CaptivePortalLogin/Android.bp
@@ -14,12 +14,11 @@
 // limitations under the License.
 //
 
-android_app {
-    name: "CaptivePortalLogin",
+java_defaults {
+    name: "CaptivePortalLoginDefaults",
     srcs: ["src/**/*.java"],
     sdk_version: "system_current",
     min_sdk_version: "28",
-    certificate: "networkstack",
     static_libs: [
         "androidx.legacy_legacy-support-v4",
         "metrics-constants-protos",
@@ -27,3 +26,18 @@
     ],
     manifest: "AndroidManifest.xml",
 }
+
+android_app {
+    name: "CaptivePortalLogin",
+    defaults: ["CaptivePortalLoginDefaults"],
+    certificate: "networkstack",
+}
+
+// Alternative CaptivePortalLogin signed with the platform cert, to use
+// with InProcessNetworkStack.
+android_app {
+    name: "PlatformCaptivePortalLogin",
+    defaults: ["CaptivePortalLoginDefaults"],
+    certificate: "platform",
+    overrides: ["CaptivePortalLogin"],
+}
diff --git a/packages/NetworkPermissionConfig/Android.bp b/packages/NetworkPermissionConfig/Android.bp
index d0d3276..6e50459 100644
--- a/packages/NetworkPermissionConfig/Android.bp
+++ b/packages/NetworkPermissionConfig/Android.bp
@@ -14,15 +14,28 @@
 // limitations under the License.
 //
 
-// Stub APK to define permissions for NetworkStack
-android_app {
-    name: "NetworkPermissionConfig",
+java_defaults {
+    name: "NetworkPermissionConfigDefaults",
     // TODO: mark app as hasCode=false in manifest once soong stops complaining about apps without
     // a classes.dex.
     srcs: ["src/**/*.java"],
     platform_apis: true,
     min_sdk_version: "28",
-    certificate: "networkstack",
     privileged: true,
     manifest: "AndroidManifest.xml",
 }
+
+// Stub APK to define permissions for NetworkStack
+android_app {
+    name: "NetworkPermissionConfig",
+    defaults: ["NetworkPermissionConfigDefaults"],
+    certificate: "networkstack",
+}
+
+// Alternative stub APK signed with platform certificate. To use with InProcessNetworkStack.
+android_app {
+    name: "PlatformNetworkPermissionConfig",
+    defaults: ["NetworkPermissionConfigDefaults"],
+    certificate: "platform",
+    overrides: ["NetworkPermissionConfig"],
+}
diff --git a/packages/NetworkStack/Android.bp b/packages/NetworkStack/Android.bp
index 64718da..e15526a 100644
--- a/packages/NetworkStack/Android.bp
+++ b/packages/NetworkStack/Android.bp
@@ -98,8 +98,6 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
-    // The permission configuration *must* be included to ensure security of the device
-    required: ["NetworkPermissionConfig"],
 }
 
 // Non-updatable network stack running in the system server process for devices not using the module
@@ -110,6 +108,8 @@
     manifest: "AndroidManifest_InProcess.xml",
     // InProcessNetworkStack is a replacement for NetworkStack
     overrides: ["NetworkStack"],
+    // The permission configuration *must* be included to ensure security of the device
+    required: ["PlatformNetworkPermissionConfig"],
 }
 
 // Updatable network stack packaged as an application
@@ -119,6 +119,8 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
+    // The permission configuration *must* be included to ensure security of the device
+    required: ["NetworkPermissionConfig"],
 }
 
 genrule {
diff --git a/packages/NetworkStack/src/android/net/apf/ApfFilter.java b/packages/NetworkStack/src/android/net/apf/ApfFilter.java
index 359c859..f054319 100644
--- a/packages/NetworkStack/src/android/net/apf/ApfFilter.java
+++ b/packages/NetworkStack/src/android/net/apf/ApfFilter.java
@@ -155,7 +155,8 @@
         DROPPED_ETHERTYPE_BLACKLISTED,
         DROPPED_ARP_REPLY_SPA_NO_HOST,
         DROPPED_IPV4_KEEPALIVE_ACK,
-        DROPPED_IPV6_KEEPALIVE_ACK;
+        DROPPED_IPV6_KEEPALIVE_ACK,
+        DROPPED_IPV4_NATT_KEEPALIVE;
 
         // Returns the negative byte offset from the end of the APF data segment for
         // a given counter.
@@ -857,12 +858,104 @@
         }
     }
 
-    // A class to hold keepalive ack information.
-    private abstract static class TcpKeepaliveAck {
+    // TODO: Refactor these subclasses to avoid so much repetition.
+    private abstract static class KeepalivePacket {
         // Note that the offset starts from IP header.
         // These must be added ether header length when generating program.
         static final int IP_HEADER_OFFSET = 0;
+        static final int IPV4_SRC_ADDR_OFFSET = IP_HEADER_OFFSET + 12;
 
+        // Append a filter for this keepalive ack to {@code gen}.
+        // Jump to drop if it matches the keepalive ack.
+        // Jump to the next filter if packet doesn't match the keepalive ack.
+        abstract void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException;
+    }
+
+    // A class to hold NAT-T keepalive ack information.
+    private class NattKeepaliveResponse extends KeepalivePacket {
+        static final int UDP_LENGTH_OFFSET = 4;
+        static final int UDP_HEADER_LEN = 8;
+
+        protected class NattKeepaliveResponseData {
+            public final byte[] srcAddress;
+            public final int srcPort;
+            public final byte[] dstAddress;
+            public final int dstPort;
+
+            NattKeepaliveResponseData(final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
+                srcAddress = sentKeepalivePacket.dstAddress;
+                srcPort = sentKeepalivePacket.dstPort;
+                dstAddress = sentKeepalivePacket.srcAddress;
+                dstPort = sentKeepalivePacket.srcPort;
+            }
+        }
+
+        protected final NattKeepaliveResponseData mPacket;
+        protected final byte[] mSrcDstAddr;
+        protected final byte[] mPortFingerprint;
+        // NAT-T keepalive packet
+        protected final byte[] mPayload = {(byte) 0xff};
+
+        NattKeepaliveResponse(final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
+            mPacket = new NattKeepaliveResponseData(sentKeepalivePacket);
+            mSrcDstAddr = concatArrays(mPacket.srcAddress, mPacket.dstAddress);
+            mPortFingerprint = generatePortFingerprint(mPacket.srcPort, mPacket.dstPort);
+        }
+
+        byte[] generatePortFingerprint(int srcPort, int dstPort) {
+            final ByteBuffer fp = ByteBuffer.allocate(4);
+            fp.order(ByteOrder.BIG_ENDIAN);
+            fp.putShort((short) srcPort);
+            fp.putShort((short) dstPort);
+            return fp.array();
+        }
+
+        @Override
+        void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+            final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked();
+
+            gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
+            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+
+            // A NAT-T keepalive packet contains 1 byte payload with the value 0xff
+            // Check payload length is 1
+            gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+            gen.addAdd(UDP_HEADER_LEN);
+            gen.addSwap();
+            gen.addLoad16(Register.R0, IPV4_TOTAL_LENGTH_OFFSET);
+            gen.addNeg(Register.R1);
+            gen.addAddR1();
+            gen.addJumpIfR0NotEquals(1, nextFilterLabel);
+
+            // Check that the ports match
+            gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+            gen.addAdd(ETH_HEADER_LEN);
+            gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel);
+
+            // Payload offset = R0 + UDP header length
+            gen.addAdd(UDP_HEADER_LEN);
+            gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel);
+
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE);
+            gen.addJump(mCountAndDropLabel);
+            gen.defineLabel(nextFilterLabel);
+        }
+
+        public String toString() {
+            try {
+                return String.format("%s -> %s",
+                        NetworkStackUtils.addressAndPortToString(
+                                InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort),
+                        NetworkStackUtils.addressAndPortToString(
+                                InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort));
+            } catch (UnknownHostException e) {
+                return "Unknown host";
+            }
+        }
+    }
+
+    // A class to hold TCP keepalive ack information.
+    private abstract static class TcpKeepaliveAck extends KeepalivePacket {
         protected static class TcpKeepaliveAckData {
             public final byte[] srcAddress;
             public final int srcPort;
@@ -870,6 +963,7 @@
             public final int dstPort;
             public final int seq;
             public final int ack;
+
             // Create the characteristics of the ack packet from the sent keepalive packet.
             TcpKeepaliveAckData(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
                 srcAddress = sentKeepalivePacket.dstAddress;
@@ -902,28 +996,18 @@
             return fp.array();
         }
 
-        static byte[] concatArrays(final byte[]... arr) {
-            int size = 0;
-            for (byte[] a : arr) {
-                size += a.length;
-            }
-            final byte[] result = new byte[size];
-            int offset = 0;
-            for (byte[] a : arr) {
-                System.arraycopy(a, 0, result, offset, a.length);
-                offset += a.length;
-            }
-            return result;
-        }
-
         public String toString() {
-            return String.format("%s(%d) -> %s(%d), seq=%d, ack=%d",
-                    mPacket.srcAddress,
-                    mPacket.srcPort,
-                    mPacket.dstAddress,
-                    mPacket.dstPort,
-                    mPacket.seq,
-                    mPacket.ack);
+            try {
+                return String.format("%s -> %s , seq=%d, ack=%d",
+                        NetworkStackUtils.addressAndPortToString(
+                                InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort),
+                        NetworkStackUtils.addressAndPortToString(
+                                InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort),
+                        Integer.toUnsignedLong(mPacket.seq),
+                        Integer.toUnsignedLong(mPacket.ack));
+            } catch (UnknownHostException e) {
+                return "Unknown host";
+            }
         }
 
         // Append a filter for this keepalive ack to {@code gen}.
@@ -933,7 +1017,6 @@
     }
 
     private class TcpKeepaliveAckV4 extends TcpKeepaliveAck {
-        private static final int IPV4_SRC_ADDR_OFFSET = IP_HEADER_OFFSET + 12;
 
         TcpKeepaliveAckV4(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
             this(new TcpKeepaliveAckData(sentKeepalivePacket));
@@ -987,7 +1070,7 @@
 
         @Override
         void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
-            throw new UnsupportedOperationException("IPv6 Keepalive is not supported yet");
+            throw new UnsupportedOperationException("IPv6 TCP Keepalive is not supported yet");
         }
     }
 
@@ -997,7 +1080,7 @@
     @GuardedBy("this")
     private ArrayList<Ra> mRas = new ArrayList<>();
     @GuardedBy("this")
-    private SparseArray<TcpKeepaliveAck> mKeepaliveAcks = new SparseArray<>();
+    private SparseArray<KeepalivePacket> mKeepalivePackets = new SparseArray<>();
 
     // There is always some marginal benefit to updating the installed APF program when an RA is
     // seen because we can extend the program's lifetime slightly, but there is some cost to
@@ -1171,9 +1254,12 @@
                 gen.addJumpIfR0Equals(broadcastAddr, mCountAndDropLabel);
             }
 
-            // If any keepalive filter matches, drop
+            // If any TCP keepalive filter matches, drop
             generateV4KeepaliveFilters(gen);
 
+            // If any NAT-T keepalive filter matches, drop
+            generateV4NattKeepaliveFilters(gen);
+
             // Otherwise, this is an IPv4 unicast, pass
             // If L2 broadcast packet, drop.
             // TODO: can we invert this condition to fall through to the common pass case below?
@@ -1184,6 +1270,7 @@
             gen.addJump(mCountAndDropLabel);
         } else {
             generateV4KeepaliveFilters(gen);
+            generateV4NattKeepaliveFilters(gen);
         }
 
         // Otherwise, pass
@@ -1191,25 +1278,36 @@
         gen.addJump(mCountAndPassLabel);
     }
 
-    private void generateV4KeepaliveFilters(ApfGenerator gen) throws IllegalInstructionException {
-        final String skipV4KeepaliveFilter = "skip_v4_keepalive_filter";
-        final boolean haveV4KeepaliveAcks = NetworkStackUtils.any(mKeepaliveAcks,
-                ack -> ack instanceof TcpKeepaliveAckV4);
+    private void generateKeepaliveFilters(ApfGenerator gen, Class<?> filterType, int proto,
+            int offset, String label) throws IllegalInstructionException {
+        final boolean haveKeepaliveResponses = NetworkStackUtils.any(mKeepalivePackets,
+                ack -> filterType.isInstance(ack));
 
-        // If no keepalive acks
-        if (!haveV4KeepaliveAcks) return;
+        // If no keepalive packets of this type
+        if (!haveKeepaliveResponses) return;
 
-        // If not tcp, skip keepalive filters
-        gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
-        gen.addJumpIfR0NotEquals(IPPROTO_TCP, skipV4KeepaliveFilter);
+        // If not the right proto, skip keepalive filters
+        gen.addLoad8(Register.R0, offset);
+        gen.addJumpIfR0NotEquals(proto, label);
 
-        // Drop IPv4 Keepalive acks
-        for (int i = 0; i < mKeepaliveAcks.size(); ++i) {
-            final TcpKeepaliveAck ack = mKeepaliveAcks.valueAt(i);
-            if (ack instanceof TcpKeepaliveAckV4) ack.generateFilterLocked(gen);
+        // Drop Keepalive responses
+        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
+            final KeepalivePacket response = mKeepalivePackets.valueAt(i);
+            if (filterType.isInstance(response)) response.generateFilterLocked(gen);
         }
 
-        gen.defineLabel(skipV4KeepaliveFilter);
+        gen.defineLabel(label);
+    }
+
+    private void generateV4KeepaliveFilters(ApfGenerator gen) throws IllegalInstructionException {
+        generateKeepaliveFilters(gen, TcpKeepaliveAckV4.class, IPPROTO_TCP, IPV4_PROTOCOL_OFFSET,
+                "skip_v4_keepalive_filter");
+    }
+
+    private void generateV4NattKeepaliveFilters(ApfGenerator gen)
+            throws IllegalInstructionException {
+        generateKeepaliveFilters(gen, NattKeepaliveResponse.class,
+                IPPROTO_UDP, IPV4_PROTOCOL_OFFSET, "skip_v4_nattkeepalive_filter");
     }
 
     /**
@@ -1294,24 +1392,8 @@
     }
 
     private void generateV6KeepaliveFilters(ApfGenerator gen) throws IllegalInstructionException {
-        final String skipV6KeepaliveFilter = "skip_v6_keepalive_filter";
-        final boolean haveV6KeepaliveAcks = NetworkStackUtils.any(mKeepaliveAcks,
-                ack -> ack instanceof TcpKeepaliveAckV6);
-
-        // If no keepalive acks
-        if (!haveV6KeepaliveAcks) return;
-
-        // If not tcp, skip keepalive filters
-        gen.addLoad8(Register.R0, IPV6_NEXT_HEADER_OFFSET);
-        gen.addJumpIfR0NotEquals(IPPROTO_TCP, skipV6KeepaliveFilter);
-
-        // Drop IPv6 Keepalive acks
-        for (int i = 0; i < mKeepaliveAcks.size(); ++i) {
-            final TcpKeepaliveAck ack = mKeepaliveAcks.valueAt(i);
-            if (ack instanceof TcpKeepaliveAckV6) ack.generateFilterLocked(gen);
-        }
-
-        gen.defineLabel(skipV6KeepaliveFilter);
+        generateKeepaliveFilters(gen, TcpKeepaliveAckV6.class, IPPROTO_TCP, IPV6_NEXT_HEADER_OFFSET,
+                "skip_v6_keepalive_filter");
     }
 
     /**
@@ -1701,26 +1783,34 @@
     public synchronized void addTcpKeepalivePacketFilter(final int slot,
             final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
         log("Adding keepalive ack(" + slot + ")");
-        if (null != mKeepaliveAcks.get(slot)) {
+        if (null != mKeepalivePackets.get(slot)) {
             throw new IllegalArgumentException("Keepalive slot " + slot + " is occupied");
         }
         final int ipVersion = sentKeepalivePacket.srcAddress.length == 4 ? 4 : 6;
-        mKeepaliveAcks.put(slot, (ipVersion == 4)
+        mKeepalivePackets.put(slot, (ipVersion == 4)
                 ? new TcpKeepaliveAckV4(sentKeepalivePacket)
                 : new TcpKeepaliveAckV6(sentKeepalivePacket));
         installNewProgramLocked();
     }
 
     /**
-     * Add NATT keepalive packet filter.
-     * This will add a filter to drop NATT keepalive packet which is passed as an argument.
+     * Add NAT-T keepalive packet filter.
+     * This will add a filter to drop NAT-T keepalive packet which is passed as an argument.
      *
      * @param slot The index used to access the filter.
      * @param sentKeepalivePacket The attributes of the sent keepalive packet.
      */
     public synchronized void addNattKeepalivePacketFilter(final int slot,
             final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
-        Log.e(TAG, "APF add NATT keepalive filter is not implemented");
+        log("Adding NAT-T keepalive packet(" + slot + ")");
+        if (null != mKeepalivePackets.get(slot)) {
+            throw new IllegalArgumentException("NAT-T Keepalive slot " + slot + " is occupied");
+        }
+        if (sentKeepalivePacket.srcAddress.length != 4) {
+            throw new IllegalArgumentException("NAT-T keepalive is only supported on IPv4");
+        }
+        mKeepalivePackets.put(slot, new NattKeepaliveResponse(sentKeepalivePacket));
+        installNewProgramLocked();
     }
 
     /**
@@ -1729,7 +1819,8 @@
      * @param slot The index used to access the filter.
      */
     public synchronized void removeKeepalivePacketFilter(int slot) {
-        mKeepaliveAcks.remove(slot);
+        log("Removing keepalive packet(" + slot + ")");
+        mKeepalivePackets.remove(slot);
         installNewProgramLocked();
     }
 
@@ -1785,14 +1876,29 @@
         }
         pw.decreaseIndent();
 
-        pw.println("Keepalive filters:");
+        pw.println("TCP Keepalive filters:");
         pw.increaseIndent();
-        for (int i = 0; i < mKeepaliveAcks.size(); ++i) {
-            final TcpKeepaliveAck keepaliveAck = mKeepaliveAcks.valueAt(i);
-            pw.print("Slot ");
-            pw.print(mKeepaliveAcks.keyAt(i));
-            pw.print(" : ");
-            pw.println(keepaliveAck);
+        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
+            final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i);
+            if (keepalivePacket instanceof TcpKeepaliveAck) {
+                pw.print("Slot ");
+                pw.print(mKeepalivePackets.keyAt(i));
+                pw.print(": ");
+                pw.println(keepalivePacket);
+            }
+        }
+        pw.decreaseIndent();
+
+        pw.println("NAT-T Keepalive filters:");
+        pw.increaseIndent();
+        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
+            final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i);
+            if (keepalivePacket instanceof NattKeepaliveResponse) {
+                pw.print("Slot ");
+                pw.print(mKeepalivePackets.keyAt(i));
+                pw.print(": ");
+                pw.println(keepalivePacket);
+            }
         }
         pw.decreaseIndent();
 
@@ -1858,4 +1964,18 @@
                 + (uint8(bytes[2]) << 8)
                 + (uint8(bytes[3]));
     }
+
+    private static byte[] concatArrays(final byte[]... arr) {
+        int size = 0;
+        for (byte[] a : arr) {
+            size += a.length;
+        }
+        final byte[] result = new byte[size];
+        int offset = 0;
+        for (byte[] a : arr) {
+            System.arraycopy(a, 0, result, offset, a.length);
+            offset += a.length;
+        }
+        return result;
+    }
 }
diff --git a/packages/NetworkStack/src/android/net/util/NetworkStackUtils.java b/packages/NetworkStack/src/android/net/util/NetworkStackUtils.java
index 2934e1c..9bf1b96 100644
--- a/packages/NetworkStack/src/android/net/util/NetworkStackUtils.java
+++ b/packages/NetworkStack/src/android/net/util/NetworkStackUtils.java
@@ -24,6 +24,8 @@
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.net.SocketException;
 import java.util.List;
 import java.util.function.Predicate;
@@ -228,4 +230,13 @@
 
     private static native void addArpEntry(byte[] ethAddr, byte[] netAddr, String ifname,
             FileDescriptor fd) throws IOException;
+
+    /**
+     * Return IP address and port in a string format.
+     */
+    public static String addressAndPortToString(InetAddress address, int port) {
+        return String.format(
+                (address instanceof Inet6Address) ? "[%s]:%d" : "%s:%d",
+                        address.getHostAddress(), port);
+    }
 }
diff --git a/packages/NetworkStack/src/com/android/networkstack/util/DnsUtils.java b/packages/NetworkStack/src/com/android/networkstack/util/DnsUtils.java
index 85f94e1..4767d55 100644
--- a/packages/NetworkStack/src/com/android/networkstack/util/DnsUtils.java
+++ b/packages/NetworkStack/src/com/android/networkstack/util/DnsUtils.java
@@ -55,12 +55,23 @@
             throws UnknownHostException {
         final List<InetAddress> result = new ArrayList<InetAddress>();
 
-        result.addAll(Arrays.asList(
-                getAllByName(dnsResolver, network, host, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
-                timeout)));
-        result.addAll(Arrays.asList(
-                getAllByName(dnsResolver, network, host, TYPE_A, FLAG_NO_CACHE_LOOKUP,
-                timeout)));
+        try {
+            result.addAll(Arrays.asList(
+                    getAllByName(dnsResolver, network, host, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+                    timeout)));
+        } catch (UnknownHostException e) {
+            // Might happen if the host is v4-only, still need to query TYPE_A
+        }
+        try {
+            result.addAll(Arrays.asList(
+                    getAllByName(dnsResolver, network, host, TYPE_A, FLAG_NO_CACHE_LOOKUP,
+                    timeout)));
+        } catch (UnknownHostException e) {
+            // Might happen if the host is v6-only, still need to return AAAA answers
+        }
+        if (result.size() == 0) {
+            throw new UnknownHostException(host);
+        }
         return result.toArray(new InetAddress[0]);
     }
 
diff --git a/packages/NetworkStack/tests/src/android/net/apf/ApfTest.java b/packages/NetworkStack/tests/src/android/net/apf/ApfTest.java
index 93ab3be..8f2b968 100644
--- a/packages/NetworkStack/tests/src/android/net/apf/ApfTest.java
+++ b/packages/NetworkStack/tests/src/android/net/apf/ApfTest.java
@@ -40,6 +40,7 @@
 import android.content.Context;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
+import android.net.NattKeepalivePacketDataParcelable;
 import android.net.TcpKeepalivePacketDataParcelable;
 import android.net.apf.ApfFilter.ApfConfiguration;
 import android.net.apf.ApfGenerator.IllegalInstructionException;
@@ -998,47 +999,54 @@
         }
     }
 
-    private static final int ETH_HEADER_LEN = 14;
-    private static final int ETH_DEST_ADDR_OFFSET = 0;
-    private static final int ETH_ETHERTYPE_OFFSET = 12;
+    private static final int ETH_HEADER_LEN               = 14;
+    private static final int ETH_DEST_ADDR_OFFSET         = 0;
+    private static final int ETH_ETHERTYPE_OFFSET         = 12;
     private static final byte[] ETH_BROADCAST_MAC_ADDRESS =
             {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
 
-    private static final int IPV4_HEADER_LEN = 20;
-    private static final int IPV4_VERSION_IHL_OFFSET = ETH_HEADER_LEN + 0;
+    private static final int IPV4_HEADER_LEN          = 20;
+    private static final int IPV4_VERSION_IHL_OFFSET  = ETH_HEADER_LEN + 0;
     private static final int IPV4_TOTAL_LENGTH_OFFSET = ETH_HEADER_LEN + 2;
-    private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9;
-    private static final int IPV4_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 12;
-    private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;
-    private static final int IPV4_TCP_HEADER_LEN = 20;
-    private static final int IPV4_TCP_HEADER_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_LEN;
-    private static final int IPV4_TCP_SRC_PORT_OFFSET = IPV4_TCP_HEADER_OFFSET + 0;
-    private static final int IPV4_TCP_DEST_PORT_OFFSET = IPV4_TCP_HEADER_OFFSET + 2;
-    private static final int IPV4_TCP_SEQ_NUM_OFFSET = IPV4_TCP_HEADER_OFFSET + 4;
-    private static final int IPV4_TCP_ACK_NUM_OFFSET = IPV4_TCP_HEADER_OFFSET + 8;
+    private static final int IPV4_PROTOCOL_OFFSET     = ETH_HEADER_LEN + 9;
+    private static final int IPV4_SRC_ADDR_OFFSET     = ETH_HEADER_LEN + 12;
+    private static final int IPV4_DEST_ADDR_OFFSET    = ETH_HEADER_LEN + 16;
+
+    private static final int IPV4_TCP_HEADER_LEN           = 20;
+    private static final int IPV4_TCP_HEADER_OFFSET        = ETH_HEADER_LEN + IPV4_HEADER_LEN;
+    private static final int IPV4_TCP_SRC_PORT_OFFSET      = IPV4_TCP_HEADER_OFFSET + 0;
+    private static final int IPV4_TCP_DEST_PORT_OFFSET     = IPV4_TCP_HEADER_OFFSET + 2;
+    private static final int IPV4_TCP_SEQ_NUM_OFFSET       = IPV4_TCP_HEADER_OFFSET + 4;
+    private static final int IPV4_TCP_ACK_NUM_OFFSET       = IPV4_TCP_HEADER_OFFSET + 8;
     private static final int IPV4_TCP_HEADER_LENGTH_OFFSET = IPV4_TCP_HEADER_OFFSET + 12;
-    private static final int IPV4_TCP_HEADER_FLAG_OFFSET = IPV4_TCP_HEADER_OFFSET + 13;
+    private static final int IPV4_TCP_HEADER_FLAG_OFFSET   = IPV4_TCP_HEADER_OFFSET + 13;
+
+    private static final int IPV4_UDP_HEADER_OFFSET    = ETH_HEADER_LEN + IPV4_HEADER_LEN;;
+    private static final int IPV4_UDP_SRC_PORT_OFFSET  = IPV4_UDP_HEADER_OFFSET + 0;
+    private static final int IPV4_UDP_DEST_PORT_OFFSET = IPV4_UDP_HEADER_OFFSET + 2;
+    private static final int IPV4_UDP_LENGTH_OFFSET    = IPV4_UDP_HEADER_OFFSET + 4;
+    private static final int IPV4_UDP_PAYLOAD_OFFSET   = IPV4_UDP_HEADER_OFFSET + 8;
     private static final byte[] IPV4_BROADCAST_ADDRESS =
             {(byte) 255, (byte) 255, (byte) 255, (byte) 255};
 
-    private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6;
-    private static final int IPV6_HEADER_LEN = 40;
-    private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8;
-    private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24;
-    private static final int IPV6_TCP_HEADER_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
-    private static final int IPV6_TCP_SRC_PORT_OFFSET = IPV6_TCP_HEADER_OFFSET + 0;
-    private static final int IPV6_TCP_DEST_PORT_OFFSET = IPV6_TCP_HEADER_OFFSET + 2;
-    private static final int IPV6_TCP_SEQ_NUM_OFFSET = IPV6_TCP_HEADER_OFFSET + 4;
-    private static final int IPV6_TCP_ACK_NUM_OFFSET = IPV6_TCP_HEADER_OFFSET + 8;
+    private static final int IPV6_HEADER_LEN             = 40;
+    private static final int IPV6_NEXT_HEADER_OFFSET     = ETH_HEADER_LEN + 6;
+    private static final int IPV6_SRC_ADDR_OFFSET        = ETH_HEADER_LEN + 8;
+    private static final int IPV6_DEST_ADDR_OFFSET       = ETH_HEADER_LEN + 24;
+    private static final int IPV6_TCP_HEADER_OFFSET      = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+    private static final int IPV6_TCP_SRC_PORT_OFFSET    = IPV6_TCP_HEADER_OFFSET + 0;
+    private static final int IPV6_TCP_DEST_PORT_OFFSET   = IPV6_TCP_HEADER_OFFSET + 2;
+    private static final int IPV6_TCP_SEQ_NUM_OFFSET     = IPV6_TCP_HEADER_OFFSET + 4;
+    private static final int IPV6_TCP_ACK_NUM_OFFSET     = IPV6_TCP_HEADER_OFFSET + 8;
     // The IPv6 all nodes address ff02::1
-    private static final byte[] IPV6_ALL_NODES_ADDRESS =
+    private static final byte[] IPV6_ALL_NODES_ADDRESS   =
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
     private static final byte[] IPV6_ALL_ROUTERS_ADDRESS =
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
 
-    private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
-    private static final int ICMP6_ROUTER_SOLICITATION = 133;
-    private static final int ICMP6_ROUTER_ADVERTISEMENT = 134;
+    private static final int ICMP6_TYPE_OFFSET           = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+    private static final int ICMP6_ROUTER_SOLICITATION   = 133;
+    private static final int ICMP6_ROUTER_ADVERTISEMENT  = 134;
     private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
     private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
 
@@ -1050,9 +1058,9 @@
     private static final int ICMP6_RA_OPTION_OFFSET =
             ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
 
-    private static final int ICMP6_PREFIX_OPTION_TYPE = 3;
-    private static final int ICMP6_PREFIX_OPTION_LEN = 32;
-    private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4;
+    private static final int ICMP6_PREFIX_OPTION_TYPE                      = 3;
+    private static final int ICMP6_PREFIX_OPTION_LEN                       = 32;
+    private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET     = 4;
     private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8;
 
     // From RFC6106: Recursive DNS Server option
@@ -1063,17 +1071,17 @@
     // From RFC4191: Route Information option
     private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
     // Above three options all have the same format:
-    private static final int ICMP6_4_BYTE_OPTION_LEN = 8;
+    private static final int ICMP6_4_BYTE_OPTION_LEN      = 8;
     private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
-    private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
+    private static final int ICMP6_4_BYTE_LIFETIME_LEN    = 4;
 
-    private static final int UDP_HEADER_LEN = 8;
+    private static final int UDP_HEADER_LEN              = 8;
     private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 22;
 
-    private static final int DHCP_CLIENT_PORT = 68;
+    private static final int DHCP_CLIENT_PORT       = 68;
     private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 48;
 
-    private static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN;
+    private static final int ARP_HEADER_OFFSET          = ETH_HEADER_LEN;
     private static final byte[] ARP_IPV4_REQUEST_HEADER = {
             0, 1, // Hardware type: Ethernet (1)
             8, 0, // Protocol type: IP (0x0800)
@@ -1714,6 +1722,83 @@
         return packet.array();
     }
 
+    @Test
+    public void testApfFilterNattKeepalivePacket() throws Exception {
+        final MockIpClientCallback cb = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog);
+        byte[] program;
+        final int srcPort = 1024;
+        final int dstPort = 4500;
+        final int slot1 = 1;
+        // NAT-T keepalive
+        final byte[] kaPayload = {(byte) 0xff};
+        final byte[] nonKaPayload = {(byte) 0xfe};
+
+        // src: 10.0.0.5, port: 1024
+        // dst: 10.0.0.6, port: 4500
+        InetAddress srcAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_SRC_ADDR);
+        InetAddress dstAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_DST_ADDR);
+
+        final NattKeepalivePacketDataParcelable parcel = new NattKeepalivePacketDataParcelable();
+        parcel.srcAddress = srcAddr.getAddress();
+        parcel.srcPort = srcPort;
+        parcel.dstAddress = dstAddr.getAddress();
+        parcel.dstPort = dstPort;
+
+        apfFilter.addNattKeepalivePacketFilter(slot1, parcel);
+        program = cb.getApfProgram();
+
+        // Verify IPv4 keepalive packet is dropped
+        // src: 10.0.0.6, port: 4500
+        // dst: 10.0.0.5, port: 1024
+        byte[] pkt = ipv4UdpPacket(IPV4_KEEPALIVE_DST_ADDR,
+                    IPV4_KEEPALIVE_SRC_ADDR, dstPort, srcPort, 1 /* dataLength */);
+        System.arraycopy(kaPayload, 0, pkt, IPV4_UDP_PAYLOAD_OFFSET, kaPayload.length);
+        assertDrop(program, pkt);
+
+        // Verify a packet with payload length 1 byte but it is not 0xff will pass the filter.
+        System.arraycopy(nonKaPayload, 0, pkt, IPV4_UDP_PAYLOAD_OFFSET, nonKaPayload.length);
+        assertPass(program, pkt);
+
+        // Verify IPv4 non-keepalive response packet from the same source address is passed
+        assertPass(program,
+                ipv4UdpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, 10 /* dataLength */));
+
+        // Verify IPv4 non-keepalive response packet from other source address is passed
+        assertPass(program,
+                ipv4UdpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, 10 /* dataLength */));
+
+        apfFilter.removeKeepalivePacketFilter(slot1);
+        apfFilter.shutdown();
+    }
+
+    private static byte[] ipv4UdpPacket(byte[] sip, byte[] dip, int sport,
+            int dport, int dataLength) {
+        final int totalLength = dataLength + IPV4_HEADER_LEN + UDP_HEADER_LEN;
+        final int udpLength = UDP_HEADER_LEN + dataLength;
+        ByteBuffer packet = ByteBuffer.wrap(new byte[totalLength + ETH_HEADER_LEN]);
+
+        // ether type
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IP);
+
+        // IPv4 header
+        packet.put(IPV4_VERSION_IHL_OFFSET, (byte) 0x45);
+        packet.putShort(IPV4_TOTAL_LENGTH_OFFSET, (short) totalLength);
+        packet.put(IPV4_PROTOCOL_OFFSET, (byte) IPPROTO_UDP);
+        put(packet, IPV4_SRC_ADDR_OFFSET, sip);
+        put(packet, IPV4_DEST_ADDR_OFFSET, dip);
+        packet.putShort(IPV4_UDP_SRC_PORT_OFFSET, (short) sport);
+        packet.putShort(IPV4_UDP_DEST_PORT_OFFSET, (short) dport);
+        packet.putShort(IPV4_UDP_LENGTH_OFFSET, (short) udpLength);
+
+        return packet.array();
+    }
+
     // Verify that the last program pushed to the IpClient.Callback properly filters the
     // given packet for the given lifetime.
     private void verifyRaLifetime(byte[] program, ByteBuffer packet, int lifetime) {
diff --git a/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
index abfb9c8..2618675 100644
--- a/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -297,9 +297,10 @@
         setOtherFallbackUrls(TEST_OTHER_FALLBACK_URL);
         setFallbackSpecs(null); // Test with no fallback spec by default
         when(mRandom.nextInt()).thenReturn(0);
-
+        // DNS probe timeout should not be defined more than half of HANDLER_TIMEOUT_MS. Otherwise,
+        // it will fail the test because of timeout expired for querying AAAA and A sequentially.
         when(mResources.getInteger(eq(R.integer.config_captive_portal_dns_probe_timeout)))
-                .thenReturn(500);
+                .thenReturn(200);
 
         doAnswer((invocation) -> {
             URL url = invocation.getArgument(0);
diff --git a/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java b/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java
index 2bfcc91..f30de13 100644
--- a/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java
@@ -175,9 +175,7 @@
 
     private long getUsageLevel(NetworkTemplate template, long start, long end) {
         try {
-            final Bucket bucket = mNetworkStatsManager.querySummaryForDevice(
-                    getNetworkType(template), getActiveSubscriberId(),
-                    start, end);
+            final Bucket bucket = mNetworkStatsManager.querySummaryForDevice(template, start, end);
             if (bucket != null) {
                 return bucket.getRxBytes() + bucket.getTxBytes();
             }
diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartDataLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartDataLoader.java
index ec5a0b5..787dc55 100644
--- a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartDataLoader.java
+++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartDataLoader.java
@@ -37,14 +37,14 @@
 
     private NetworkCycleChartDataLoader(Builder builder) {
         super(builder);
-        mData = new ArrayList<NetworkCycleChartData>();
+        mData = new ArrayList<>();
     }
 
     @Override
     void recordUsage(long start, long end) {
         try {
             final NetworkStats.Bucket bucket = mNetworkStatsManager.querySummaryForDevice(
-                mNetworkType, mSubId, start, end);
+                    mNetworkTemplate, start, end);
             final long total = bucket == null ? 0L : bucket.getRxBytes() + bucket.getTxBytes();
             if (total > 0L) {
                 final NetworkCycleChartData.Builder builder = new NetworkCycleChartData.Builder();
@@ -81,7 +81,7 @@
             long usage = 0L;
             try {
                 final NetworkStats.Bucket bucket = mNetworkStatsManager.querySummaryForDevice(
-                    mNetworkType, mSubId, bucketStart, bucketEnd);
+                        mNetworkTemplate, bucketStart, bucketEnd);
                 if (bucket != null) {
                     usage = bucket.getRxBytes() + bucket.getTxBytes();
                 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUidLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUidLoader.java
index bd9a636..43c05b8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUidLoader.java
+++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUidLoader.java
@@ -23,11 +23,11 @@
 import android.content.Context;
 import android.util.Log;
 
+import androidx.annotation.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import androidx.annotation.VisibleForTesting;
-
 /**
  * Loader for network data usage history. It returns a list of usage data per billing cycle for the
  * specific Uid(s).
@@ -44,7 +44,7 @@
         super(builder);
         mUids = builder.mUids;
         mRetrieveDetail = builder.mRetrieveDetail;
-        mData = new ArrayList<NetworkCycleDataForUid>();
+        mData = new ArrayList<>();
     }
 
     @Override
@@ -54,7 +54,7 @@
             long totalForeground = 0L;
             for (int uid : mUids) {
                 final NetworkStats stats = mNetworkStatsManager.queryDetailsForUid(
-                    mNetworkType, mSubId, start, end, uid);
+                        mNetworkTemplate, start, end, uid);
                 final long usage = getTotalUsage(stats);
                 if (usage > 0L) {
                     totalUsage += usage;
@@ -100,7 +100,7 @@
 
     private long getForegroundUsage(long start, long end, int uid) {
         final NetworkStats stats = mNetworkStatsManager.queryDetailsForUidTagState(
-            mNetworkType, mSubId, start, end, uid, TAG_NONE, STATE_FOREGROUND);
+                mNetworkTemplate, start, end, uid, TAG_NONE, STATE_FOREGROUND);
         return getTotalUsage(stats);
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java
index dd6d563..3e95b01 100644
--- a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java
+++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java
@@ -49,18 +49,14 @@
 public abstract class NetworkCycleDataLoader<D> extends AsyncTaskLoader<D> {
     private static final String TAG = "NetworkCycleDataLoader";
     protected final NetworkStatsManager mNetworkStatsManager;
-    protected final String mSubId;
-    protected final int mNetworkType;
+    protected final NetworkTemplate mNetworkTemplate;
     private final NetworkPolicy mPolicy;
-    private final NetworkTemplate mNetworkTemplate;
     private final ArrayList<Long> mCycles;
     @VisibleForTesting
     final INetworkStatsService mNetworkStatsService;
 
     protected NetworkCycleDataLoader(Builder<?> builder) {
         super(builder.mContext);
-        mSubId = builder.mSubId;
-        mNetworkType = builder.mNetworkType;
         mNetworkTemplate = builder.mNetworkTemplate;
         mCycles = builder.mCycles;
         mNetworkStatsManager = (NetworkStatsManager)
@@ -180,8 +176,6 @@
 
     public static abstract class Builder<T extends NetworkCycleDataLoader> {
         private final Context mContext;
-        private String mSubId;
-        private int mNetworkType;
         private NetworkTemplate mNetworkTemplate;
         private ArrayList<Long> mCycles;
 
@@ -189,14 +183,8 @@
             mContext = context;
         }
 
-        public Builder<T> setSubscriberId(String subId) {
-            mSubId = subId;
-            return this;
-        }
-
         public Builder<T> setNetworkTemplate(NetworkTemplate template) {
             mNetworkTemplate = template;
-            mNetworkType = DataUsageController.getNetworkType(template);
             return this;
         }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkStatsSummaryLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkStatsSummaryLoader.java
index 34e6097..ed093629 100644
--- a/packages/SettingsLib/src/com/android/settingslib/net/NetworkStatsSummaryLoader.java
+++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkStatsSummaryLoader.java
@@ -16,9 +16,10 @@
 
 package com.android.settingslib.net;
 
-import android.app.usage.NetworkStatsManager;
 import android.app.usage.NetworkStats;
+import android.app.usage.NetworkStatsManager;
 import android.content.Context;
+import android.net.NetworkTemplate;
 import android.os.RemoteException;
 import android.util.Log;
 
@@ -33,15 +34,13 @@
     private final NetworkStatsManager mNetworkStatsManager;
     private final long mStart;
     private final long mEnd;
-    private final String mSubId;
-    private final int mNetworkType;
+    private final NetworkTemplate mNetworkTemplate;
 
     private NetworkStatsSummaryLoader(Builder builder) {
         super(builder.mContext);
         mStart = builder.mStart;
         mEnd = builder.mEnd;
-        mSubId = builder.mSubId;
-        mNetworkType = builder.mNetworkType;
+        mNetworkTemplate = builder.mNetworkTemplate;
         mNetworkStatsManager = (NetworkStatsManager)
                 builder.mContext.getSystemService(Context.NETWORK_STATS_SERVICE);
     }
@@ -55,7 +54,7 @@
     @Override
     public NetworkStats loadInBackground() {
         try {
-            return mNetworkStatsManager.querySummary(mNetworkType, mSubId, mStart, mEnd);
+            return mNetworkStatsManager.querySummary(mNetworkTemplate, mStart, mEnd);
         } catch (RemoteException e) {
             Log.e(TAG, "Exception querying network detail.", e);
             return null;
@@ -78,8 +77,7 @@
         private final Context mContext;
         private long mStart;
         private long mEnd;
-        private String mSubId;
-        private int mNetworkType;
+        private NetworkTemplate mNetworkTemplate;
 
         public Builder(Context context) {
             mContext = context;
@@ -95,13 +93,11 @@
             return this;
         }
 
-        public Builder setSubscriberId(String subId) {
-            mSubId = subId;
-            return this;
-        }
-
-        public Builder setNetworkType(int networkType) {
-            mNetworkType = networkType;
+        /**
+         * Set {@link NetworkTemplate} for builder
+         */
+        public Builder setNetworkTemplate(NetworkTemplate template) {
+            mNetworkTemplate = template;
             return this;
         }
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/DataUsageControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/DataUsageControllerTest.java
index a28bb6c..3da5e76 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/DataUsageControllerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/DataUsageControllerTest.java
@@ -31,7 +31,6 @@
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
-import android.net.ConnectivityManager;
 import android.net.INetworkStatsSession;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
@@ -52,6 +51,7 @@
 public class DataUsageControllerTest {
 
     private static final String SUB_ID = "Test Subscriber";
+    private static final String SUB_ID_2 = "Test Subscriber 2";
 
     @Mock
     private INetworkStatsSession mSession;
@@ -63,6 +63,9 @@
     private NetworkStatsManager mNetworkStatsManager;
     @Mock
     private Context mContext;
+    private NetworkTemplate mNetworkTemplate;
+    private NetworkTemplate mNetworkTemplate2;
+    private NetworkTemplate mWifiNetworkTemplate;
 
     private DataUsageController mController;
     private NetworkStatsHistory mNetworkStatsHistory;
@@ -83,24 +86,27 @@
                 .when(mSession).getHistoryForNetwork(any(NetworkTemplate.class), anyInt());
         ShadowSubscriptionManager.setDefaultDataSubscriptionId(mDefaultSubscriptionId);
         doReturn(SUB_ID).when(mTelephonyManager).getSubscriberId();
+
+        mNetworkTemplate = NetworkTemplate.buildTemplateMobileAll(SUB_ID);
+        mNetworkTemplate2 = NetworkTemplate.buildTemplateMobileAll(SUB_ID_2);
+        mWifiNetworkTemplate = NetworkTemplate.buildTemplateWifiWildcard();
     }
 
     @Test
     public void getHistoricalUsageLevel_shouldQuerySummaryForDevice() throws Exception {
+        mController.getHistoricalUsageLevel(mWifiNetworkTemplate);
 
-        mController.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard());
-
-        verify(mNetworkStatsManager).querySummaryForDevice(eq(ConnectivityManager.TYPE_WIFI),
-                eq(SUB_ID), eq(0L) /* startTime */, anyLong() /* endTime */);
+        verify(mNetworkStatsManager).querySummaryForDevice(eq(mWifiNetworkTemplate),
+                eq(0L) /* startTime */, anyLong() /* endTime */);
     }
 
     @Test
     public void getHistoricalUsageLevel_noUsageData_shouldReturn0() throws Exception {
-        when(mNetworkStatsManager.querySummaryForDevice(eq(ConnectivityManager.TYPE_WIFI),
-                eq(SUB_ID), eq(0L) /* startTime */, anyLong() /* endTime */))
+        when(mNetworkStatsManager.querySummaryForDevice(eq(mWifiNetworkTemplate),
+                eq(0L) /* startTime */, anyLong() /* endTime */))
                 .thenReturn(mock(NetworkStats.Bucket.class));
-        assertThat(mController.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()))
-            .isEqualTo(0L);
+        assertThat(mController.getHistoricalUsageLevel(mWifiNetworkTemplate))
+                .isEqualTo(0L);
     }
 
     @Test
@@ -110,10 +116,10 @@
         final NetworkStats.Bucket bucket = mock(NetworkStats.Bucket.class);
         when(bucket.getRxBytes()).thenReturn(receivedBytes);
         when(bucket.getTxBytes()).thenReturn(transmittedBytes);
-        when(mNetworkStatsManager.querySummaryForDevice(eq(ConnectivityManager.TYPE_WIFI),
-                eq(SUB_ID), eq(0L) /* startTime */, anyLong() /* endTime */)).thenReturn(bucket);
+        when(mNetworkStatsManager.querySummaryForDevice(eq(mWifiNetworkTemplate),
+                eq(0L) /* startTime */, anyLong() /* endTime */)).thenReturn(bucket);
 
-        assertThat(mController.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()))
+        assertThat(mController.getHistoricalUsageLevel(mWifiNetworkTemplate))
                 .isEqualTo(receivedBytes + transmittedBytes);
     }
 
@@ -126,9 +132,8 @@
         final NetworkStats.Bucket defaultSubscriberBucket = mock(NetworkStats.Bucket.class);
         when(defaultSubscriberBucket.getRxBytes()).thenReturn(defaultSubRx);
         when(defaultSubscriberBucket.getTxBytes()).thenReturn(defaultSubTx);
-        when(mNetworkStatsManager.querySummaryForDevice(eq(ConnectivityManager.TYPE_MOBILE),
-                eq(SUB_ID), eq(0L)/* startTime */, anyLong() /* endTime */)).thenReturn(
-                defaultSubscriberBucket);
+        when(mNetworkStatsManager.querySummaryForDevice(eq(mNetworkTemplate), eq(0L)/* startTime */,
+                anyLong() /* endTime */)).thenReturn(defaultSubscriberBucket);
 
         // Now setup a stats bucket for a different, non-default subscription / subscriber ID.
         final long nonDefaultSubRx = 7654321L;
@@ -137,25 +142,21 @@
         when(nonDefaultSubscriberBucket.getRxBytes()).thenReturn(nonDefaultSubRx);
         when(nonDefaultSubscriberBucket.getTxBytes()).thenReturn(nonDefaultSubTx);
         final int explicitSubscriptionId = 55;
-        final String subscriberId2 = "Test Subscriber 2";
-        when(mNetworkStatsManager.querySummaryForDevice(eq(ConnectivityManager.TYPE_MOBILE),
-                eq(subscriberId2), eq(0L)/* startTime */, anyLong() /* endTime */)).thenReturn(
+        when(mNetworkStatsManager.querySummaryForDevice(eq(mNetworkTemplate2),
+                eq(0L)/* startTime */, anyLong() /* endTime */)).thenReturn(
                 nonDefaultSubscriberBucket);
-        doReturn(subscriberId2).when(mTelephonyManager).getSubscriberId();
+        doReturn(SUB_ID_2).when(mTelephonyManager).getSubscriberId();
 
         // Now verify that when we're asking for stats on the non-default subscription, we get
         // the data back for that subscription and *not* the default one.
         mController.setSubscriptionId(explicitSubscriptionId);
 
-        assertThat(mController.getHistoricalUsageLevel(
-                NetworkTemplate.buildTemplateMobileAll(subscriberId2))).isEqualTo(
+        assertThat(mController.getHistoricalUsageLevel(mNetworkTemplate2)).isEqualTo(
                 nonDefaultSubRx + nonDefaultSubTx);
-
-        verify(mTelephonyManager).createForSubscriptionId(explicitSubscriptionId);
     }
 
     @Test
-    public void getTelephonyManager_shouldCreateWithExplicitSubId() throws Exception {
+    public void getTelephonyManager_shouldCreateWithExplicitSubId() {
         int explicitSubId = 1;
         TelephonyManager tmForSub1 = new TelephonyManager(mContext, explicitSubId);
         when(mTelephonyManager.createForSubscriptionId(eq(explicitSubId))).thenReturn(tmForSub1);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleChartDataLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleChartDataLoaderTest.java
index 011f234..c3e1613 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleChartDataLoaderTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleChartDataLoaderTest.java
@@ -21,9 +21,9 @@
 
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
-import android.net.ConnectivityManager;
 import android.net.NetworkPolicy;
 import android.net.NetworkPolicyManager;
+import android.net.NetworkTemplate;
 import android.os.RemoteException;
 import android.text.format.DateUtils;
 
@@ -43,6 +43,8 @@
     private NetworkPolicyManager mNetworkPolicyManager;
     @Mock
     private Context mContext;
+    @Mock
+    private NetworkTemplate mNetworkTemplate;
 
     private NetworkCycleChartDataLoader mLoader;
 
@@ -60,13 +62,12 @@
     public void recordUsage_shouldQueryNetworkSummaryForDevice() throws RemoteException {
         final long end = System.currentTimeMillis();
         final long start = end - (DateUtils.WEEK_IN_MILLIS * 4);
-        final int networkType = ConnectivityManager.TYPE_MOBILE;
-        final String subId = "TestSubscriber";
         mLoader = NetworkCycleChartDataLoader.builder(mContext)
-            .setSubscriberId(subId).build();
+                .setNetworkTemplate(mNetworkTemplate)
+                .build();
 
         mLoader.recordUsage(start, end);
 
-        verify(mNetworkStatsManager).querySummaryForDevice(networkType, subId, start, end);
+        verify(mNetworkStatsManager).querySummaryForDevice(mNetworkTemplate, start, end);
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataForUidLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataForUidLoaderTest.java
index aafb46a..877eb61 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataForUidLoaderTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataForUidLoaderTest.java
@@ -28,9 +28,9 @@
 
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
-import android.net.ConnectivityManager;
 import android.net.NetworkPolicy;
 import android.net.NetworkPolicyManager;
+import android.net.NetworkTemplate;
 import android.text.format.DateUtils;
 
 import org.junit.Before;
@@ -42,6 +42,7 @@
 
 @RunWith(RobolectricTestRunner.class)
 public class NetworkCycleDataForUidLoaderTest {
+    private static final String SUB_ID = "Test Subscriber";
 
     @Mock
     private NetworkStatsManager mNetworkStatsManager;
@@ -49,6 +50,7 @@
     private NetworkPolicyManager mNetworkPolicyManager;
     @Mock
     private Context mContext;
+    private NetworkTemplate mNetworkTemplate;
 
     private NetworkCycleDataForUidLoader mLoader;
 
@@ -56,64 +58,62 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         when(mContext.getSystemService(Context.NETWORK_STATS_SERVICE))
-            .thenReturn(mNetworkStatsManager);
+                .thenReturn(mNetworkStatsManager);
         when(mContext.getSystemService(Context.NETWORK_POLICY_SERVICE))
-            .thenReturn(mNetworkPolicyManager);
+                .thenReturn(mNetworkPolicyManager);
         when(mNetworkPolicyManager.getNetworkPolicies()).thenReturn(new NetworkPolicy[0]);
+        mNetworkTemplate = NetworkTemplate.buildTemplateMobileAll(SUB_ID);
     }
 
     @Test
     public void recordUsage_shouldQueryNetworkDetailsForUidAndForegroundState() {
         final long end = System.currentTimeMillis();
         final long start = end - (DateUtils.WEEK_IN_MILLIS * 4);
-        final int networkType = ConnectivityManager.TYPE_MOBILE;
-        final String subId = "TestSubscriber";
         final int uid = 1;
         mLoader = spy(NetworkCycleDataForUidLoader.builder(mContext)
-            .addUid(uid).setSubscriberId(subId).build());
+                .addUid(uid)
+                .setNetworkTemplate(mNetworkTemplate)
+                .build());
         doReturn(1024L).when(mLoader).getTotalUsage(any());
 
         mLoader.recordUsage(start, end);
 
-        verify(mNetworkStatsManager).queryDetailsForUid(networkType, subId, start, end, uid);
+        verify(mNetworkStatsManager).queryDetailsForUid(mNetworkTemplate, start, end, uid);
         verify(mNetworkStatsManager).queryDetailsForUidTagState(
-            networkType, subId, start, end, uid, TAG_NONE, STATE_FOREGROUND);
+                mNetworkTemplate, start, end, uid, TAG_NONE, STATE_FOREGROUND);
     }
 
     @Test
     public void recordUsage_retrieveDetailIsFalse_shouldNotQueryNetworkForegroundState() {
         final long end = System.currentTimeMillis();
         final long start = end - (DateUtils.WEEK_IN_MILLIS * 4);
-        final int networkType = ConnectivityManager.TYPE_MOBILE;
-        final String subId = "TestSubscriber";
         final int uid = 1;
         mLoader = spy(NetworkCycleDataForUidLoader.builder(mContext)
-            .setRetrieveDetail(false).addUid(uid).setSubscriberId(subId).build());
+                .setRetrieveDetail(false).addUid(uid).build());
         doReturn(1024L).when(mLoader).getTotalUsage(any());
 
         mLoader.recordUsage(start, end);
         verify(mNetworkStatsManager, never()).queryDetailsForUidTagState(
-            networkType, subId, start, end, uid, TAG_NONE, STATE_FOREGROUND);
+                mNetworkTemplate, start, end, uid, TAG_NONE, STATE_FOREGROUND);
     }
 
     @Test
     public void recordUsage_multipleUids_shouldQueryNetworkDetailsForEachUid() {
         final long end = System.currentTimeMillis();
         final long start = end - (DateUtils.WEEK_IN_MILLIS * 4);
-        final int networkType = ConnectivityManager.TYPE_MOBILE;
-        final String subId = "TestSubscriber";
         mLoader = spy(NetworkCycleDataForUidLoader.builder(mContext)
-            .addUid(1)
-            .addUid(2)
-            .addUid(3)
-            .setSubscriberId(subId).build());
+                .addUid(1)
+                .addUid(2)
+                .addUid(3)
+                .setNetworkTemplate(mNetworkTemplate)
+                .build());
         doReturn(1024L).when(mLoader).getTotalUsage(any());
 
         mLoader.recordUsage(start, end);
 
-        verify(mNetworkStatsManager).queryDetailsForUid(networkType, subId, start, end, 1);
-        verify(mNetworkStatsManager).queryDetailsForUid(networkType, subId, start, end, 2);
-        verify(mNetworkStatsManager).queryDetailsForUid(networkType, subId, start, end, 3);
+        verify(mNetworkStatsManager).queryDetailsForUid(mNetworkTemplate, start, end, 1);
+        verify(mNetworkStatsManager).queryDetailsForUid(mNetworkTemplate, start, end, 2);
+        verify(mNetworkStatsManager).queryDetailsForUid(mNetworkTemplate, start, end, 3);
     }
 
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataLoaderTest.java
index c5f54bb..74b9151 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataLoaderTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataLoaderTest.java
@@ -126,8 +126,6 @@
         when(mIterator.next()).thenReturn(cycle);
         mLoader = spy(new NetworkCycleDataTestLoader(mContext));
         ReflectionHelpers.setField(mLoader, "mPolicy", mPolicy);
-        ReflectionHelpers.setField(mLoader, "mNetworkType", networkType);
-        ReflectionHelpers.setField(mLoader, "mSubId", subId);
 
         mLoader.loadPolicyData();
 
diff --git a/packages/SystemUI/legacy/recents/src/com/android/systemui/recents/LegacyRecentsImpl.java b/packages/SystemUI/legacy/recents/src/com/android/systemui/recents/LegacyRecentsImpl.java
index b7bb751..a150de9 100644
--- a/packages/SystemUI/legacy/recents/src/com/android/systemui/recents/LegacyRecentsImpl.java
+++ b/packages/SystemUI/legacy/recents/src/com/android/systemui/recents/LegacyRecentsImpl.java
@@ -662,11 +662,17 @@
 
     public final void onBusEvent(ExpandPipEvent event) {
         PipUI pipUi = getComponent(PipUI.class);
+        if (pipUi == null) {
+            return;
+        }
         pipUi.expandPip();
     }
 
     public final void onBusEvent(HidePipMenuEvent event) {
         PipUI pipUi = getComponent(PipUI.class);
+        if (pipUi == null) {
+            return;
+        }
         event.getAnimationTrigger().increment();
         pipUi.hidePipMenu(() -> {
                 event.getAnimationTrigger().increment();
diff --git a/packages/SystemUI/res/drawable/bubble_flyout.xml b/packages/SystemUI/res/drawable/bubble_flyout.xml
deleted file mode 100644
index afe5372..0000000
--- a/packages/SystemUI/res/drawable/bubble_flyout.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<!--
-  ~ 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
-  -->
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item>
-        <shape android:shape="rectangle">
-            <solid android:color="?android:attr/colorBackgroundFloating" />
-            <corners
-                android:bottomLeftRadius="?android:attr/dialogCornerRadius"
-                android:topLeftRadius="?android:attr/dialogCornerRadius"
-                android:bottomRightRadius="?android:attr/dialogCornerRadius"
-                android:topRightRadius="?android:attr/dialogCornerRadius" />
-            <padding
-                android:left="@dimen/bubble_flyout_pointer_size"
-                android:right="@dimen/bubble_flyout_pointer_size" />
-        </shape>
-    </item>
-</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/biometric_dialog.xml b/packages/SystemUI/res/layout/biometric_dialog.xml
index 1abb873..c560d7e 100644
--- a/packages/SystemUI/res/layout/biometric_dialog.xml
+++ b/packages/SystemUI/res/layout/biometric_dialog.xml
@@ -37,7 +37,8 @@
             android:id="@+id/space"
             android:layout_width="match_parent"
             android:layout_height="0dp"
-            android:layout_weight="1" />
+            android:layout_weight="1"
+            android:contentDescription="@string/biometric_dialog_empty_space_description"/>
 
         <ScrollView
             android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/bubble_flyout.xml b/packages/SystemUI/res/layout/bubble_flyout.xml
index 0e4d298..5f773f4 100644
--- a/packages/SystemUI/res/layout/bubble_flyout.xml
+++ b/packages/SystemUI/res/layout/bubble_flyout.xml
@@ -13,18 +13,13 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:paddingLeft="@dimen/bubble_flyout_pointer_size"
-    android:paddingRight="@dimen/bubble_flyout_pointer_size">
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <FrameLayout
-        android:id="@+id/bubble_flyout"
+        android:id="@+id/bubble_flyout_text_container"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
-        android:background="@drawable/bubble_flyout"
+        android:clipToPadding="false"
         android:paddingLeft="@dimen/bubble_flyout_padding_x"
         android:paddingRight="@dimen/bubble_flyout_padding_x"
         android:paddingTop="@dimen/bubble_flyout_padding_y"
@@ -41,4 +36,4 @@
 
     </FrameLayout>
 
-</FrameLayout>
\ No newline at end of file
+</merge>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index cdef09d..2792a01 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -31,23 +31,36 @@
         android:layout_height="wrap_content"
         android:layout_marginBottom="@dimen/keyguard_indication_margin_bottom"
         android:layout_gravity="bottom|center_horizontal"
-        android:orientation="vertical">
+        android:orientation="horizontal">
 
-        <com.android.systemui.statusbar.phone.KeyguardIndicationTextView
-            android:id="@+id/keyguard_indication_enterprise_disclosure"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:gravity="center"
-            android:textAppearance="@style/TextAppearance.Keyguard.BottomArea"
-            android:visibility="gone" />
+        <include layout="@layout/left_docked_overlay" />
 
-        <com.android.systemui.statusbar.phone.KeyguardIndicationTextView
-            android:id="@+id/keyguard_indication_text"
-            android:layout_width="match_parent"
+        <LinearLayout
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:gravity="center"
-            android:textAppearance="@style/TextAppearance.Keyguard.BottomArea"
-            android:accessibilityLiveRegion="polite" />
+            android:layout_weight="1"
+            android:layout_gravity="center_vertical|center_horizontal"
+            android:orientation="vertical">
+
+            <com.android.systemui.statusbar.phone.KeyguardIndicationTextView
+                android:id="@+id/keyguard_indication_enterprise_disclosure"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:textAppearance="@style/TextAppearance.Keyguard.BottomArea"
+                android:visibility="gone" />
+
+            <com.android.systemui.statusbar.phone.KeyguardIndicationTextView
+                android:id="@+id/keyguard_indication_text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:textAppearance="@style/TextAppearance.Keyguard.BottomArea"
+                android:accessibilityLiveRegion="polite" />
+
+        </LinearLayout>
+
+        <include layout="@layout/right_docked_overlay" />
 
     </LinearLayout>
 
diff --git a/packages/SystemUI/res/layout/left_docked_overlay.xml b/packages/SystemUI/res/layout/left_docked_overlay.xml
new file mode 100644
index 0000000..430143c
--- /dev/null
+++ b/packages/SystemUI/res/layout/left_docked_overlay.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<!-- empty stub -->
+<merge />
diff --git a/packages/SystemUI/res/layout/right_docked_overlay.xml b/packages/SystemUI/res/layout/right_docked_overlay.xml
new file mode 100644
index 0000000..430143c
--- /dev/null
+++ b/packages/SystemUI/res/layout/right_docked_overlay.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<!-- empty stub -->
+<merge />
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2643221..e01e6a8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -288,8 +288,18 @@
 
     <!-- Message shown when a biometric is authenticated, asking the user to confirm authentication [CHAR LIMIT=30] -->
     <string name="biometric_dialog_confirm">Confirm</string>
-    <!-- Button name on BiometricPrompt shown when a biometric is detected but not authenticated. Tapping the button resumes authentication [CHAR_LIMIT=30] -->
+    <!-- Button name on BiometricPrompt shown when a biometric is detected but not authenticated. Tapping the button resumes authentication [CHAR LIMIT=30] -->
     <string name="biometric_dialog_try_again">Try again</string>
+    <!-- Content description for empty spaces that are not taken by the biometric dialog. Clicking on these areas will cancel authentication and dismiss the biometric dialog [CHAR LIMIT=NONE] -->
+    <string name="biometric_dialog_empty_space_description">Empty region, tap to cancel authentication</string>
+    <!-- Content description for the face icon when the device is not authenticating anymore [CHAR LIMIT=NONE] -->
+    <string name="biometric_dialog_face_icon_description_idle">Please try again</string>
+    <!-- Content description for the face icon when the device is authenticating [CHAR LIMIT=NONE] -->
+    <string name="biometric_dialog_face_icon_description_authenticating">Looking for your face</string>
+    <!-- Content description for the face icon when the user has been authenticated [CHAR LIMIT=NONE] -->
+    <string name="biometric_dialog_face_icon_description_authenticated">Face authenticated</string>
+    <!-- Content description for the face icon when the user has been authenticated and the confirm button has been pressed [CHAR LIMIT=NONE] -->
+    <string name="biometric_dialog_face_icon_description_confirmed">Confirmed</string>
 
     <!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
     <string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index dd6ccb2..ea8565e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -238,6 +238,8 @@
     private boolean mIsDreaming;
     private final DevicePolicyManager mDevicePolicyManager;
     private boolean mLogoutEnabled;
+    // If the user long pressed the lock icon, disabling face auth for the current session.
+    private boolean mLockIconPressed;
 
     /**
      * Short delay before restarting biometric authentication after a successful try
@@ -1384,6 +1386,7 @@
     }
 
     private void handleScreenTurnedOff() {
+        mLockIconPressed = false;
         mHardwareFingerprintUnavailableRetryCount = 0;
         mHardwareFaceUnavailableRetryCount = 0;
         final int count = mCallbacks.size();
@@ -1625,10 +1628,19 @@
         // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
         return (mBouncer || mAuthInterruptActive || awakeKeyguard || shouldListenForFaceAssistant())
                 && !mSwitchingUser && !getUserCanSkipBouncer(user) && !isFaceDisabled(user)
-                && !mKeyguardGoingAway && mFaceSettingEnabledForUser
+                && !mKeyguardGoingAway && mFaceSettingEnabledForUser && !mLockIconPressed
                 && mUserManager.isUserUnlocked(user) && mIsPrimaryUser;
     }
 
+    /**
+     * Whenever the lock icon is long pressed, disabling trust agents.
+     * This means that we cannot auth passively (face) until the user presses power.
+     */
+    public void onLockIconPressed() {
+        mLockIconPressed = true;
+        mUserFaceAuthenticated.put(getCurrentUser(), false);
+        updateFaceListeningState();
+    }
 
     private void startListeningForFingerprint() {
         if (mFingerprintRunningState == BIOMETRIC_STATE_CANCELLING) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
index 9dfcf7d..45c19ad 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
@@ -325,7 +325,6 @@
 
     private void handleTryAgainPressed() {
         try {
-            mCurrentDialog.clearTemporaryMessage();
             mReceiver.onTryAgainPressed();
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException when handling try again", e);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java
index f99587b..5717a54 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java
@@ -33,7 +33,6 @@
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
@@ -224,13 +223,11 @@
         });
 
         mTryAgainButton.setOnClickListener((View v) -> {
+            handleResetMessage();
             updateState(STATE_AUTHENTICATING);
             showTryAgainButton(false /* show */);
             mCallback.onTryAgainPressed();
         });
-
-        mLayout.setFocusableInTouchMode(true);
-        mLayout.requestFocus();
     }
 
     public void onSaveState(Bundle bundle) {
@@ -269,6 +266,7 @@
         if (mRestoredState == null) {
             updateState(STATE_AUTHENTICATING);
             mErrorText.setText(getHintStringResourceId());
+            mErrorText.setContentDescription(mContext.getString(getHintStringResourceId()));
             mErrorText.setVisibility(View.VISIBLE);
         } else {
             updateState(mState);
@@ -278,7 +276,6 @@
 
         mTitleText.setVisibility(View.VISIBLE);
         mTitleText.setText(titleText);
-        mTitleText.setSelected(true);
 
         final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
         if (TextUtils.isEmpty(subtitleText)) {
@@ -323,11 +320,10 @@
 
     private void setDismissesDialog(View v) {
         v.setClickable(true);
-        v.setOnTouchListener((View view, MotionEvent event) -> {
+        v.setOnClickListener(v1 -> {
             if (mState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) {
                 mCallback.onUserCanceled();
             }
-            return true;
         });
     }
 
@@ -421,11 +417,6 @@
                 BiometricPrompt.HIDE_DIALOG_DELAY);
     }
 
-    public void clearTemporaryMessage() {
-        mHandler.removeMessages(MSG_RESET_MESSAGE);
-        mHandler.obtainMessage(MSG_RESET_MESSAGE).sendToTarget();
-    }
-
     /**
      * Transient help message (acquire) is received, dialog stays showing. Sensor stays in
      * "authenticating" state.
@@ -484,6 +475,7 @@
         mPositiveButton.setVisibility(bundle.getInt(KEY_CONFIRM_VISIBILITY));
         mState = bundle.getInt(KEY_STATE);
         mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
+        mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
         mErrorText.setVisibility(bundle.getInt(KEY_ERROR_TEXT_VISIBILITY));
         mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR));
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java
index dbbb71c..8f26f18 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java
@@ -288,6 +288,7 @@
     @Override
     protected void handleResetMessage() {
         mErrorText.setText(getHintStringResourceId());
+        mErrorText.setContentDescription(mContext.getString(getHintStringResourceId()));
         mErrorText.setTextColor(mTextColor);
         if (getState() == STATE_AUTHENTICATING) {
             mErrorText.setVisibility(View.VISIBLE);
@@ -406,13 +407,21 @@
             } else {
                 mIconController.showIcon(R.drawable.face_dialog_pulse_dark_to_light);
             }
+            mBiometricIcon.setContentDescription(mContext.getString(
+                    R.string.biometric_dialog_face_icon_description_authenticating));
         } else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
             mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
+            mBiometricIcon.setContentDescription(mContext.getString(
+                    R.string.biometric_dialog_face_icon_description_confirmed));
         } else if (oldState == STATE_ERROR && newState == STATE_IDLE) {
             mIconController.animateOnce(R.drawable.face_dialog_error_to_idle);
+            mBiometricIcon.setContentDescription(mContext.getString(
+                    R.string.biometric_dialog_face_icon_description_idle));
         } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) {
             mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
             mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
+            mBiometricIcon.setContentDescription(mContext.getString(
+                    R.string.biometric_dialog_face_icon_description_authenticated));
         } else if (newState == STATE_ERROR) {
             // It's easier to only check newState and gate showing the animation on the
             // mErrorToIdleAnimationRunnable as a proxy, than add a ton of extra state. For example,
@@ -426,11 +435,17 @@
             }
         } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
             mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
+            mBiometricIcon.setContentDescription(mContext.getString(
+                    R.string.biometric_dialog_face_icon_description_authenticated));
         } else if (newState == STATE_PENDING_CONFIRMATION) {
             mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
             mIconController.animateOnce(R.drawable.face_dialog_wink_from_dark);
+            mBiometricIcon.setContentDescription(mContext.getString(
+                    R.string.biometric_dialog_face_icon_description_authenticated));
         } else if (newState == STATE_IDLE) {
             mIconController.showStatic(R.drawable.face_dialog_idle_static);
+            mBiometricIcon.setContentDescription(mContext.getString(
+                    R.string.biometric_dialog_face_icon_description_idle));
         } else {
             Log.w(TAG, "Unknown animation from " + oldState + " -> " + newState);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java
index 845b084..74ad0fa 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java
@@ -18,12 +18,15 @@
 import static android.graphics.Paint.ANTI_ALIAS_FLAG;
 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
 
+import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.Log;
 
+import com.android.systemui.R;
+
 // XXX: Mostly opied from launcher code / can we share?
 /**
  * Contains parameters necessary to draw a badge for an icon (e.g. the size of the badge).
@@ -32,20 +35,31 @@
 
     private static final String TAG = "BadgeRenderer";
 
-    // The badge sizes are defined as percentages of the app icon size.
+    /** The badge sizes are defined as percentages of the app icon size. */
     private static final float SIZE_PERCENTAGE = 0.38f;
 
-    // Extra scale down of the dot
+    /** Extra scale down of the dot. */
     private static final float DOT_SCALE = 0.6f;
 
     private final float mDotCenterOffset;
     private final float mCircleRadius;
     private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
 
-    public BadgeRenderer(int iconSizePx) {
-        mDotCenterOffset = SIZE_PERCENTAGE * iconSizePx;
-        int size = (int) (DOT_SCALE * mDotCenterOffset);
-        mCircleRadius = size / 2f;
+    public BadgeRenderer(Context context) {
+        mDotCenterOffset = getDotCenterOffset(context);
+        mCircleRadius = getDotRadius(mDotCenterOffset);
+    }
+
+    /** Space between the center of the dot and the top or left of the bubble stack. */
+    static float getDotCenterOffset(Context context) {
+        final int iconSizePx =
+                context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size);
+        return SIZE_PERCENTAGE * iconSizePx;
+    }
+
+    static float getDotRadius(float dotCenterOffset) {
+        int size = (int) (DOT_SCALE * dotCenterOffset);
+        return size / 2f;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
index f15e8e4..783780f 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
@@ -57,7 +57,7 @@
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         mIconSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size);
-        mDotRenderer = new BadgeRenderer(mIconSize);
+        mDotRenderer = new BadgeRenderer(getContext());
 
         TypedArray ta = context.obtainStyledAttributes(
                 new int[] {android.R.attr.colorBackgroundFloating});
@@ -83,6 +83,10 @@
         invalidate();
     }
 
+    public boolean getDotPosition() {
+        return mOnLeft;
+    }
+
     /**
      * Set whether the dot should show or not.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index ac4a93b..8aad0f8 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -83,7 +83,7 @@
 
     public void updateDotVisibility() {
         if (iconView != null) {
-            iconView.updateDotVisibility();
+            iconView.updateDotVisibility(true /* animate */);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
new file mode 100644
index 0000000..71f68c1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
@@ -0,0 +1,412 @@
+/*
+ * 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.bubbles;
+
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
+import android.animation.ArgbEvaluator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.drawable.ShapeDrawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
+
+import com.android.systemui.R;
+import com.android.systemui.recents.TriangleShape;
+
+/**
+ * Flyout view that appears as a 'chat bubble' alongside the bubble stack. The flyout can visually
+ * transform into the 'new' dot, which is used during flyout dismiss animations/gestures.
+ */
+public class BubbleFlyoutView extends FrameLayout {
+    /** Max width of the flyout, in terms of percent of the screen width. */
+    private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f;
+
+    private final int mFlyoutPadding;
+    private final int mFlyoutSpaceFromBubble;
+    private final int mPointerSize;
+    private final int mBubbleSize;
+    private final int mFlyoutElevation;
+    private final int mBubbleElevation;
+    private final int mFloatingBackgroundColor;
+    private final float mCornerRadius;
+
+    private final ViewGroup mFlyoutTextContainer;
+    private final TextView mFlyoutText;
+    /** Spring animation for the flyout. */
+    private final SpringAnimation mFlyoutSpring =
+            new SpringAnimation(this, DynamicAnimation.TRANSLATION_X);
+
+    /** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */
+    private final float mNewDotRadius;
+    private final float mNewDotSize;
+    private final float mNewDotOffsetFromBubbleBounds;
+
+    /**
+     * The paint used to draw the background, whose color changes as the flyout transitions to the
+     * tinted 'new' dot.
+     */
+    private final Paint mBgPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
+    private final ArgbEvaluator mArgbEvaluator = new ArgbEvaluator();
+
+    /**
+     * Triangular ShapeDrawables used for the triangle that points from the flyout to the bubble
+     * stack (a chat-bubble effect).
+     */
+    private final ShapeDrawable mLeftTriangleShape;
+    private final ShapeDrawable mRightTriangleShape;
+
+    /** Whether the flyout arrow is on the left (pointing left) or right (pointing right). */
+    private boolean mArrowPointingLeft = true;
+
+    /** Color of the 'new' dot that the flyout will transform into. */
+    private int mDotColor;
+
+    /** The outline of the triangle, used for elevation shadows. */
+    private final Outline mTriangleOutline = new Outline();
+
+    /** The bounds of the flyout background, kept up to date as it transitions to the 'new' dot. */
+    private final RectF mBgRect = new RectF();
+
+    /**
+     * Percent progress in the transition from flyout to 'new' dot. These two values are the inverse
+     * of each other (if we're 40% transitioned to the dot, we're 60% flyout), but it makes the code
+     * much more readable.
+     */
+    private float mPercentTransitionedToDot = 1f;
+    private float mPercentStillFlyout = 0f;
+
+    /**
+     * The difference in values between the flyout and the dot. These differences are gradually
+     * added over the course of the animation to transform the flyout into the 'new' dot.
+     */
+    private float mFlyoutToDotWidthDelta = 0f;
+    private float mFlyoutToDotHeightDelta = 0f;
+    private float mFlyoutToDotCornerRadiusDelta;
+
+    /** The translation values when the flyout is completely transitioned into the dot. */
+    private float mTranslationXWhenDot = 0f;
+    private float mTranslationYWhenDot = 0f;
+
+    /**
+     * The current translation values applied to the flyout background as it transitions into the
+     * 'new' dot.
+     */
+    private float mBgTranslationX;
+    private float mBgTranslationY;
+
+    /** The flyout's X translation when at rest (not animating or dragging). */
+    private float mRestingTranslationX = 0f;
+
+    /** Callback to run when the flyout is hidden. */
+    private Runnable mOnHide;
+
+    public BubbleFlyoutView(Context context) {
+        super(context);
+        LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true);
+
+        mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container);
+        mFlyoutText = mFlyoutTextContainer.findViewById(R.id.bubble_flyout_text);
+
+        final Resources res = getResources();
+        mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x);
+        mFlyoutSpaceFromBubble = res.getDimensionPixelSize(R.dimen.bubble_flyout_space_from_bubble);
+        mPointerSize = res.getDimensionPixelSize(R.dimen.bubble_flyout_pointer_size);
+        mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+        mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
+        mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation);
+        mNewDotOffsetFromBubbleBounds = BadgeRenderer.getDotCenterOffset(context);
+        mNewDotRadius = BadgeRenderer.getDotRadius(mNewDotOffsetFromBubbleBounds);
+        mNewDotSize = mNewDotRadius * 2f;
+
+        final TypedArray ta = mContext.obtainStyledAttributes(
+                new int[] {
+                        android.R.attr.colorBackgroundFloating,
+                        android.R.attr.dialogCornerRadius});
+        mFloatingBackgroundColor = ta.getColor(0, Color.WHITE);
+        mCornerRadius = ta.getDimensionPixelSize(1, 0);
+        mFlyoutToDotCornerRadiusDelta = mNewDotRadius - mCornerRadius;
+        ta.recycle();
+
+        // Add padding for the pointer on either side, onDraw will draw it in this space.
+        setPadding(mPointerSize, 0, mPointerSize, 0);
+        setWillNotDraw(false);
+        setClipChildren(false);
+        setTranslationZ(mFlyoutElevation);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                BubbleFlyoutView.this.getOutline(outline);
+            }
+        });
+
+        mBgPaint.setColor(mFloatingBackgroundColor);
+
+        mLeftTriangleShape =
+                new ShapeDrawable(TriangleShape.createHorizontal(
+                        mPointerSize, mPointerSize, true /* isPointingLeft */));
+        mLeftTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize);
+        mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor);
+
+        mRightTriangleShape =
+                new ShapeDrawable(TriangleShape.createHorizontal(
+                        mPointerSize, mPointerSize, false /* isPointingLeft */));
+        mRightTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize);
+        mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        renderBackground(canvas);
+        invalidateOutline();
+        super.onDraw(canvas);
+    }
+
+    /** Configures the flyout and animates it in. */
+    void showFlyout(
+            CharSequence updateMessage, PointF stackPos, float parentWidth,
+            boolean arrowPointingLeft, int dotColor, Runnable onHide) {
+        mArrowPointingLeft = arrowPointingLeft;
+        mDotColor = dotColor;
+        mOnHide = onHide;
+
+        setCollapsePercent(0f);
+        setAlpha(0f);
+        setVisibility(VISIBLE);
+
+        // Set the flyout TextView's max width in terms of percent, and then subtract out the
+        // padding so that the entire flyout view will be the desired width (rather than the
+        // TextView being the desired width + extra padding).
+        mFlyoutText.setMaxWidth(
+                (int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2);
+        mFlyoutText.setText(updateMessage);
+
+        // Wait for the TextView to lay out so we know its line count.
+        post(() -> {
+            // Multi line flyouts get top-aligned to the bubble.
+            if (mFlyoutText.getLineCount() > 1) {
+                setTranslationY(stackPos.y);
+            } else {
+                // Single line flyouts are vertically centered with respect to the bubble.
+                setTranslationY(
+                        stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f);
+            }
+
+            // Calculate the translation required to position the flyout next to the bubble stack,
+            // with the desired padding.
+            mRestingTranslationX = mArrowPointingLeft
+                    ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble
+                    : stackPos.x - getWidth() - mFlyoutSpaceFromBubble;
+
+            // Translate towards the stack slightly.
+            setTranslationX(
+                    mRestingTranslationX + (arrowPointingLeft ? -mBubbleSize : mBubbleSize));
+
+            // Fade in the entire flyout and spring it to its normal position.
+            animate().alpha(1f);
+            mFlyoutSpring.animateToFinalPosition(mRestingTranslationX);
+
+            // Calculate the difference in size between the flyout and the 'dot' so that we can
+            // transform into the dot later.
+            mFlyoutToDotWidthDelta = getWidth() - mNewDotSize;
+            mFlyoutToDotHeightDelta = getHeight() - mNewDotSize;
+
+            // Calculate the translation values needed to be in the correct 'new dot' position.
+            final float distanceFromFlyoutLeftToDotCenterX =
+                    mFlyoutSpaceFromBubble + mNewDotOffsetFromBubbleBounds / 2;
+            if (mArrowPointingLeft) {
+                mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX - mNewDotRadius;
+            } else {
+                mTranslationXWhenDot =
+                        getWidth() + distanceFromFlyoutLeftToDotCenterX - mNewDotRadius;
+            }
+
+            mTranslationYWhenDot =
+                    getHeight() / 2f
+                            - mNewDotRadius
+                            - mBubbleSize / 2f
+                            + mNewDotOffsetFromBubbleBounds / 2;
+        });
+    }
+
+    /**
+     * Hides the flyout and runs the optional callback passed into showFlyout. The flyout has been
+     * animated into the 'new' dot by the time we call this, so no animations are needed.
+     */
+    void hideFlyout() {
+        if (mOnHide != null) {
+            mOnHide.run();
+            mOnHide = null;
+        }
+
+        setVisibility(GONE);
+    }
+
+    /** Sets the percentage that the flyout should be collapsed into dot form. */
+    void setCollapsePercent(float percentCollapsed) {
+        mPercentTransitionedToDot = Math.max(0f, Math.min(percentCollapsed, 1f));
+        mPercentStillFlyout = (1f - mPercentTransitionedToDot);
+
+        // Move and fade out the text.
+        mFlyoutText.setTranslationX(
+                (mArrowPointingLeft ? -getWidth() : getWidth()) * mPercentTransitionedToDot);
+        mFlyoutText.setAlpha(clampPercentage(
+                (mPercentStillFlyout - (1f - BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS))
+                        / BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS));
+
+        // Reduce the elevation towards that of the topmost bubble.
+        setTranslationZ(
+                mFlyoutElevation
+                        - (mFlyoutElevation - mBubbleElevation) * mPercentTransitionedToDot);
+        invalidate();
+    }
+
+    /** Return the flyout's resting X translation (translation when not dragging or animating). */
+    float getRestingTranslationX() {
+        return mRestingTranslationX;
+    }
+
+    /** Clamps a float to between 0 and 1. */
+    private float clampPercentage(float percent) {
+        return Math.min(1f, Math.max(0f, percent));
+    }
+
+    /**
+     * Renders the background, which is either the rounded 'chat bubble' flyout, or some state
+     * between that and the 'new' dot over the bubbles.
+     */
+    private void renderBackground(Canvas canvas) {
+        // Calculate the width, height, and corner radius of the flyout given the current collapsed
+        // percentage.
+        final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot);
+        final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot);
+        final float cornerRadius = mCornerRadius
+                - (mFlyoutToDotCornerRadiusDelta * mPercentTransitionedToDot);
+
+        // Translate the flyout background towards the collapsed 'dot' state.
+        mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot;
+        mBgTranslationY = mTranslationYWhenDot * mPercentTransitionedToDot;
+
+        // Set the bounds of the rounded rectangle that serves as either the flyout background or
+        // the collapsed 'dot'. These bounds will also be used to provide the outline for elevation
+        // shadows. In the expanded flyout state, the left and right bounds leave space for the
+        // pointer triangle - as the flyout collapses, this space is reduced since the triangle
+        // retracts into the flyout.
+        mBgRect.set(
+                mPointerSize * mPercentStillFlyout /* left */,
+                0 /* top */,
+                width - mPointerSize * mPercentStillFlyout /* right */,
+                height /* bottom */);
+
+        mBgPaint.setColor(
+                (int) mArgbEvaluator.evaluate(
+                        mPercentTransitionedToDot, mFloatingBackgroundColor, mDotColor));
+
+        canvas.save();
+        canvas.translate(mBgTranslationX, mBgTranslationY);
+        renderPointerTriangle(canvas, width, height);
+        canvas.drawRoundRect(mBgRect, cornerRadius, cornerRadius, mBgPaint);
+        canvas.restore();
+    }
+
+    /** Renders the 'pointer' triangle that points from the flyout to the bubble stack. */
+    private void renderPointerTriangle(
+            Canvas canvas, float currentFlyoutWidth, float currentFlyoutHeight) {
+        canvas.save();
+
+        // Translation to apply for the 'retraction' effect as the flyout collapses.
+        final float retractionTranslationX =
+                (mArrowPointingLeft ? 1 : -1) * (mPercentTransitionedToDot * mPointerSize * 2f);
+
+        // Place the arrow either at the left side, or the far right, depending on whether the
+        // flyout is on the left or right side.
+        final float arrowTranslationX =
+                mArrowPointingLeft
+                        ? retractionTranslationX
+                        : currentFlyoutWidth - mPointerSize + retractionTranslationX;
+
+        // Vertically center the arrow at all times.
+        final float arrowTranslationY = currentFlyoutHeight / 2f - mPointerSize / 2f;
+
+        // Draw the appropriate direction of arrow.
+        final ShapeDrawable relevantTriangle =
+                mArrowPointingLeft ? mLeftTriangleShape : mRightTriangleShape;
+        canvas.translate(arrowTranslationX, arrowTranslationY);
+        relevantTriangle.setAlpha((int) (255f * mPercentStillFlyout));
+        relevantTriangle.draw(canvas);
+
+        // Save the triangle's outline for use in the outline provider, offsetting it to reflect its
+        // current position.
+        relevantTriangle.getOutline(mTriangleOutline);
+        mTriangleOutline.offset((int) arrowTranslationX, (int) arrowTranslationY);
+
+        canvas.restore();
+    }
+
+    /** Builds an outline that includes the transformed flyout background and triangle. */
+    private void getOutline(Outline outline) {
+        if (!mTriangleOutline.isEmpty()) {
+            // Draw the rect into the outline as a path so we can merge the triangle path into it.
+            final Path rectPath = new Path();
+            rectPath.addRoundRect(mBgRect, mCornerRadius, mCornerRadius, Path.Direction.CW);
+            outline.setConvexPath(rectPath);
+
+            // Get rid of the triangle path once it has disappeared behind the flyout.
+            if (mPercentStillFlyout > 0.5f) {
+                outline.mPath.addPath(mTriangleOutline.mPath);
+            }
+
+            // Translate the outline to match the background's position.
+            final Matrix outlineMatrix = new Matrix();
+            outlineMatrix.postTranslate(getLeft() + mBgTranslationX, getTop() + mBgTranslationY);
+
+            // At the very end, retract the outline into the bubble so the shadow will be pulled
+            // into the flyout-dot as it (visually) becomes part of the bubble. We can't do this by
+            // animating translationZ to zero since then it'll go under the bubbles, which have
+            // elevation.
+            if (mPercentTransitionedToDot > 0.98f) {
+                final float percentBetween99and100 = (mPercentTransitionedToDot - 0.98f) / .02f;
+                final float percentShadowVisible = 1f - percentBetween99and100;
+
+                // Keep it centered.
+                outlineMatrix.postTranslate(
+                        mNewDotRadius * percentBetween99and100,
+                        mNewDotRadius * percentBetween99and100);
+                outlineMatrix.preScale(percentShadowVisible, percentShadowVisible);
+            }
+
+            outline.mPath.transform(outlineMatrix);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 2b17425..4fef157 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -25,8 +25,6 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Color;
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Outline;
@@ -35,8 +33,6 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.drawable.LayerDrawable;
-import android.graphics.drawable.ShapeDrawable;
 import android.os.Bundle;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
@@ -56,11 +52,11 @@
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.widget.FrameLayout;
-import android.widget.TextView;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
@@ -70,7 +66,6 @@
 import com.android.systemui.bubbles.animation.ExpandedAnimationController;
 import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
 import com.android.systemui.bubbles.animation.StackAnimationController;
-import com.android.systemui.recents.TriangleShape;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
 import java.math.BigDecimal;
@@ -86,12 +81,21 @@
     private static final String TAG = "BubbleStackView";
     private static final boolean DEBUG = false;
 
+    /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
+    static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
+
+    /** Velocity required to dismiss the flyout via drag. */
+    private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
+
+    /**
+     * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
+     * for every 8 pixels overscrolled).
+     */
+    private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
+
     /** Duration of the flyout alpha animations. */
     private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
 
-    /** Max width of the flyout, in terms of percent of the screen width. */
-    private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f;
-
     /** Percent to darken the bubbles when they're in the dismiss target. */
     private static final float DARKEN_PERCENT = 0.3f;
 
@@ -152,17 +156,9 @@
 
     private FrameLayout mExpandedViewContainer;
 
-    private FrameLayout mFlyoutContainer;
-    private FrameLayout mFlyout;
-    private TextView mFlyoutText;
-    private ShapeDrawable mLeftFlyoutTriangle;
-    private ShapeDrawable mRightFlyoutTriangle;
-    /** Spring animation for the flyout. */
-    private SpringAnimation mFlyoutSpring;
+    private BubbleFlyoutView mFlyout;
     /** Runnable that fades out the flyout and then sets it to GONE. */
-    private Runnable mHideFlyout =
-            () -> mFlyoutContainer.animate().alpha(0f).withEndAction(
-                    () -> mFlyoutContainer.setVisibility(GONE));
+    private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
 
     /** Layout change listener that moves the stack to the nearest valid position on rotation. */
     private OnLayoutChangeListener mMoveStackToValidPositionOnLayoutListener;
@@ -176,9 +172,6 @@
 
     private int mBubbleSize;
     private int mBubblePadding;
-    private int mFlyoutPadding;
-    private int mFlyoutSpaceFromBubble;
-    private int mPointerSize;
     private int mExpandedAnimateXDistance;
     private int mExpandedAnimateYDistance;
     private int mStatusBarHeight;
@@ -189,8 +182,11 @@
     private boolean mIsExpanded;
     private boolean mImeVisible;
 
-    /** Whether the stack is currently being dragged. */
-    private boolean mIsDragging = false;
+    /** Whether the stack is currently on the left side of the screen, or animating there. */
+    private boolean mStackOnLeftOrWillBe = false;
+
+    /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
+    private boolean mIsGestureInProgress = false;
 
     private BubbleTouchHandler mTouchHandler;
     private BubbleController.BubbleExpandListener mExpandListener;
@@ -249,6 +245,40 @@
         }
     };
 
+    /** Float property that 'drags' the flyout. */
+    private final FloatPropertyCompat mFlyoutCollapseProperty =
+            new FloatPropertyCompat("FlyoutCollapseSpring") {
+                @Override
+                public float getValue(Object o) {
+                    return mFlyoutDragDeltaX;
+                }
+
+                @Override
+                public void setValue(Object o, float v) {
+                    onFlyoutDragged(v);
+                }
+            };
+
+    /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
+    private final SpringAnimation mFlyoutTransitionSpring =
+            new SpringAnimation(this, mFlyoutCollapseProperty);
+
+    /** Distance the flyout has been dragged in the X axis. */
+    private float mFlyoutDragDeltaX = 0f;
+
+    /**
+     * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
+     * it immediately.
+     */
+    private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
+            (dynamicAnimation, b, v, v1) -> {
+                if (mFlyoutDragDeltaX == 0) {
+                    mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
+                } else {
+                    mFlyout.hideFlyout();
+                }
+            };
+
     @NonNull private final SurfaceSynchronizer mSurfaceSynchronizer;
 
     private BubbleDismissView mDismissContainer;
@@ -267,9 +297,6 @@
         Resources res = getResources();
         mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
         mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
-        mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x);
-        mFlyoutSpaceFromBubble = res.getDimensionPixelSize(R.dimen.bubble_flyout_space_from_bubble);
-        mPointerSize = res.getDimensionPixelSize(R.dimen.bubble_flyout_pointer_size);
         mExpandedAnimateXDistance =
                 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
         mExpandedAnimateYDistance =
@@ -307,17 +334,24 @@
         mExpandedViewContainer.setClipChildren(false);
         addView(mExpandedViewContainer);
 
-        mFlyoutContainer = (FrameLayout) mInflater.inflate(R.layout.bubble_flyout, this, false);
-        mFlyoutContainer.setVisibility(GONE);
-        mFlyoutContainer.setClipToPadding(false);
-        mFlyoutContainer.setClipChildren(false);
-        mFlyoutContainer.animate()
+        mFlyout = new BubbleFlyoutView(context);
+        mFlyout.setVisibility(GONE);
+        mFlyout.animate()
                 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
                 .setInterpolator(new AccelerateDecelerateInterpolator());
+        addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
 
-        mFlyout = mFlyoutContainer.findViewById(R.id.bubble_flyout);
-        addView(mFlyoutContainer);
-        setupFlyout();
+        mFlyoutTransitionSpring.setSpring(new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+        mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
+
+        mDismissContainer = new BubbleDismissView(mContext);
+        mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams(
+                MATCH_PARENT,
+                getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height),
+                Gravity.BOTTOM));
+        addView(mDismissContainer);
 
         mDismissContainer = new BubbleDismissView(mContext);
         mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams(
@@ -742,7 +776,7 @@
             }
             // Outside parts of view we care about.
             return null;
-        } else if (mFlyoutContainer.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) {
+        } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) {
             return mFlyout;
         }
 
@@ -931,7 +965,6 @@
         mBubbleContainer.setController(mStackAnimationController);
         hideFlyoutImmediate();
 
-        mIsDragging = true;
         mDraggingInDismissTarget = false;
     }
 
@@ -948,20 +981,87 @@
         if (DEBUG) {
             Log.d(TAG, "onDragFinish");
         }
-        // TODO: Add fling to bottom to dismiss.
-        mIsDragging = false;
 
         if (mIsExpanded || mIsExpansionAnimating) {
             return;
         }
 
-        mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
+        final float newStackX = mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
         logBubbleEvent(null /* no bubble associated with bubble stack move */,
                 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
 
+        mStackOnLeftOrWillBe = newStackX <= 0;
+        updateBubbleShadowsAndDotPosition(true /* animate */);
         springOutDismissTargetAndHideCircle();
     }
 
+    void onFlyoutDragStart() {
+        mFlyout.removeCallbacks(mHideFlyout);
+    }
+
+    void onFlyoutDragged(float deltaX) {
+        final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
+        mFlyoutDragDeltaX = deltaX;
+
+        final float collapsePercent =
+                onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
+        mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
+
+        // Calculate how to translate the flyout if it has been dragged too far in etiher direction.
+        float overscrollTranslation = 0f;
+        if (collapsePercent < 0f || collapsePercent > 1f) {
+            // Whether we are more than 100% transitioned to the dot.
+            final boolean overscrollingPastDot = collapsePercent > 1f;
+
+            // Whether we are overscrolling physically to the left - this can either be pulling the
+            // flyout away from the stack (if the stack is on the right) or pushing it to the left
+            // after it has already become the dot.
+            final boolean overscrollingLeft =
+                    (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
+
+            overscrollTranslation =
+                    (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
+                            * (overscrollingLeft ? -1 : 1)
+                            * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
+                                // Attenuate the smaller dot less than the larger flyout.
+                                / (overscrollingPastDot ? 2 : 1)));
+        }
+
+        mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
+    }
+
+    /**
+     * Called when the flyout drag has finished, and returns true if the gesture successfully
+     * dismissed the flyout.
+     */
+    void onFlyoutDragFinished(float deltaX, float velX) {
+        final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
+        final boolean metRequiredVelocity =
+                onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
+        final boolean metRequiredDeltaX =
+                onLeft
+                        ? deltaX < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
+                        : deltaX > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
+        final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
+        final boolean shouldDismiss = metRequiredVelocity || (metRequiredDeltaX && !isCancelFling);
+
+        mFlyout.removeCallbacks(mHideFlyout);
+        animateFlyoutCollapsed(shouldDismiss, velX);
+    }
+
+    /**
+     * Called when the first touch event of a gesture (stack drag, bubble drag, flyout drag, etc.)
+     * is received.
+     */
+    void onGestureStart() {
+        mIsGestureInProgress = true;
+    }
+
+    /** Called when a gesture is completed or cancelled. */
+    void onGestureFinished() {
+        mIsGestureInProgress = false;
+    }
+
     /** Prepares and starts the desaturate/darken animation on the bubble stack. */
     private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
         mDesaturateAndDarkenTargetView = targetView;
@@ -1119,12 +1219,22 @@
         mShowingDismiss = false;
     }
 
-
     /** Whether the location of the given MotionEvent is within the dismiss target area. */
-    public boolean isInDismissTarget(MotionEvent ev) {
+    boolean isInDismissTarget(MotionEvent ev) {
         return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY());
     }
 
+    /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
+    private void animateFlyoutCollapsed(boolean collapsed, float velX) {
+        final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
+        mFlyoutTransitionSpring
+                .setStartValue(mFlyoutDragDeltaX)
+                .setStartVelocity(velX)
+                .animateToFinalPosition(collapsed
+                        ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
+                        : 0f);
+    }
+
     /**
      * Calculates how large the expanded view of the bubble can be. This takes into account the
      * y position when the bubbles are expanded as well as the bounds of the dismiss target.
@@ -1161,55 +1271,27 @@
         final CharSequence updateMessage = bubble.entry.getUpdateMessage(getContext());
 
         // Show the message if one exists, and we're not expanded or animating expansion.
-        if (updateMessage != null && !isExpanded() && !mIsExpansionAnimating && !mIsDragging) {
-            final PointF stackPos = mStackAnimationController.getStackPosition();
+        if (updateMessage != null
+                && !isExpanded()
+                && !mIsExpansionAnimating
+                && !mIsGestureInProgress) {
+            if (bubble.iconView != null) {
+                bubble.iconView.setSuppressDot(true /* suppressDot */, false /* animate */);
+                mFlyoutDragDeltaX = 0f;
+                mFlyout.setAlpha(0f);
 
-            // Set the flyout TextView's max width in terms of percent, and then subtract out the
-            // padding so that the entire flyout view will be the desired width (rather than the
-            // TextView being the desired width + extra padding).
-            mFlyoutText.setMaxWidth(
-                    (int) (getWidth() * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2);
-
-            mFlyoutContainer.setAlpha(0f);
-            mFlyoutContainer.setVisibility(VISIBLE);
-
-            mFlyoutText.setText(updateMessage);
-
-            final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
-
-            if (onLeft) {
-                mLeftFlyoutTriangle.setAlpha(255);
-                mRightFlyoutTriangle.setAlpha(0);
-            } else {
-                mLeftFlyoutTriangle.setAlpha(0);
-                mRightFlyoutTriangle.setAlpha(255);
+                // Post in case layout isn't complete and getWidth returns 0.
+                post(() -> mFlyout.showFlyout(
+                        updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
+                        mStackAnimationController.isStackOnLeftSide(),
+                        bubble.iconView.getBadgeColor(),
+                        () -> {
+                            bubble.iconView.setSuppressDot(
+                                    false /* suppressDot */, false /* animate */);
+                        }));
             }
-
-            mFlyoutContainer.post(() -> {
-                // Multi line flyouts get top-aligned to the bubble.
-                if (mFlyoutText.getLineCount() > 1) {
-                    mFlyoutContainer.setTranslationY(stackPos.y);
-                } else {
-                    // Single line flyouts are vertically centered with respect to the bubble.
-                    mFlyoutContainer.setTranslationY(
-                            stackPos.y + (mBubbleSize - mFlyout.getHeight()) / 2f);
-                }
-
-                final float destinationX = onLeft
-                        ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble
-                        : stackPos.x - mFlyoutContainer.getWidth() - mFlyoutSpaceFromBubble;
-
-                // Translate towards the stack slightly, then spring out from the stack.
-                mFlyoutContainer.setTranslationX(
-                        destinationX + (onLeft ? -mBubblePadding : mBubblePadding));
-
-                mFlyoutContainer.animate().alpha(1f);
-                mFlyoutSpring.animateToFinalPosition(destinationX);
-
-                mFlyout.removeCallbacks(mHideFlyout);
-                mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
-            });
-
+            mFlyout.removeCallbacks(mHideFlyout);
+            mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
             logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
         }
     }
@@ -1217,7 +1299,7 @@
     /** Hide the flyout immediately and cancel any pending hide runnables. */
     private void hideFlyoutImmediate() {
         mFlyout.removeCallbacks(mHideFlyout);
-        mHideFlyout.run();
+        mFlyout.hideFlyout();
     }
 
     @Override
@@ -1230,7 +1312,7 @@
             mBubbleContainer.getBoundsOnScreen(outRect);
         }
 
-        if (mFlyoutContainer.getVisibility() == View.VISIBLE) {
+        if (mFlyout.getVisibility() == View.VISIBLE) {
             final Rect flyoutBounds = new Rect();
             mFlyout.getBoundsOnScreen(flyoutBounds);
             outRect.union(flyoutBounds);
@@ -1287,78 +1369,11 @@
         }
     }
 
-    /** Sets up the flyout views and drawables. */
-    private void setupFlyout() {
-        // Retrieve the styled floating background color.
-        TypedArray ta = mContext.obtainStyledAttributes(
-                new int[]{android.R.attr.colorBackgroundFloating});
-        final int floatingBackgroundColor = ta.getColor(0, Color.WHITE);
-        ta.recycle();
-
-        // Retrieve the flyout background, which is currently a rounded white rectangle with a
-        // shadow but no triangular arrow pointing anywhere.
-        final LayerDrawable flyoutBackground = (LayerDrawable) mFlyout.getBackground();
-
-        // Create the triangle drawables and set their color.
-        mLeftFlyoutTriangle =
-                new ShapeDrawable(TriangleShape.createHorizontal(
-                        mPointerSize, mPointerSize, true /* isPointingLeft */));
-        mRightFlyoutTriangle =
-                new ShapeDrawable(TriangleShape.createHorizontal(
-                        mPointerSize, mPointerSize, false /* isPointingLeft */));
-        mLeftFlyoutTriangle.getPaint().setColor(floatingBackgroundColor);
-        mRightFlyoutTriangle.getPaint().setColor(floatingBackgroundColor);
-
-        // Add both triangles to the drawable. We'll show and hide the appropriate ones when we show
-        // the flyout.
-        final int leftTriangleIndex = flyoutBackground.addLayer(mLeftFlyoutTriangle);
-        flyoutBackground.setLayerSize(leftTriangleIndex, mPointerSize, mPointerSize);
-        flyoutBackground.setLayerGravity(leftTriangleIndex, Gravity.LEFT | Gravity.CENTER_VERTICAL);
-        flyoutBackground.setLayerInsetLeft(leftTriangleIndex, -mPointerSize);
-
-        final int rightTriangleIndex = flyoutBackground.addLayer(mRightFlyoutTriangle);
-        flyoutBackground.setLayerSize(rightTriangleIndex, mPointerSize, mPointerSize);
-        flyoutBackground.setLayerGravity(
-                rightTriangleIndex, Gravity.RIGHT | Gravity.CENTER_VERTICAL);
-        flyoutBackground.setLayerInsetRight(rightTriangleIndex, -mPointerSize);
-
-        // Append the appropriate triangle's outline to the view's outline so that the shadows look
-        // correct.
-        mFlyout.setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                final boolean leftPointing = mStackAnimationController.isStackOnLeftSide();
-
-                // Get the outline from the appropriate triangle.
-                final Outline triangleOutline = new Outline();
-                if (leftPointing) {
-                    mLeftFlyoutTriangle.getOutline(triangleOutline);
-                } else {
-                    mRightFlyoutTriangle.getOutline(triangleOutline);
-                }
-
-                // Offset it to the correct position, since it has no intrinsic position since
-                // that is maintained by the parent LayerDrawable.
-                triangleOutline.offset(
-                        leftPointing ? -mPointerSize : mFlyout.getWidth(),
-                        mFlyout.getHeight() / 2 - mPointerSize / 2);
-
-                // Merge the outlines.
-                final Outline compoundOutline = new Outline();
-                flyoutBackground.getOutline(compoundOutline);
-                compoundOutline.mPath.addPath(triangleOutline.mPath);
-                outline.set(compoundOutline);
-            }
-        });
-
-        mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text);
-        mFlyoutSpring = new SpringAnimation(mFlyoutContainer, DynamicAnimation.TRANSLATION_X);
-    }
-
     private void applyCurrentState() {
         if (DEBUG) {
             Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded);
         }
+
         mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
         if (mIsExpanded) {
             // First update the view so that it calculates a new height (ensuring the y position
@@ -1376,10 +1391,15 @@
             }
         }
 
+        mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
+        updateBubbleShadowsAndDotPosition(false);
+    }
+
+    /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
+    private void updateBubbleShadowsAndDotPosition(boolean animate) {
         int bubbsCount = mBubbleContainer.getChildCount();
         for (int i = 0; i < bubbsCount; i++) {
             BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
-            bv.updateDotVisibility();
             bv.setZ((BubbleController.MAX_BUBBLES
                     * getResources().getDimensionPixelSize(R.dimen.bubble_elevation)) - i);
 
@@ -1393,6 +1413,11 @@
                 }
             });
             bv.setClipToOutline(false);
+
+            // If the dot is on the left, and so is the stack, we need to change the dot position.
+            if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
+                bv.setDotPosition(!mStackOnLeftOrWillBe, animate);
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index f429c2c..8fe8bd3 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -111,12 +111,13 @@
                 trackMovement(event);
 
                 mTouchDown.set(rawX, rawY);
+                mStack.onGestureStart();
 
                 if (isStack) {
                     mViewPositionOnTouchDown.set(mStack.getStackPosition());
                     mStack.onDragStart();
                 } else if (isFlyout) {
-                    // TODO(b/129768381): Make the flyout dismissable with a gesture.
+                    mStack.onFlyoutDragStart();
                 } else {
                     mViewPositionOnTouchDown.set(
                             mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
@@ -137,7 +138,7 @@
                     if (isStack) {
                         mStack.onDragged(viewX, viewY);
                     } else if (isFlyout) {
-                        // TODO(b/129768381): Make the flyout dismissable with a gesture.
+                        mStack.onFlyoutDragged(deltaX);
                     } else {
                         mStack.onBubbleDragged(mTouchedView, viewX, viewY);
                     }
@@ -152,8 +153,10 @@
                     final float velY = mVelocityTracker.getYVelocity();
 
                     // If the touch event is within the dismiss target, magnet the stack to it.
-                    mStack.animateMagnetToDismissTarget(
-                            mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
+                    if (!isFlyout) {
+                        mStack.animateMagnetToDismissTarget(
+                                mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
+                    }
                 }
                 break;
 
@@ -174,7 +177,9 @@
                                 : mInDismissTarget
                                         || velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY;
 
-                if (shouldDismiss) {
+                if (isFlyout && mMovedEnough) {
+                    mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
+                } else if (shouldDismiss) {
                     final String individualBubbleKey =
                             isStack ? null : ((BubbleView) mTouchedView).getKey();
                     mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
@@ -200,7 +205,7 @@
                     }
                 } else if (mTouchedView == mStack.getExpandedBubbleView()) {
                     mBubbleData.setExpanded(false);
-                } else if (isStack) {
+                } else if (isStack || isFlyout) {
                     // Toggle expansion
                     mBubbleData.setExpanded(!mBubbleData.isExpanded());
                 } else {
@@ -251,9 +256,12 @@
             mVelocityTracker.recycle();
             mVelocityTracker = null;
         }
+
         mTouchedView = null;
         mMovedEnough = false;
         mInDismissTarget = false;
+
+        mStack.onGestureFinished();
     }
 
     private void trackMovement(MotionEvent event) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
index 2681b6d..aa32b94 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
@@ -48,9 +48,12 @@
     private Context mContext;
 
     private BadgedImageView mBadgedImageView;
+    private int mBadgeColor;
     private int mPadding;
     private int mIconInset;
 
+    private boolean mSuppressDot = false;
+
     private NotificationEntry mEntry;
 
     public BubbleView(Context context) {
@@ -130,18 +133,54 @@
         return (mEntry != null) ? mEntry.getRow() : null;
     }
 
+    /** Changes the dot's visibility to match the bubble view's state. */
+    void updateDotVisibility(boolean animate) {
+        updateDotVisibility(animate, null /* after */);
+    }
+
     /**
-     * Marks this bubble as "read", i.e. no badge should show.
+     * Changes the dot's visibility to match the bubble view's state, running the provided callback
+     * after animation if requested.
      */
-    public void updateDotVisibility() {
-        boolean showDot = getEntry().showInShadeWhenBubble();
-        animateDot(showDot);
+    void updateDotVisibility(boolean animate, Runnable after) {
+        boolean showDot = getEntry().showInShadeWhenBubble() && !mSuppressDot;
+
+        if (animate) {
+            animateDot(showDot, after);
+        } else {
+            mBadgedImageView.setShowDot(showDot);
+        }
+    }
+
+    /**
+     * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
+     * flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
+     */
+    void setSuppressDot(boolean suppressDot, boolean animate) {
+        mSuppressDot = suppressDot;
+        updateDotVisibility(animate);
+    }
+
+    /** Sets the position of the 'new' dot, animating it out and back in if requested. */
+    void setDotPosition(boolean onLeft, boolean animate) {
+        if (animate && onLeft != mBadgedImageView.getDotPosition() && !mSuppressDot) {
+            animateDot(false /* showDot */, () -> {
+                mBadgedImageView.setDotPosition(onLeft);
+                animateDot(true /* showDot */, null);
+            });
+        } else {
+            mBadgedImageView.setDotPosition(onLeft);
+        }
+    }
+
+    boolean getDotPositionOnLeft() {
+        return mBadgedImageView.getDotPosition();
     }
 
     /**
      * Animates the badge to show or hide.
      */
-    private void animateDot(boolean showDot) {
+    private void animateDot(boolean showDot, Runnable after) {
         if (mBadgedImageView.isShowingDot() != showDot) {
             mBadgedImageView.setShowDot(showDot);
             mBadgedImageView.clearAnimation();
@@ -152,9 +191,13 @@
                         fraction = showDot ? fraction : 1 - fraction;
                         mBadgedImageView.setDotScale(fraction);
                     }).withEndAction(() -> {
-                if (!showDot) {
-                    mBadgedImageView.setShowDot(false);
-                }
+                        if (!showDot) {
+                            mBadgedImageView.setShowDot(false);
+                        }
+
+                        if (after != null) {
+                            after.run();
+                        }
             }).start();
         }
     }
@@ -181,8 +224,13 @@
             mBadgedImageView.setImageDrawable(iconDrawable);
         }
         int badgeColor = determineDominateColor(iconDrawable, n.color);
+        mBadgeColor = badgeColor;
         mBadgedImageView.setDotColor(badgeColor);
-        animateDot(mEntry.showInShadeWhenBubble() /* showDot */);
+        animateDot(mEntry.showInShadeWhenBubble() /* showDot */, null /* after */);
+    }
+
+    int getBadgeColor() {
+        return mBadgeColor;
     }
 
     private Drawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index f937525..8529ed4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -225,8 +225,10 @@
     /**
      * Flings the stack starting with the given velocities, springing it to the nearest edge
      * afterward.
+     *
+     * @return The X value that the stack will end up at after the fling/spring.
      */
-    public void flingStackThenSpringToEdge(float x, float velX, float velY) {
+    public float flingStackThenSpringToEdge(float x, float velX, float velY) {
         final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2;
 
         final boolean stackShouldFlingLeft = stackOnLeftSide
@@ -281,6 +283,7 @@
                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
         mIsMovingFromFlinging = true;
+        return destinationRelativeX;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index a76c9dc..fd76a79 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -189,6 +189,7 @@
         mLockscreenGestureLogger.write(MetricsProto.MetricsEvent.ACTION_LS_LOCK,
                 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
         showTransientIndication(R.string.keyguard_indication_trust_disabled);
+        mKeyguardUpdateMonitor.onLockIconPressed();
         mLockPatternUtils.requireCredentialEntry(KeyguardUpdateMonitor.getCurrentUser());
 
         return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
index 0b83b69..712e962 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -262,7 +262,9 @@
      * Propagate {@link StatusBar} pulsing state.
      */
     public void setPulsing(boolean pulsing) {
-        mLockIcon.setPulsing(pulsing);
+        if (mLockIcon != null) {
+            mLockIcon.setPulsing(pulsing);
+        }
     }
 
     /**
@@ -270,14 +272,18 @@
      * @param wakeAndUnlock If the type is {@link BiometricUnlockController#isWakeAndUnlock()}
      */
     public void onBiometricAuthModeChanged(boolean wakeAndUnlock) {
-        mLockIcon.onBiometricAuthModeChanged(wakeAndUnlock);
+        if (mLockIcon != null) {
+            mLockIcon.onBiometricAuthModeChanged(wakeAndUnlock);
+        }
     }
 
     /**
      * Called after finished unlocking and the status bar window is already collapsed.
      */
     public void onKeyguardFadedAway() {
-        mLockIcon.onKeyguardFadedAway();
+        if (mLockIcon != null) {
+            mLockIcon.onKeyguardFadedAway();
+        }
     }
 
     public void setStatusBarView(PhoneStatusBarView statusBarView) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
new file mode 100644
index 0000000..173237f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.bubbles;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Color;
+import android.graphics.PointF;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class BubbleFlyoutViewTest extends SysuiTestCase {
+    private BubbleFlyoutView mFlyout;
+    private TextView mFlyoutText;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mFlyout = new BubbleFlyoutView(getContext());
+
+        mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text);
+    }
+
+    @Test
+    public void testShowFlyout_isVisible() {
+        mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null);
+        assertEquals("Hello", mFlyoutText.getText());
+        assertEquals(View.VISIBLE, mFlyout.getVisibility());
+        assertEquals(1f, mFlyoutText.getAlpha(), .01f);
+    }
+
+    @Test
+    public void testFlyoutHide_runsCallback() {
+        Runnable after = Mockito.mock(Runnable.class);
+        mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, after);
+        mFlyout.hideFlyout();
+
+        verify(after).run();
+    }
+
+    @Test
+    public void testSetCollapsePercent() {
+        mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null);
+
+        float initialTranslationZ = mFlyout.getTranslationZ();
+
+        mFlyout.setCollapsePercent(1f);
+        assertEquals(0f, mFlyoutText.getAlpha(), 0.01f);
+        assertNotSame(0f, mFlyoutText.getTranslationX()); // Should have moved to collapse.
+        assertTrue(mFlyout.getTranslationZ() < initialTranslationZ); // Should be descending.
+
+        mFlyout.setCollapsePercent(0f);
+        assertEquals(1f, mFlyoutText.getAlpha(), 0.01f);
+        assertEquals(0f, mFlyoutText.getTranslationX());
+        assertEquals(initialTranslationZ, mFlyout.getTranslationZ());
+
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java
deleted file mode 100644
index bafae6c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.bubbles;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.when;
-
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.widget.TextView;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class BubbleStackViewTest extends SysuiTestCase {
-    private BubbleStackView mStackView;
-    @Mock private Bubble mBubble;
-    @Mock private NotificationEntry mNotifEntry;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mStackView = new BubbleStackView(mContext, new BubbleData(getContext()), null);
-        mBubble.entry = mNotifEntry;
-    }
-
-    @Test
-    public void testAnimateInFlyoutForBubble() {
-        when(mNotifEntry.getUpdateMessage(any())).thenReturn("Test Flyout Message.");
-        mStackView.animateInFlyoutForBubble(mBubble);
-
-        assertEquals("Test Flyout Message.",
-                ((TextView) mStackView.findViewById(R.id.bubble_flyout_text)).getText());
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 6d9a77c..daee55b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -261,6 +261,7 @@
 
         longClickCaptor.getValue().onLongClick(mLockIcon);
         verify(mLockPatternUtils).requireCredentialEntry(anyInt());
+        verify(mKeyguardUpdateMonitor).onLockIconPressed();
     }
 
     @Test
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index facc299..f12bfc3 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -3497,7 +3497,8 @@
      */
     @Override
     public void startCaptivePortalAppInternal(Network network, Bundle appExtras) {
-        mContext.checkCallingOrSelfPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+        mContext.enforceCallingOrSelfPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                "ConnectivityService");
 
         final Intent appIntent = new Intent(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
         appIntent.putExtras(appExtras);
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index c646149..fc355b7 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -286,6 +286,7 @@
     private static final String ATTR_NICKNAME = "nickname";
     private static final String ATTR_USER_FLAGS = "userFlags";
     private static final String ATTR_CREATED_MILLIS = "createdMillis";
+    private static final String ATTR_LAST_SEEN_MILLIS = "lastSeenMillis";
     private static final String ATTR_LAST_TRIM_MILLIS = "lastTrimMillis";
     private static final String ATTR_LAST_BENCH_MILLIS = "lastBenchMillis";
 
@@ -1313,7 +1314,7 @@
     private void onVolumeStateChangedLocked(VolumeInfo vol, int oldState, int newState) {
         // Remember that we saw this volume so we're ready to accept user
         // metadata, or so we can annoy them when a private volume is ejected
-        if (vol.isMountedReadable() && !TextUtils.isEmpty(vol.fsUuid)) {
+        if (!TextUtils.isEmpty(vol.fsUuid)) {
             VolumeRecord rec = mRecords.get(vol.fsUuid);
             if (rec == null) {
                 rec = new VolumeRecord(vol.type, vol.fsUuid);
@@ -1323,14 +1324,15 @@
                     rec.nickname = vol.disk.getDescription();
                 }
                 mRecords.put(rec.fsUuid, rec);
-                writeSettingsLocked();
             } else {
                 // Handle upgrade case where we didn't store partition GUID
                 if (TextUtils.isEmpty(rec.partGuid)) {
                     rec.partGuid = vol.partGuid;
-                    writeSettingsLocked();
                 }
             }
+
+            rec.lastSeenMillis = System.currentTimeMillis();
+            writeSettingsLocked();
         }
 
         mCallbacks.notifyVolumeStateChanged(vol, oldState, newState);
@@ -1731,9 +1733,10 @@
         meta.partGuid = readStringAttribute(in, ATTR_PART_GUID);
         meta.nickname = readStringAttribute(in, ATTR_NICKNAME);
         meta.userFlags = readIntAttribute(in, ATTR_USER_FLAGS);
-        meta.createdMillis = readLongAttribute(in, ATTR_CREATED_MILLIS);
-        meta.lastTrimMillis = readLongAttribute(in, ATTR_LAST_TRIM_MILLIS);
-        meta.lastBenchMillis = readLongAttribute(in, ATTR_LAST_BENCH_MILLIS);
+        meta.createdMillis = readLongAttribute(in, ATTR_CREATED_MILLIS, 0);
+        meta.lastSeenMillis = readLongAttribute(in, ATTR_LAST_SEEN_MILLIS, 0);
+        meta.lastTrimMillis = readLongAttribute(in, ATTR_LAST_TRIM_MILLIS, 0);
+        meta.lastBenchMillis = readLongAttribute(in, ATTR_LAST_BENCH_MILLIS, 0);
         return meta;
     }
 
@@ -1745,6 +1748,7 @@
         writeStringAttribute(out, ATTR_NICKNAME, rec.nickname);
         writeIntAttribute(out, ATTR_USER_FLAGS, rec.userFlags);
         writeLongAttribute(out, ATTR_CREATED_MILLIS, rec.createdMillis);
+        writeLongAttribute(out, ATTR_LAST_SEEN_MILLIS, rec.lastSeenMillis);
         writeLongAttribute(out, ATTR_LAST_TRIM_MILLIS, rec.lastTrimMillis);
         writeLongAttribute(out, ATTR_LAST_BENCH_MILLIS, rec.lastBenchMillis);
         out.endTag(null, TAG_VOLUME);
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index f9aaf11..a7fb99f 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -140,6 +140,7 @@
         private boolean mCompleted;
         private Monitor mCurrentMonitor;
         private long mStartTime;
+        private int mPauseCount;
 
         HandlerChecker(Handler handler, String name, long waitMaxMillis) {
             mHandler = handler;
@@ -160,17 +161,18 @@
                 mMonitors.addAll(mMonitorQueue);
                 mMonitorQueue.clear();
             }
-            if (mMonitors.size() == 0 && mHandler.getLooper().getQueue().isPolling()) {
+            if ((mMonitors.size() == 0 && mHandler.getLooper().getQueue().isPolling())
+                    || (mPauseCount > 0)) {
+                // Don't schedule until after resume OR
                 // If the target looper has recently been polling, then
                 // there is no reason to enqueue our checker on it since that
                 // is as good as it not being deadlocked.  This avoid having
-                // to do a context switch to check the thread.  Note that we
-                // only do this if mCheckReboot is false and we have no
-                // monitors, since those would need to be executed at this point.
+                // to do a context switch to check the thread. Note that we
+                // only do this if we have no monitors since those would need to
+                // be executed at this point.
                 mCompleted = true;
                 return;
             }
-
             if (!mCompleted) {
                 // we already have a check in flight, so no need
                 return;
@@ -236,6 +238,28 @@
                 mCurrentMonitor = null;
             }
         }
+
+        /** Pause the HandlerChecker. */
+        public void pauseLocked(String reason) {
+            mPauseCount++;
+            // Mark as completed, because there's a chance we called this after the watchog
+            // thread loop called Object#wait after 'WAITED_HALF'. In that case we want to ensure
+            // the next call to #getCompletionStateLocked for this checker returns 'COMPLETED'
+            mCompleted = true;
+            Slog.i(TAG, "Pausing HandlerChecker: " + mName + " for reason: "
+                    + reason + ". Pause count: " + mPauseCount);
+        }
+
+        /** Resume the HandlerChecker from the last {@link #pauseLocked}. */
+        public void resumeLocked(String reason) {
+            if (mPauseCount > 0) {
+                mPauseCount--;
+                Slog.i(TAG, "Resuming HandlerChecker: " + mName + " for reason: "
+                        + reason + ". Pause count: " + mPauseCount);
+            } else {
+                Slog.wtf(TAG, "Already resumed HandlerChecker: " + mName);
+            }
+        }
     }
 
     final class RebootRequestReceiver extends BroadcastReceiver {
@@ -364,6 +388,51 @@
     }
 
     /**
+     * Pauses Watchdog action for the currently running thread. Useful before executing long running
+     * operations that could falsely trigger the watchdog. Each call to this will require a matching
+     * call to {@link #resumeWatchingCurrentThread}.
+     *
+     * <p>If the current thread has not been added to the Watchdog, this call is a no-op.
+     *
+     * <p>If the Watchdog is already paused for the current thread, this call adds
+     * adds another pause and will require an additional {@link #resumeCurrentThread} to resume.
+     *
+     * <p>Note: Use with care, as any deadlocks on the current thread will be undetected until all
+     * pauses have been resumed.
+     */
+    public void pauseWatchingCurrentThread(String reason) {
+        synchronized (this) {
+            for (HandlerChecker hc : mHandlerCheckers) {
+                if (Thread.currentThread().equals(hc.getThread())) {
+                    hc.pauseLocked(reason);
+                }
+            }
+        }
+    }
+
+    /**
+     * Resumes the last pause from {@link #pauseWatchingCurrentThread} for the currently running
+     * thread.
+     *
+     * <p>If the current thread has not been added to the Watchdog, this call is a no-op.
+     *
+     * <p>If the Watchdog action for the current thread is already resumed, this call logs a wtf.
+     *
+     * <p>If all pauses have been resumed, the Watchdog action is finally resumed, otherwise,
+     * the Watchdog action for the current thread remains paused until resume is called at least
+     * as many times as the calls to pause.
+     */
+    public void resumeWatchingCurrentThread(String reason) {
+        synchronized (this) {
+            for (HandlerChecker hc : mHandlerCheckers) {
+                if (Thread.currentThread().equals(hc.getThread())) {
+                    hc.resumeLocked(reason);
+                }
+            }
+        }
+    }
+
+    /**
      * Perform a full reboot of the system.
      */
     void rebootSystem(String reason) {
diff --git a/services/core/java/com/android/server/am/PreBootBroadcaster.java b/services/core/java/com/android/server/am/PreBootBroadcaster.java
index 376999d..beb0e47 100644
--- a/services/core/java/com/android/server/am/PreBootBroadcaster.java
+++ b/services/core/java/com/android/server/am/PreBootBroadcaster.java
@@ -107,9 +107,11 @@
         EventLogTags.writeAmPreBoot(mUserId, componentName.getPackageName());
 
         mIntent.setComponent(componentName);
-        mService.broadcastIntentLocked(null, null, mIntent, null, this, 0, null, null, null,
-                AppOpsManager.OP_NONE, null, true, false, ActivityManagerService.MY_PID,
-                Process.SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), mUserId);
+        synchronized (mService) {
+            mService.broadcastIntentLocked(null, null, mIntent, null, this, 0, null, null, null,
+                    AppOpsManager.OP_NONE, null, true, false, ActivityManagerService.MY_PID,
+                    Process.SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), mUserId);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 696697e..b394eea 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -1658,23 +1658,15 @@
         app.killed = false;
         final long startSeq = app.startSeq = ++mProcStartSeqCounter;
         app.setStartParams(uid, hostingRecord, seInfo, startTime);
+        app.setUsingWrapper(invokeWith != null
+                || SystemProperties.get("wrap." + app.processName) != null);
+        mPendingStarts.put(startSeq, app);
+
         if (mService.mConstants.FLAG_PROCESS_START_ASYNC) {
             if (DEBUG_PROCESSES) Slog.i(TAG_PROCESSES,
                     "Posting procStart msg for " + app.toShortString());
             mService.mProcStartHandler.post(() -> {
                 try {
-                    synchronized (mService) {
-                        final String reason = isProcStartValidLocked(app, startSeq);
-                        if (reason != null) {
-                            Slog.w(TAG_PROCESSES, app + " not valid anymore,"
-                                    + " don't start process, " + reason);
-                            app.pendingStart = false;
-                            return;
-                        }
-                        app.setUsingWrapper(invokeWith != null
-                                || SystemProperties.get("wrap." + app.processName) != null);
-                        mPendingStarts.put(startSeq, app);
-                    }
                     final Process.ProcessStartResult startResult = startProcess(app.hostingRecord,
                             entryPoint, app, app.startUid, gids, runtimeFlags, mountExternal,
                             app.seInfo, requiredAbi, instructionSet, invokeWith, app.startTime);
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 88919df..b9a6a10 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -760,7 +760,6 @@
         @Override // Binder call
         public int canAuthenticate(String opPackageName) {
             checkPermission();
-            checkAppOp(opPackageName, Binder.getCallingUid());
 
             final int userId = UserHandle.getCallingUserId();
             final long ident = Binder.clearCallingIdentity();
@@ -833,9 +832,9 @@
     }
 
     private void checkPermission() {
-        if (getContext().checkCallingPermission(USE_FINGERPRINT)
+        if (getContext().checkCallingOrSelfPermission(USE_FINGERPRINT)
                 != PackageManager.PERMISSION_GRANTED) {
-            getContext().enforceCallingPermission(USE_BIOMETRIC,
+            getContext().enforceCallingOrSelfPermission(USE_BIOMETRIC,
                     "Must have USE_BIOMETRIC permission");
         }
     }
diff --git a/services/core/java/com/android/server/connectivity/PermissionMonitor.java b/services/core/java/com/android/server/connectivity/PermissionMonitor.java
index f8582cd..d05369e9 100644
--- a/services/core/java/com/android/server/connectivity/PermissionMonitor.java
+++ b/services/core/java/com/android/server/connectivity/PermissionMonitor.java
@@ -650,7 +650,7 @@
                 case INetd.PERMISSION_UPDATE_DEVICE_STATS:
                     updateStatsPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
                     break;
-                case INetd.NO_PERMISSIONS:
+                case INetd.PERMISSION_NONE:
                     noPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
                     break;
                 case INetd.PERMISSION_UNINSTALLED:
@@ -676,7 +676,7 @@
                         ArrayUtils.convertToIntArray(updateStatsPermissionAppIds));
             }
             if (noPermissionAppIds.size() != 0) {
-                mNetd.trafficSetNetPermForUids(INetd.NO_PERMISSIONS,
+                mNetd.trafficSetNetPermForUids(INetd.PERMISSION_NONE,
                         ArrayUtils.convertToIntArray(noPermissionAppIds));
             }
             if (uninstalledAppIds.size() != 0) {
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index b676618..35a82ae 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -49,6 +49,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManagerInternal;
+import android.content.pm.ParceledListSlice;
 import android.content.pm.ServiceInfo;
 import android.database.ContentObserver;
 import android.net.Uri;
@@ -1625,6 +1626,15 @@
     }
 
     /**
+     * Maximum time buffer in which JobScheduler will try to optimize periodic job scheduling. This
+     * does not cause a job's period to be larger than requested (eg: if the requested period is
+     * shorter than this buffer). This is used to put a limit on when JobScheduler will intervene
+     * and try to optimize scheduling if the current job finished less than this amount of time to
+     * the start of the next period
+     */
+    private static final long PERIODIC_JOB_WINDOW_BUFFER = 30 * MINUTE_IN_MILLIS;
+
+    /**
      * Called after a periodic has executed so we can reschedule it. We take the last execution
      * time of the job to be the time of completion (i.e. the time at which this function is
      * called).
@@ -1644,16 +1654,18 @@
         final long period = periodicToReschedule.getJob().getIntervalMillis();
         final long latestRunTimeElapsed = periodicToReschedule.getOriginalLatestRunTimeElapsed();
         final long flex = periodicToReschedule.getJob().getFlexMillis();
+        long rescheduleBuffer = 0;
 
+        final long diffMs = Math.abs(elapsedNow - latestRunTimeElapsed);
         if (elapsedNow > latestRunTimeElapsed) {
             // The job ran past its expected run window. Have it count towards the current window
             // and schedule a new job for the next window.
             if (DEBUG) {
                 Slog.i(TAG, "Periodic job ran after its intended window.");
             }
-            final long diffMs = (elapsedNow - latestRunTimeElapsed);
             int numSkippedWindows = (int) (diffMs / period) + 1; // +1 to include original window
-            if (period != flex && diffMs > Math.min(30 * MINUTE_IN_MILLIS, (period - flex) / 2)) {
+            if (period != flex && diffMs > Math.min(PERIODIC_JOB_WINDOW_BUFFER,
+                    (period - flex) / 2)) {
                 if (DEBUG) {
                     Slog.d(TAG, "Custom flex job ran too close to next window.");
                 }
@@ -1664,9 +1676,15 @@
             newLatestRuntimeElapsed = latestRunTimeElapsed + (period * numSkippedWindows);
         } else {
             newLatestRuntimeElapsed = latestRunTimeElapsed + period;
+            if (diffMs < PERIODIC_JOB_WINDOW_BUFFER && diffMs < period / 6) {
+                // Add a little buffer to the start of the next window so the job doesn't run
+                // too soon after this completed one.
+                rescheduleBuffer = Math.min(PERIODIC_JOB_WINDOW_BUFFER, period / 6 - diffMs);
+            }
         }
 
-        final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed - flex;
+        final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed
+                - Math.min(flex, period - rescheduleBuffer);
 
         if (DEBUG) {
             Slog.v(TAG, "Rescheduling executed periodic. New execution window [" +
@@ -2764,12 +2782,12 @@
         }
 
         @Override
-        public List<JobInfo> getAllPendingJobs() throws RemoteException {
+        public ParceledListSlice<JobInfo> getAllPendingJobs() throws RemoteException {
             final int uid = Binder.getCallingUid();
 
             long ident = Binder.clearCallingIdentity();
             try {
-                return JobSchedulerService.this.getPendingJobs(uid);
+                return new ParceledListSlice<>(JobSchedulerService.this.getPendingJobs(uid));
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -2905,7 +2923,7 @@
          * <p class="note">This is a slow operation, so it should be called sparingly.
          */
         @Override
-        public List<JobSnapshot> getAllJobSnapshots() {
+        public ParceledListSlice<JobSnapshot> getAllJobSnapshots() {
             final int uid = Binder.getCallingUid();
             if (uid != Process.SYSTEM_UID) {
                 throw new SecurityException(
@@ -2916,7 +2934,7 @@
                 mJobs.forEachJob((job) -> snapshots.add(
                         new JobSnapshot(job.getJob(), job.getSatisfiedConstraintFlags(),
                                 isReadyToBeExecutedLocked(job))));
-                return snapshots;
+                return new ParceledListSlice<>(snapshots);
             }
         }
     };
diff --git a/services/core/java/com/android/server/location/GnssConfiguration.java b/services/core/java/com/android/server/location/GnssConfiguration.java
index bd6662d..aa51aec 100644
--- a/services/core/java/com/android/server/location/GnssConfiguration.java
+++ b/services/core/java/com/android/server/location/GnssConfiguration.java
@@ -317,8 +317,10 @@
         if (configManager == null) {
             return;
         }
-        PersistableBundle configs = configManager.getConfigForSubId(
-                SubscriptionManager.getDefaultDataSubscriptionId());
+
+        int ddSubId = SubscriptionManager.getDefaultDataSubscriptionId();
+        PersistableBundle configs = SubscriptionManager.isValidSubscriptionId(ddSubId)
+                ? configManager.getConfigForSubId(ddSubId) : null;
         if (configs == null) {
             if (DEBUG) Log.d(TAG, "SIM not ready, use default carrier config.");
             configs = CarrierConfigManager.getDefaultConfig();
diff --git a/services/core/java/com/android/server/location/GnssLocationProvider.java b/services/core/java/com/android/server/location/GnssLocationProvider.java
index 5b7eca6..b2315c7 100644
--- a/services/core/java/com/android/server/location/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/GnssLocationProvider.java
@@ -60,7 +60,6 @@
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
-import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
 import android.telephony.TelephonyManager;
 import android.telephony.gsm.GsmCellLocation;
 import android.text.TextUtils;
@@ -75,6 +74,7 @@
 import com.android.internal.location.ProviderProperties;
 import com.android.internal.location.ProviderRequest;
 import com.android.internal.location.gnssmetrics.GnssMetrics;
+import com.android.internal.telephony.TelephonyIntents;
 import com.android.server.location.GnssSatelliteBlacklistHelper.GnssSatelliteBlacklistCallback;
 import com.android.server.location.NtpTimeHelper.InjectNtpTimeCallback;
 
@@ -184,7 +184,6 @@
     private static final int DOWNLOAD_PSDS_DATA = 6;
     private static final int UPDATE_LOCATION = 7;  // Handle external location from network listener
     private static final int DOWNLOAD_PSDS_DATA_FINISHED = 11;
-    private static final int SUBSCRIPTION_OR_CARRIER_CONFIG_CHANGED = 12;
     private static final int INITIALIZE_HANDLER = 13;
     private static final int REQUEST_LOCATION = 16;
     private static final int REPORT_LOCATION = 17; // HAL reports location
@@ -484,22 +483,13 @@
                     updateLowPowerMode();
                     break;
                 case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED:
+                case TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED:
                     subscriptionOrCarrierConfigChanged(context);
                     break;
             }
         }
     };
 
-    // TODO: replace OnSubscriptionsChangedListener with ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED
-    //       broadcast receiver.
-    private final OnSubscriptionsChangedListener mOnSubscriptionsChangedListener =
-            new OnSubscriptionsChangedListener() {
-                @Override
-                public void onSubscriptionsChanged() {
-                    sendMessage(SUBSCRIPTION_OR_CARRIER_CONFIG_CHANGED, 0, null);
-                }
-            };
-
     /**
      * Implements {@link GnssSatelliteBlacklistCallback#onUpdateSatelliteBlacklist}.
      */
@@ -515,12 +505,15 @@
                 mContext.getSystemService(Context.TELEPHONY_SERVICE);
         CarrierConfigManager configManager = (CarrierConfigManager)
                 mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
-        String mccMnc = phone.getSimOperator();
+        int ddSubId = SubscriptionManager.getDefaultDataSubscriptionId();
+        String mccMnc = SubscriptionManager.isValidSubscriptionId(ddSubId)
+                ? phone.getSimOperator(ddSubId) : phone.getSimOperator();
         boolean isKeepLppProfile = false;
         if (!TextUtils.isEmpty(mccMnc)) {
             if (DEBUG) Log.d(TAG, "SIM MCC/MNC is available: " + mccMnc);
             if (configManager != null) {
-                PersistableBundle b = configManager.getConfig();
+                PersistableBundle b = SubscriptionManager.isValidSubscriptionId(ddSubId)
+                        ? configManager.getConfigForSubId(ddSubId) : null;
                 if (b != null) {
                     isKeepLppProfile =
                             b.getBoolean(CarrierConfigManager.Gps.KEY_PERSIST_LPP_MODE_BOOL);
@@ -539,7 +532,6 @@
                 SystemProperties.set(GnssConfiguration.LPP_PROFILE, "");
             }
             reloadGpsProperties();
-            mNIHandler.setSuplEsEnabled(mSuplEsEnabled);
         } else {
             if (DEBUG) Log.d(TAG, "SIM MCC/MNC is still not available");
         }
@@ -577,9 +569,9 @@
         mC2KServerPort = mGnssConfiguration.getC2KPort(TCP_MIN_PORT);
         mNIHandler.setEmergencyExtensionSeconds(mGnssConfiguration.getEsExtensionSec());
         mSuplEsEnabled = mGnssConfiguration.getSuplEs(0) == 1;
+        mNIHandler.setSuplEsEnabled(mSuplEsEnabled);
         if (mGnssVisibilityControl != null) {
-            mGnssVisibilityControl.updateProxyApps(mGnssConfiguration.getProxyApps());
-            mGnssVisibilityControl.setEsNotify(mGnssConfiguration.getEsNotify(0));
+            mGnssVisibilityControl.onConfigurationUpdated(mGnssConfiguration);
         }
     }
 
@@ -1892,28 +1884,34 @@
         TelephonyManager phone = (TelephonyManager)
                 mContext.getSystemService(Context.TELEPHONY_SERVICE);
         int type = AGPS_SETID_TYPE_NONE;
-        String data = "";
+        String setId = null;
 
+        int ddSubId = SubscriptionManager.getDefaultDataSubscriptionId();
         if ((flags & AGPS_RIL_REQUEST_SETID_IMSI) == AGPS_RIL_REQUEST_SETID_IMSI) {
-            String data_temp = phone.getSubscriberId();
-            if (data_temp == null) {
-                // This means the framework does not have the SIM card ready.
-            } else {
+            if (SubscriptionManager.isValidSubscriptionId(ddSubId)) {
+                setId = phone.getSubscriberId(ddSubId);
+            }
+            if (setId == null) {
+                setId = phone.getSubscriberId();
+            }
+            if (setId != null) {
                 // This means the framework has the SIM card.
-                data = data_temp;
                 type = AGPS_SETID_TYPE_IMSI;
             }
         } else if ((flags & AGPS_RIL_REQUEST_SETID_MSISDN) == AGPS_RIL_REQUEST_SETID_MSISDN) {
-            String data_temp = phone.getLine1Number();
-            if (data_temp == null) {
-                // This means the framework does not have the SIM card ready.
-            } else {
+            if (SubscriptionManager.isValidSubscriptionId(ddSubId)) {
+                setId = phone.getLine1Number(ddSubId);
+            }
+            if (setId == null) {
+                setId = phone.getLine1Number();
+            }
+            if (setId != null) {
                 // This means the framework has the SIM card.
-                data = data_temp;
                 type = AGPS_SETID_TYPE_MSISDN;
             }
         }
-        native_agps_set_id(type, data);
+
+        native_agps_set_id(type, (setId == null) ? "" : setId);
     }
 
     @NativeEntryPoint
@@ -2025,9 +2023,6 @@
                 case UPDATE_LOCATION:
                     handleUpdateLocation((Location) msg.obj);
                     break;
-                case SUBSCRIPTION_OR_CARRIER_CONFIG_CHANGED:
-                    subscriptionOrCarrierConfigChanged(mContext);
-                    break;
                 case INITIALIZE_HANDLER:
                     handleInitialize();
                     break;
@@ -2066,17 +2061,6 @@
             // (this configuration might change in the future based on SIM changes)
             reloadGpsProperties();
 
-            // TODO: When this object "finishes" we should unregister by invoking
-            // SubscriptionManager.getInstance(mContext).unregister
-            // (mOnSubscriptionsChangedListener);
-            // This is not strictly necessary because it will be unregistered if the
-            // notification fails but it is good form.
-
-            // Register for SubscriptionInfo list changes which is guaranteed
-            // to invoke onSubscriptionsChanged the first time.
-            SubscriptionManager.from(mContext)
-                    .addOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
-
             // listen for events
             IntentFilter intentFilter = new IntentFilter();
             intentFilter.addAction(ALARM_WAKEUP);
@@ -2086,6 +2070,7 @@
             intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
             intentFilter.addAction(Intent.ACTION_SCREEN_ON);
             intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+            intentFilter.addAction(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
             mContext.registerReceiver(mBroadcastReceiver, intentFilter, null, this);
 
             mNetworkConnectivityHandler.registerNetworkCallbacks();
@@ -2164,8 +2149,6 @@
                 return "DOWNLOAD_PSDS_DATA_FINISHED";
             case UPDATE_LOCATION:
                 return "UPDATE_LOCATION";
-            case SUBSCRIPTION_OR_CARRIER_CONFIG_CHANGED:
-                return "SUBSCRIPTION_OR_CARRIER_CONFIG_CHANGED";
             case INITIALIZE_HANDLER:
                 return "INITIALIZE_HANDLER";
             case REPORT_LOCATION:
diff --git a/services/core/java/com/android/server/location/GnssVisibilityControl.java b/services/core/java/com/android/server/location/GnssVisibilityControl.java
index a3670a4..c49d900 100644
--- a/services/core/java/com/android/server/location/GnssVisibilityControl.java
+++ b/services/core/java/com/android/server/location/GnssVisibilityControl.java
@@ -76,7 +76,7 @@
     private final GpsNetInitiatedHandler mNiHandler;
 
     private boolean mIsGpsEnabled;
-    private volatile boolean mEsNotify;
+    private boolean mEsNotify;
 
     // Number of non-framework location access proxy apps is expected to be small (< 5).
     private static final int ARRAY_MAP_INITIAL_CAPACITY_PROXY_APP_TO_LOCATION_PERMISSIONS = 7;
@@ -124,10 +124,6 @@
         }
     }
 
-    void updateProxyApps(List<String> nfwLocationAccessProxyApps) {
-        runOnHandler(() -> handleUpdateProxyApps(nfwLocationAccessProxyApps));
-    }
-
     void reportNfwNotification(String proxyAppPackageName, byte protocolStack,
             String otherProtocolStackName, byte requestor, String requestorId, byte responseType,
             boolean inEmergencyMode, boolean isCachedLocation) {
@@ -136,15 +132,25 @@
                         requestor, requestorId, responseType, inEmergencyMode, isCachedLocation)));
     }
 
-    void setEsNotify(int esNotifyConfig) {
-        if (esNotifyConfig != ES_NOTIFY_NONE && esNotifyConfig != ES_NOTIFY_ALL) {
+    void onConfigurationUpdated(GnssConfiguration configuration) {
+        // The configuration object must be accessed only in the caller thread and not in mHandler.
+        List<String> nfwLocationAccessProxyApps = configuration.getProxyApps();
+        int esNotify = configuration.getEsNotify(ES_NOTIFY_NONE);
+        runOnHandler(() -> {
+            setEsNotify(esNotify);
+            handleUpdateProxyApps(nfwLocationAccessProxyApps);
+        });
+    }
+
+    private void setEsNotify(int esNotify) {
+        if (esNotify != ES_NOTIFY_NONE && esNotify != ES_NOTIFY_ALL) {
             Log.e(TAG, "Config parameter " + GnssConfiguration.CONFIG_ES_NOTIFY_INT
-                    + " is set to invalid value: " + esNotifyConfig
+                    + " is set to invalid value: " + esNotify
                     + ". Using default value: " + ES_NOTIFY_NONE);
-            esNotifyConfig = ES_NOTIFY_NONE;
+            esNotify = ES_NOTIFY_NONE;
         }
 
-        mEsNotify = (esNotifyConfig == ES_NOTIFY_ALL);
+        mEsNotify = (esNotify == ES_NOTIFY_ALL);
     }
 
     private void handleInitialize() {
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index c2125b0..b246eb6 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -421,8 +421,9 @@
                     new PasswordSlotManager());
         }
 
-        public boolean hasBiometrics() {
-            return BiometricManager.hasBiometrics(mContext);
+        public boolean hasEnrolledBiometrics() {
+            BiometricManager bm = mContext.getSystemService(BiometricManager.class);
+            return bm.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS;
         }
 
         public int binderGetCallingUid() {
@@ -2502,7 +2503,8 @@
         // TODO: When lockout is handled under the HAL for all biometrics (fingerprint),
         // we need to generate challenge for each one, have it signed by GK and reset lockout
         // for each modality.
-        if (!hasChallenge && pm.hasSystemFeature(PackageManager.FEATURE_FACE)) {
+        if (!hasChallenge && pm.hasSystemFeature(PackageManager.FEATURE_FACE)
+                && mInjector.hasEnrolledBiometrics()) {
             challenge = mContext.getSystemService(FaceManager.class).generateChallenge();
         }
 
@@ -2544,8 +2546,8 @@
         if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
             notifyActivePasswordMetricsAvailable(credentialType, userCredential, userId);
             unlockKeystore(authResult.authToken.deriveKeyStorePassword(), userId);
-            // Reset lockout
-            if (mInjector.hasBiometrics()) {
+            // Reset lockout only if user has enrolled templates
+            if (mInjector.hasEnrolledBiometrics()) {
                 BiometricManager bm = mContext.getSystemService(BiometricManager.class);
                 Slog.i(TAG, "Resetting lockout, length: "
                         + authResult.gkResponse.getPayload().length);
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordCrypto.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordCrypto.java
index ef94000..388e51f 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordCrypto.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordCrypto.java
@@ -30,6 +30,7 @@
 import java.security.SecureRandom;
 import java.security.UnrecoverableKeyException;
 import java.security.cert.CertificateException;
+import java.security.spec.InvalidParameterSpecException;
 import java.util.Arrays;
 
 import javax.crypto.BadPaddingException;
@@ -43,6 +44,7 @@
 
 public class SyntheticPasswordCrypto {
     private static final int PROFILE_KEY_IV_SIZE = 12;
+    private static final int DEFAULT_TAG_LENGTH_BITS = 128;
     private static final int AES_KEY_LENGTH = 32; // 256-bit AES key
     private static final byte[] APPLICATION_ID_PERSONALIZATION = "application-id".getBytes();
     // Time between the user credential is verified with GK and the decryption of synthetic password
@@ -60,13 +62,14 @@
         byte[] ciphertext = Arrays.copyOfRange(blob, PROFILE_KEY_IV_SIZE, blob.length);
         Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                 + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE);
-        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
+        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(DEFAULT_TAG_LENGTH_BITS, iv));
         return cipher.doFinal(ciphertext);
     }
 
     private static byte[] encrypt(SecretKey key, byte[] blob)
             throws IOException, NoSuchAlgorithmException, NoSuchPaddingException,
-            InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+            InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
+            InvalidParameterSpecException {
         if (blob == null) {
             return null;
         }
@@ -79,6 +82,11 @@
         if (iv.length != PROFILE_KEY_IV_SIZE) {
             throw new RuntimeException("Invalid iv length: " + iv.length);
         }
+        final GCMParameterSpec spec = cipher.getParameters().getParameterSpec(
+                GCMParameterSpec.class);
+        if (spec.getTLen() != DEFAULT_TAG_LENGTH_BITS) {
+            throw new RuntimeException("Invalid tag length: " + spec.getTLen());
+        }
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         outputStream.write(iv);
         outputStream.write(ciphertext);
@@ -92,7 +100,8 @@
         try {
             return encrypt(key, message);
         } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
-                | IllegalBlockSizeException | BadPaddingException | IOException e) {
+                | IllegalBlockSizeException | BadPaddingException | IOException
+                | InvalidParameterSpecException e) {
             e.printStackTrace();
             return null;
         }
@@ -147,7 +156,7 @@
     public static byte[] createBlob(String keyAlias, byte[] data, byte[] applicationId, long sid) {
         try {
             KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
-            keyGenerator.init(new SecureRandom());
+            keyGenerator.init(AES_KEY_LENGTH * 8, new SecureRandom());
             SecretKey secretKey = keyGenerator.generateKey();
             KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
             keyStore.load(null);
@@ -169,7 +178,8 @@
         } catch (CertificateException | IOException | BadPaddingException
                 | IllegalBlockSizeException
                 | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException
-                | InvalidKeyException e) {
+                | InvalidKeyException
+                | InvalidParameterSpecException e) {
             e.printStackTrace();
             throw new RuntimeException("Failed to encrypt blob", e);
         }
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index bab612d..9d11596 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -73,6 +73,7 @@
 
     public static final int FLAG_STORAGE_DE = IInstalld.FLAG_STORAGE_DE;
     public static final int FLAG_STORAGE_CE = IInstalld.FLAG_STORAGE_CE;
+    public static final int FLAG_STORAGE_EXTERNAL = IInstalld.FLAG_STORAGE_EXTERNAL;
 
     public static final int FLAG_CLEAR_CACHE_ONLY = IInstalld.FLAG_CLEAR_CACHE_ONLY;
     public static final int FLAG_CLEAR_CODE_CACHE_ONLY = IInstalld.FLAG_CLEAR_CODE_CACHE_ONLY;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9d70209..23aa8f0 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -89,6 +89,7 @@
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
+import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL;
 
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_PARENT;
@@ -3249,7 +3250,7 @@
                     if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, ps.volumeUuid)) {
                         // No apps are running this early, so no need to freeze
                         clearAppDataLIF(ps.pkg, UserHandle.USER_ALL,
-                                StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE
+                                FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
                                         | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
                     }
                 }
@@ -3502,8 +3503,8 @@
                 }
                 return false;
             }
-            clearAppDataLIF(pkg, UserHandle.USER_ALL, FLAG_STORAGE_DE
-                    | FLAG_STORAGE_CE | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+            clearAppDataLIF(pkg, UserHandle.USER_ALL, FLAG_STORAGE_DE | FLAG_STORAGE_CE
+                    | FLAG_STORAGE_EXTERNAL | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
             mDexManager.notifyPackageUpdated(pkg.packageName,
                     pkg.baseCodePath, pkg.splitCodePaths);
         }
@@ -15975,6 +15976,9 @@
             synchronized (mInstallLock) {
                 // Clean up both app data and code
                 // All package moves are frozen until finished
+
+                // We purposefully exclude FLAG_STORAGE_EXTERNAL here, since
+                // this task was only focused on moving data on internal storage.
                 for (int userId : userIds) {
                     try {
                         mInstaller.destroyAppData(volumeUuid, move.packageName, userId,
@@ -17075,8 +17079,8 @@
             final String packageName = pkg.packageName;
             prepareAppDataAfterInstallLIF(pkg);
             if (reconciledPkg.prepareResult.clearCodeCache) {
-                clearAppDataLIF(pkg, UserHandle.USER_ALL, StorageManager.FLAG_STORAGE_DE
-                        | StorageManager.FLAG_STORAGE_CE | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+                clearAppDataLIF(pkg, UserHandle.USER_ALL, FLAG_STORAGE_DE | FLAG_STORAGE_CE
+                        | FLAG_STORAGE_EXTERNAL | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
             }
             if (reconciledPkg.prepareResult.replace) {
                 mDexManager.notifyPackageUpdated(pkg.packageName,
@@ -18848,7 +18852,7 @@
                 resolvedPkg.setVolumeUuid(deletedPs.volumeUuid);
             }
             destroyAppDataLIF(resolvedPkg, UserHandle.USER_ALL,
-                    StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
+                    FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
             destroyAppProfilesLIF(resolvedPkg);
             if (outInfo != null) {
                 outInfo.dataRemoved = true;
@@ -19600,7 +19604,7 @@
             }
 
             destroyAppDataLIF(pkg, nextUserId,
-                    StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
+                    FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
             clearDefaultBrowserIfNeededForUser(ps.name, nextUserId);
             removeKeystoreDataIfNeeded(nextUserId, ps.appId);
             final SparseBooleanArray changedUsers = new SparseBooleanArray();
@@ -19736,7 +19740,7 @@
         }
 
         clearAppDataLIF(pkg, userId,
-                StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
+                FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
 
         final int appId = UserHandle.getAppId(pkg.applicationInfo.uid);
         removeKeystoreDataIfNeeded(userId, appId);
@@ -19967,8 +19971,7 @@
             }
             if (doClearData) {
                 synchronized (mInstallLock) {
-                    final int flags = StorageManager.FLAG_STORAGE_DE
-                            | StorageManager.FLAG_STORAGE_CE;
+                    final int flags = FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL;
                     // We're only clearing cache files, so we don't care if the
                     // app is unfrozen and still able to run
                     clearAppDataLIF(pkg, userId, flags | Installer.FLAG_CLEAR_CACHE_ONLY);
@@ -22516,9 +22519,8 @@
                 }
 
                 if (!Build.FINGERPRINT.equals(ver.fingerprint)) {
-                    clearAppDataLIF(ps.pkg, UserHandle.USER_ALL,
-                            StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE
-                                    | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+                    clearAppDataLIF(ps.pkg, UserHandle.USER_ALL, FLAG_STORAGE_DE | FLAG_STORAGE_CE
+                            | FLAG_STORAGE_EXTERNAL | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 37c1aaa..b2ba290 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -929,6 +929,7 @@
                 final BasePermission bp = mSettings.getPermissionLocked(permName);
                 final boolean appSupportsRuntimePermissions =
                         pkg.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M;
+                String upgradedActivityRecognitionPermission = null;
 
                 if (DEBUG_INSTALL) {
                     Log.i(TAG, "Package " + pkg.packageName + " checking " + permName + ": " + bp);
@@ -947,11 +948,40 @@
                 // Cache newImplicitPermissions before modifing permissionsState as for the shared
                 // uids the original and new state are the same object
                 if (!origPermissions.hasRequestedPermission(permName)
-                        && pkg.implicitPermissions.contains(permName)) {
-                    newImplicitPermissions.add(permName);
+                        && (pkg.implicitPermissions.contains(permName)
+                                || (permName.equals(Manifest.permission.ACTIVITY_RECOGNITION)))) {
+                    if (pkg.implicitPermissions.contains(permName)) {
+                        // If permName is an implicit permission, try to auto-grant
+                        newImplicitPermissions.add(permName);
 
-                    if (DEBUG_PERMISSIONS) {
-                        Slog.i(TAG, permName + " is newly added for " + pkg.packageName);
+                        if (DEBUG_PERMISSIONS) {
+                            Slog.i(TAG, permName + " is newly added for " + pkg.packageName);
+                        }
+                    } else {
+                        // Special case for Activity Recognition permission. Even if AR permission
+                        // is not an implicit permission we want to add it to the list (try to
+                        // auto-grant it) if the app was installed on a device before AR permission
+                        // was split, regardless of if the app now requests the new AR permission
+                        // or has updated its target SDK and AR is no longer implicit to it.
+                        // This is a compatibility workaround for apps when AR permission was
+                        // split in Q.
+                        int numSplitPerms = PermissionManager.SPLIT_PERMISSIONS.size();
+                        for (int splitPermNum = 0; splitPermNum < numSplitPerms; splitPermNum++) {
+                            PermissionManager.SplitPermissionInfo sp =
+                                    PermissionManager.SPLIT_PERMISSIONS.get(splitPermNum);
+                            String splitPermName = sp.getSplitPermission();
+                            if (sp.getNewPermissions().contains(permName)
+                                    && origPermissions.hasInstallPermission(splitPermName)) {
+                                upgradedActivityRecognitionPermission = splitPermName;
+                                newImplicitPermissions.add(permName);
+
+                                if (DEBUG_PERMISSIONS) {
+                                    Slog.i(TAG, permName + " is newly added for "
+                                            + pkg.packageName);
+                                }
+                                break;
+                            }
+                        }
                     }
                 }
 
@@ -985,7 +1015,8 @@
                     // For all apps normal permissions are install time ones.
                     grant = GRANT_INSTALL;
                 } else if (bp.isRuntime()) {
-                    if (origPermissions.hasInstallPermission(bp.getName())) {
+                    if (origPermissions.hasInstallPermission(bp.getName())
+                            || upgradedActivityRecognitionPermission != null) {
                         // Before Q we represented some runtime permissions as install permissions,
                         // in Q we cannot do this anymore. Hence upgrade them all.
                         grant = GRANT_UPGRADE;
@@ -1161,10 +1192,15 @@
                                     .getInstallPermissionState(perm);
                             int flags = (permState != null) ? permState.getFlags() : 0;
 
+                            BasePermission bpToRevoke =
+                                    upgradedActivityRecognitionPermission == null
+                                    ? bp : mSettings.getPermissionLocked(
+                                            upgradedActivityRecognitionPermission);
                             // Remove install permission
-                            if (origPermissions.revokeInstallPermission(bp)
+                            if (origPermissions.revokeInstallPermission(bpToRevoke)
                                     != PERMISSION_OPERATION_FAILURE) {
-                                origPermissions.updatePermissionFlags(bp, UserHandle.USER_ALL,
+                                origPermissions.updatePermissionFlags(bpToRevoke,
+                                        UserHandle.USER_ALL,
                                         (MASK_PERMISSION_FLAGS_ALL
                                                 & ~FLAG_PERMISSION_APPLY_RESTRICTION), 0);
                                 changedInstallPermission = true;
@@ -1489,9 +1525,11 @@
                     for (int userNum = 0; userNum < numUsers; userNum++) {
                         int userId = users[userNum];
 
-                        ps.updatePermissionFlags(bp, userId,
-                                FLAG_PERMISSION_REVOKE_WHEN_REQUESTED,
-                                FLAG_PERMISSION_REVOKE_WHEN_REQUESTED);
+                        if (!newPerm.equals(Manifest.permission.ACTIVITY_RECOGNITION)) {
+                            ps.updatePermissionFlags(bp, userId,
+                                    FLAG_PERMISSION_REVOKE_WHEN_REQUESTED,
+                                    FLAG_PERMISSION_REVOKE_WHEN_REQUESTED);
+                        }
                         updatedUserIds = ArrayUtils.appendInt(updatedUserIds, userId);
 
                         boolean inheritsFromInstallPerm = false;
diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java
index 1edb93a..7d0da68 100644
--- a/services/core/java/com/android/server/stats/StatsCompanionService.java
+++ b/services/core/java/com/android/server/stats/StatsCompanionService.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.stats;
 
+import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
 import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
 import static android.os.Process.getPidsForCommands;
@@ -33,6 +34,11 @@
 import android.app.ActivityManagerInternal;
 import android.app.AlarmManager;
 import android.app.AlarmManager.OnAlarmListener;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.HistoricalOps;
+import android.app.AppOpsManager.HistoricalOpsRequest;
+import android.app.AppOpsManager.HistoricalPackageOps;
+import android.app.AppOpsManager.HistoricalUidOps;
 import android.app.ProcessMemoryHighWaterMark;
 import android.app.ProcessMemoryState;
 import android.app.StatsManager;
@@ -146,6 +152,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -154,6 +162,7 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
@@ -1941,6 +1950,53 @@
         }
     }
 
+    private void pullAppOps(long elapsedNanos, final long wallClockNanos,
+            List<StatsLogEventWrapper> pulledData) {
+        long token = Binder.clearCallingIdentity();
+        try {
+            AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
+
+            CompletableFuture<HistoricalOps> ops = new CompletableFuture<>();
+            HistoricalOpsRequest histOpsRequest =
+                    new HistoricalOpsRequest.Builder(
+                            Instant.now().minus(1, ChronoUnit.HOURS).toEpochMilli(),
+                            Long.MAX_VALUE).build();
+            appOps.getHistoricalOps(histOpsRequest, mContext.getMainExecutor(), ops::complete);
+
+            HistoricalOps histOps = ops.get(EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS,
+                    TimeUnit.MILLISECONDS);
+
+            StatsLogEventWrapper e = new StatsLogEventWrapper(StatsLog.APP_OPS, elapsedNanos,
+                    wallClockNanos);
+
+            for (int uidIdx = 0; uidIdx < histOps.getUidCount(); uidIdx++) {
+                final HistoricalUidOps uidOps = histOps.getUidOpsAt(uidIdx);
+                final int uid = uidOps.getUid();
+                for (int pkgIdx = 0; pkgIdx < uidOps.getPackageCount(); pkgIdx++) {
+                    final HistoricalPackageOps packageOps = uidOps.getPackageOpsAt(pkgIdx);
+                    for (int opIdx = 0; opIdx < packageOps.getOpCount(); opIdx++) {
+                        final AppOpsManager.HistoricalOp op  = packageOps.getOpAt(opIdx);
+                        e.writeInt(uid);
+                        e.writeString(packageOps.getPackageName());
+                        e.writeInt(op.getOpCode());
+                        e.writeLong(op.getForegroundAccessCount(OP_FLAGS_ALL_TRUSTED));
+                        e.writeLong(op.getBackgroundAccessCount(OP_FLAGS_ALL_TRUSTED));
+                        e.writeLong(op.getForegroundRejectCount(OP_FLAGS_ALL_TRUSTED));
+                        e.writeLong(op.getBackgroundRejectCount(OP_FLAGS_ALL_TRUSTED));
+                        e.writeLong(op.getForegroundAccessDuration(OP_FLAGS_ALL_TRUSTED));
+                        e.writeLong(op.getBackgroundAccessDuration(OP_FLAGS_ALL_TRUSTED));
+                        pulledData.add(e);
+                    }
+                }
+            }
+        } catch (Throwable t) {
+            Log.e(TAG, "Could not read appops", t);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+
     /**
      * Add a RoleHolder atom for each package that holds a role.
      *
@@ -2331,6 +2387,10 @@
                 pullFaceSettings(tagId, elapsedNanos, wallClockNanos, ret);
                 break;
             }
+            case StatsLog.APP_OPS: {
+                pullAppOps(elapsedNanos, wallClockNanos, ret);
+                break;
+            }
             default:
                 Slog.w(TAG, "No such tagId data as " + tagId);
                 return null;
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index ba41586..3d59e66 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -281,8 +281,9 @@
         if (display != null && inSplitScreenPrimaryWindowingMode()) {
             // If we created a docked stack we want to resize it so it resizes all other stacks
             // in the system.
-            getStackDockedModeBounds(null, null, mTmpRect2, mTmpRect3);
-            mStackSupervisor.resizeDockedStackLocked(getRequestedOverrideBounds(), mTmpRect2,
+            getStackDockedModeBounds(null /* dockedBounds */, null /* currentTempTaskBounds */,
+                    mTmpRect /* outStackBounds */, mTmpRect2 /* outTempTaskBounds */);
+            mStackSupervisor.resizeDockedStackLocked(getRequestedOverrideBounds(), mTmpRect,
                     mTmpRect2, null, null, PRESERVE_WINDOWS);
         }
         mRootActivityContainer.updateUIDsPresentOnDisplay();
@@ -396,7 +397,6 @@
 
     private final Rect mTmpRect = new Rect();
     private final Rect mTmpRect2 = new Rect();
-    private final Rect mTmpRect3 = new Rect();
     private final ActivityOptions mTmpOptions = ActivityOptions.makeBasic();
 
     /** List for processing through a set of activities */
@@ -512,7 +512,6 @@
         mWindowManager = mService.mWindowManager;
         mStackId = stackId;
         mCurrentUser = mService.mAmInternal.getCurrentUserId();
-        mTmpRect2.setEmpty();
         // Set display id before setting activity and window type to make sure it won't affect
         // stacks on a wrong display.
         mDisplayId = display.mDisplayId;
@@ -572,90 +571,87 @@
     public void onConfigurationChanged(Configuration newParentConfig) {
         final int prevWindowingMode = getWindowingMode();
         final boolean prevIsAlwaysOnTop = isAlwaysOnTop();
-        final ActivityDisplay display = getDisplay();
         final int prevRotation = getWindowConfiguration().getRotation();
         final int prevDensity = getConfiguration().densityDpi;
         final int prevScreenW = getConfiguration().screenWidthDp;
         final int prevScreenH = getConfiguration().screenHeightDp;
-
-        getBounds(mTmpRect); // previous bounds
+        final Rect newBounds = mTmpRect;
+        // Initialize the new bounds by previous bounds as the input and output for calculating
+        // override bounds in pinned (pip) or split-screen mode.
+        getBounds(newBounds);
 
         super.onConfigurationChanged(newParentConfig);
-        if (display == null) {
-            return;
-        }
-        if (getTaskStack() == null) {
+        final ActivityDisplay display = getDisplay();
+        if (display == null || getTaskStack() == null) {
             return;
         }
 
+        final boolean windowingModeChanged = prevWindowingMode != getWindowingMode();
+        final int overrideWindowingMode = getRequestedOverrideWindowingMode();
         // Update bounds if applicable
         boolean hasNewOverrideBounds = false;
         // Use override windowing mode to prevent extra bounds changes if inheriting the mode.
-        if (getRequestedOverrideWindowingMode() == WINDOWING_MODE_PINNED) {
+        if (overrideWindowingMode == WINDOWING_MODE_PINNED) {
             // Pinned calculation already includes rotation
-            mTmpRect2.set(mTmpRect);
-            hasNewOverrideBounds = getTaskStack().calculatePinnedBoundsForConfigChange(mTmpRect2);
-        } else {
+            hasNewOverrideBounds = getTaskStack().calculatePinnedBoundsForConfigChange(newBounds);
+        } else if (!matchParentBounds()) {
+            // If the parent (display) has rotated, rotate our bounds to best-fit where their
+            // bounds were on the pre-rotated display.
             final int newRotation = getWindowConfiguration().getRotation();
-            if (!matchParentBounds()) {
-                // If the parent (display) has rotated, rotate our bounds to best-fit where their
-                // bounds were on the pre-rotated display.
-                if (prevRotation != newRotation) {
-                    mTmpRect2.set(mTmpRect);
-                    getDisplay().mDisplayContent
-                            .rotateBounds(newParentConfig.windowConfiguration.getBounds(),
-                                    prevRotation, newRotation, mTmpRect2);
-                    hasNewOverrideBounds = true;
-                }
+            final boolean rotationChanged = prevRotation != newRotation;
+            if (rotationChanged) {
+                display.mDisplayContent.rotateBounds(
+                        newParentConfig.windowConfiguration.getBounds(), prevRotation, newRotation,
+                        newBounds);
+                hasNewOverrideBounds = true;
+            }
 
+            // Use override windowing mode to prevent extra bounds changes if inheriting the mode.
+            if (overrideWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
+                    || overrideWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
                 // If entering split screen or if something about the available split area changes,
                 // recalculate the split windows to match the new configuration.
-                if (prevRotation != newRotation
+                if (rotationChanged || windowingModeChanged
                         || prevDensity != getConfiguration().densityDpi
-                        || prevWindowingMode != getWindowingMode()
                         || prevScreenW != getConfiguration().screenWidthDp
                         || prevScreenH != getConfiguration().screenHeightDp) {
-                    // Use override windowing mode to prevent extra bounds changes if inheriting
-                    // the mode.
-                    if (getRequestedOverrideWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
-                            || getRequestedOverrideWindowingMode()
-                            == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
-                        mTmpRect2.set(mTmpRect);
-                        getTaskStack()
-                                .calculateDockedBoundsForConfigChange(newParentConfig, mTmpRect2);
-                        hasNewOverrideBounds = true;
-                    }
+                    getTaskStack().calculateDockedBoundsForConfigChange(newParentConfig, newBounds);
+                    hasNewOverrideBounds = true;
                 }
             }
         }
-        if (getWindowingMode() != prevWindowingMode) {
+
+        if (windowingModeChanged) {
             // Use override windowing mode to prevent extra bounds changes if inheriting the mode.
-            if (getRequestedOverrideWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
-                getStackDockedModeBounds(null, null, mTmpRect2, mTmpRect3);
+            if (overrideWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
+                getStackDockedModeBounds(null /* dockedBounds */, null /* currentTempTaskBounds */,
+                        newBounds /* outStackBounds */, mTmpRect2 /* outTempTaskBounds */);
                 // immediately resize so docked bounds are available in onSplitScreenModeActivated
                 setTaskDisplayedBounds(null);
-                setTaskBounds(mTmpRect2);
-                setBounds(mTmpRect2);
-            } else if (
-                    getRequestedOverrideWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
+                setTaskBounds(newBounds);
+                setBounds(newBounds);
+                newBounds.set(newBounds);
+            } else if (overrideWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
                 Rect dockedBounds = display.getSplitScreenPrimaryStack().getBounds();
                 final boolean isMinimizedDock =
-                        getDisplay().mDisplayContent.getDockedDividerController().isMinimizedDock();
+                        display.mDisplayContent.getDockedDividerController().isMinimizedDock();
                 if (isMinimizedDock) {
                     TaskRecord topTask = display.getSplitScreenPrimaryStack().topTask();
                     if (topTask != null) {
                         dockedBounds = topTask.getBounds();
                     }
                 }
-                getStackDockedModeBounds(dockedBounds, null, mTmpRect2, mTmpRect3);
+                getStackDockedModeBounds(dockedBounds, null /* currentTempTaskBounds */,
+                        newBounds /* outStackBounds */, mTmpRect2 /* outTempTaskBounds */);
                 hasNewOverrideBounds = true;
             }
-        }
-        if (prevWindowingMode != getWindowingMode()) {
             display.onStackWindowingModeChanged(this);
         }
         if (hasNewOverrideBounds) {
-            mRootActivityContainer.resizeStack(this, mTmpRect2, null, null, PRESERVE_WINDOWS,
+            // Note the resizeStack may enter onConfigurationChanged recursively, so we make a copy
+            // of the temporary bounds (newBounds is mTmpRect) to avoid it being modified.
+            mRootActivityContainer.resizeStack(this, new Rect(newBounds), null /* tempTaskBounds */,
+                    null /* tempTaskInsetBounds */, PRESERVE_WINDOWS,
                     true /* allowResizeInDockedMode */, true /* deferResume */);
         }
         if (prevIsAlwaysOnTop != isAlwaysOnTop()) {
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index b8442a8..1cdb49d 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -46,7 +46,6 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.SuspendDialogInfo;
 import android.content.pm.UserInfo;
-import android.os.Binder;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -304,7 +303,7 @@
             return null;
         }
         // TODO(b/28935539): should allow certain activities to bypass work challenge
-        final IntentSender target = createIntentSenderForOriginalIntent(Binder.getCallingUid(),
+        final IntentSender target = createIntentSenderForOriginalIntent(mCallingUid,
                 FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE);
         final KeyguardManager km = (KeyguardManager) mServiceContext
                 .getSystemService(KEYGUARD_SERVICE);
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index fd90f03..ecbecba 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -2005,7 +2005,7 @@
         }
         layoutLetterbox(winHint);
         if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
-            mLetterbox.applySurfaceChanges(mPendingTransaction);
+            mLetterbox.applySurfaceChanges(getPendingTransaction());
         }
     }
 
@@ -3059,13 +3059,13 @@
 
         if (mSurfaceControl != null) {
             if (show && !mLastSurfaceShowing) {
-                mPendingTransaction.show(mSurfaceControl);
+                getPendingTransaction().show(mSurfaceControl);
             } else if (!show && mLastSurfaceShowing) {
-                mPendingTransaction.hide(mSurfaceControl);
+                getPendingTransaction().hide(mSurfaceControl);
             }
         }
         if (mThumbnail != null) {
-            mThumbnail.setShowing(mPendingTransaction, show);
+            mThumbnail.setShowing(getPendingTransaction(), show);
         }
         mLastSurfaceShowing = show;
         super.prepareSurfaces();
@@ -3225,8 +3225,8 @@
 
     private void updateColorTransform() {
         if (mSurfaceControl != null && mLastAppSaturationInfo != null) {
-            mPendingTransaction.setColorTransform(mSurfaceControl, mLastAppSaturationInfo.mMatrix,
-                    mLastAppSaturationInfo.mTranslation);
+            getPendingTransaction().setColorTransform(mSurfaceControl,
+                    mLastAppSaturationInfo.mMatrix, mLastAppSaturationInfo.mTranslation);
             mWmService.scheduleAnimationLocked();
         }
     }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 58a03b2..82ea4fe 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1480,7 +1480,7 @@
         // the top of the method, the caller is obligated to call computeNewConfigurationLocked().
         // By updating the Display info here it will be available to
         // #computeScreenConfiguration() later.
-        updateDisplayAndOrientation(getConfiguration().uiMode);
+        updateDisplayAndOrientation(getConfiguration().uiMode, null /* outConfig */);
 
         // NOTE: We disable the rotation in the emulator because
         //       it doesn't support hardware OpenGL emulation yet.
@@ -1578,7 +1578,7 @@
      * changed.
      * Do not call if {@link WindowManagerService#mDisplayReady} == false.
      */
-    private DisplayInfo updateDisplayAndOrientation(int uiMode) {
+    private DisplayInfo updateDisplayAndOrientation(int uiMode, Configuration outConfig) {
         // Use the effective "visual" dimensions based on current rotation
         final boolean rotated = (mRotation == ROTATION_90 || mRotation == ROTATION_270);
         final int dw = rotated ? mBaseDisplayHeight : mBaseDisplayWidth;
@@ -1610,6 +1610,9 @@
             mDisplayInfo.flags &= ~Display.FLAG_SCALING_DISABLED;
         }
 
+        computeSizeRangesAndScreenLayout(mDisplayInfo, rotated, uiMode, dw, dh,
+                mDisplayMetrics.density, outConfig);
+
         // We usually set the override info in DisplayManager so that we get consistent display
         // metrics values when displays are changing and don't send out new values until WM is aware
         // of them. However, we don't do this for displays that serve as containers for ActivityView
@@ -1658,7 +1661,7 @@
      * Do not call if mDisplayReady == false.
      */
     void computeScreenConfiguration(Configuration config) {
-        final DisplayInfo displayInfo = updateDisplayAndOrientation(config.uiMode);
+        final DisplayInfo displayInfo = updateDisplayAndOrientation(config.uiMode, config);
         calculateBounds(displayInfo, mTmpBounds);
         config.windowConfiguration.setBounds(mTmpBounds);
 
@@ -1688,9 +1691,6 @@
         final boolean rotated = (displayInfo.rotation == Surface.ROTATION_90
                 || displayInfo.rotation == Surface.ROTATION_270);
 
-        computeSizeRangesAndScreenLayout(displayInfo, rotated, config.uiMode, dw, dh, density,
-                config);
-
         config.screenLayout = (config.screenLayout & ~Configuration.SCREENLAYOUT_ROUND_MASK)
                 | ((displayInfo.flags & Display.FLAG_ROUND) != 0
                 ? Configuration.SCREENLAYOUT_ROUND_YES
@@ -1844,6 +1844,10 @@
         adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_90, uiMode, unrotDh, unrotDw);
         adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_180, uiMode, unrotDw, unrotDh);
         adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_270, uiMode, unrotDh, unrotDw);
+
+        if (outConfig == null) {
+            return;
+        }
         int sl = Configuration.resetScreenLayout(outConfig.screenLayout);
         sl = reduceConfigLayout(sl, Surface.ROTATION_0, density, unrotDw, unrotDh, uiMode,
                 displayInfo.displayCutout);
@@ -3340,7 +3344,7 @@
         final SurfaceControl newParent =
                 shouldAttachToDisplay ? mWindowingLayer : computeImeParent();
         if (newParent != null) {
-            mPendingTransaction.reparent(mImeWindowsContainers.mSurfaceControl, newParent);
+            getPendingTransaction().reparent(mImeWindowsContainers.mSurfaceControl, newParent);
             scheduleAnimation();
         }
     }
@@ -3747,7 +3751,8 @@
             mPortalWindowHandle.touchableRegion.getBounds(mTmpRect);
             if (!mTmpBounds.equals(mTmpRect)) {
                 mPortalWindowHandle.touchableRegion.set(mTmpBounds);
-                mPendingTransaction.setInputWindowInfo(mParentSurfaceControl, mPortalWindowHandle);
+                getPendingTransaction().setInputWindowInfo(
+                        mParentSurfaceControl, mPortalWindowHandle);
             }
         }
     }
@@ -4846,18 +4851,23 @@
         try {
             final ScreenRotationAnimation screenRotationAnimation =
                     mWmService.mAnimator.getScreenRotationAnimationLocked(mDisplayId);
+            final Transaction transaction = getPendingTransaction();
             if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()) {
                 screenRotationAnimation.getEnterTransformation().getMatrix().getValues(mTmpFloats);
-                mPendingTransaction.setMatrix(mWindowingLayer,
+                transaction.setMatrix(mWindowingLayer,
                         mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y],
                         mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]);
-                mPendingTransaction.setPosition(mWindowingLayer,
+                transaction.setPosition(mWindowingLayer,
                         mTmpFloats[Matrix.MTRANS_X], mTmpFloats[Matrix.MTRANS_Y]);
-                mPendingTransaction.setAlpha(mWindowingLayer,
+                transaction.setAlpha(mWindowingLayer,
                         screenRotationAnimation.getEnterTransformation().getAlpha());
             }
 
             super.prepareSurfaces();
+
+            // TODO: Once we totally eliminate global transaction we will pass transaction in here
+            //       rather than merging to global.
+            SurfaceControl.mergeToGlobalTransaction(transaction);
         } finally {
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         }
@@ -5013,7 +5023,7 @@
         if (mPortalWindowHandle == null) {
             mPortalWindowHandle = createPortalWindowHandle(sc.toString());
         }
-        mPendingTransaction.setInputWindowInfo(sc, mPortalWindowHandle)
+        getPendingTransaction().setInputWindowInfo(sc, mPortalWindowHandle)
                 .reparent(mWindowingLayer, sc).reparent(mOverlayLayer, sc);
     }
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index bb7867c..3820106 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -138,6 +138,7 @@
         setOrientation(SCREEN_ORIENTATION_UNSET);
     }
 
+    @Override
     DisplayContent getDisplayContent() {
         return mStack != null ? mStack.getDisplayContent() : null;
     }
diff --git a/services/core/java/com/android/server/wm/TaskScreenshotAnimatable.java b/services/core/java/com/android/server/wm/TaskScreenshotAnimatable.java
index 17e4b89..ee4e462 100644
--- a/services/core/java/com/android/server/wm/TaskScreenshotAnimatable.java
+++ b/services/core/java/com/android/server/wm/TaskScreenshotAnimatable.java
@@ -77,7 +77,7 @@
 
     @Override
     public SurfaceControl.Transaction getPendingTransaction() {
-        return mTask.mPendingTransaction;
+        return mTask.getPendingTransaction();
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index 757f6a1..ab5e071 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -93,10 +93,6 @@
     /** Unique identifier */
     final int mStackId;
 
-    /** The display this stack sits under. */
-    // TODO: Track parent marks like this in WindowContainer.
-    private DisplayContent mDisplayContent;
-
     /** For comparison with DisplayContent bounds. */
     private Rect mTmpRect = new Rect();
     private Rect mTmpRect2 = new Rect();
@@ -177,10 +173,6 @@
         EventLog.writeEvent(EventLogTags.WM_STACK_CREATED, stackId);
     }
 
-    DisplayContent getDisplayContent() {
-        return mDisplayContent;
-    }
-
     Task findHomeTask() {
         if (!isActivityTypeHome() || mChildren.isEmpty()) {
             return null;
@@ -825,8 +817,7 @@
             throw new IllegalStateException("onDisplayChanged: Already attached");
         }
 
-        final boolean movedToNewDisplay = mDisplayContent == null;
-        mDisplayContent = dc;
+        super.onDisplayChanged(dc);
 
         updateSurfaceBounds();
         if (mAnimationBackgroundSurface == null) {
@@ -834,8 +825,6 @@
                     .setName("animation background stackId=" + mStackId)
                     .build();
         }
-
-        super.onDisplayChanged(dc);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index d5c3e4f..bbef261 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -109,14 +109,19 @@
     // The owner/creator for this container. No controller if null.
     WindowContainerController mController;
 
+    // The display this window container is on.
+    protected DisplayContent mDisplayContent;
+
     protected SurfaceControl mSurfaceControl;
     private int mLastLayer = 0;
     private SurfaceControl mLastRelativeToLayer = null;
 
+    // TODO(b/132320879): Remove this from WindowContainers except DisplayContent.
+    private final Transaction mPendingTransaction;
+
     /**
      * Applied as part of the animation pass in "prepareSurfaces".
      */
-    protected final Transaction mPendingTransaction;
     protected final SurfaceAnimator mSurfaceAnimator;
     protected final WindowManagerService mWmService;
 
@@ -320,12 +325,12 @@
         }
 
         if (mSurfaceControl != null) {
-            mPendingTransaction.remove(mSurfaceControl);
+            getPendingTransaction().remove(mSurfaceControl);
 
             // Merge to parent transaction to ensure the transactions on this WindowContainer are
             // applied in native even if WindowContainer is removed.
             if (mParent != null) {
-                mParent.getPendingTransaction().merge(mPendingTransaction);
+                mParent.getPendingTransaction().merge(getPendingTransaction());
             }
 
             mSurfaceControl = null;
@@ -508,12 +513,20 @@
      * @param dc The display this container is on after changes.
      */
     void onDisplayChanged(DisplayContent dc) {
+        mDisplayContent = dc;
+        if (dc != null && dc != this) {
+            dc.getPendingTransaction().merge(mPendingTransaction);
+        }
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             final WindowContainer child = mChildren.get(i);
             child.onDisplayChanged(dc);
         }
     }
 
+    DisplayContent getDisplayContent() {
+        return mDisplayContent;
+    }
+
     void setWaitingForDrawnIfResizingChanged() {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             final WindowContainer wc = mChildren.get(i);
@@ -1180,13 +1193,7 @@
         }
     }
 
-    /**
-     * TODO: Once we totally eliminate global transaction we will pass transaction in here
-     * rather than merging to global.
-     */
     void prepareSurfaces() {
-        SurfaceControl.mergeToGlobalTransaction(getPendingTransaction());
-
         // If a leash has been set when the transaction was committed, then the leash reparent has
         // been committed.
         mCommittedReparentToAnimationLeash = mSurfaceAnimator.hasLeash();
@@ -1204,8 +1211,8 @@
     }
 
     /**
-     * Trigger a call to prepareSurfaces from the animation thread, such that
-     * mPendingTransaction will be applied.
+     * Trigger a call to prepareSurfaces from the animation thread, such that pending transactions
+     * will be applied.
      */
     void scheduleAnimation() {
         if (mParent != null) {
@@ -1224,6 +1231,14 @@
 
     @Override
     public Transaction getPendingTransaction() {
+        final DisplayContent displayContent = getDisplayContent();
+        if (displayContent != null && displayContent != this) {
+            return displayContent.getPendingTransaction();
+        }
+        // This WindowContainer has not attached to a display yet or this is a DisplayContent, so we
+        // let the caller to save the surface operations within the local mPendingTransaction.
+        // If this is not a DisplayContent, we will merge it to the pending transaction of its
+        // display once it attaches to it.
         return mPendingTransaction;
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index dd3c600..5ef184a 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1313,6 +1313,7 @@
         mOrientationChangeTimedOut = true;
     }
 
+    @Override
     DisplayContent getDisplayContent() {
         return mToken.getDisplayContent();
     }
@@ -4602,7 +4603,7 @@
                 new WindowAnimationSpec(anim, mSurfacePosition, false /* canSkipFirstFrame */,
                         0 /* windowCornerRadius */),
                 mWmService.mSurfaceAnimationRunner);
-        startAnimation(mPendingTransaction, adapter);
+        startAnimation(getPendingTransaction(), adapter);
         commitPendingTransaction();
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index f0b9c62..8aee0f2 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -82,9 +82,6 @@
     // windows will be put to the bottom of the list.
     boolean sendingToBottom;
 
-    // The display this token is on.
-    protected DisplayContent mDisplayContent;
-
     /** The owner has {@link android.Manifest.permission#MANAGE_APP_TOKENS} */
     final boolean mOwnerCanManageAppTokens;
 
@@ -249,10 +246,6 @@
         return null;
     }
 
-    DisplayContent getDisplayContent() {
-        return mDisplayContent;
-    }
-
     @Override
     void removeImmediately() {
         if (mDisplayContent != null) {
@@ -266,7 +259,6 @@
     @Override
     void onDisplayChanged(DisplayContent dc) {
         dc.reParentWindowToken(this);
-        mDisplayContent = dc;
 
         // TODO(b/36740756): One day this should perhaps be hooked
         // up with goodToGo, so we don't move a window
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 8c65fa8..e42ed3b 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1049,12 +1049,7 @@
             mDisplayManagerService.windowManagerAndInputReady();
             traceEnd();
 
-            // Skip Bluetooth if we have an emulator kernel
-            // TODO: Use a more reliable check to see if this product should
-            // support Bluetooth - see bug 988521
-            if (isEmulator) {
-                Slog.i(TAG, "No Bluetooth Service (emulator)");
-            } else if (mFactoryTestMode == FactoryTest.FACTORY_TEST_LOW_LEVEL) {
+            if (mFactoryTestMode == FactoryTest.FACTORY_TEST_LOW_LEVEL) {
                 Slog.i(TAG, "No Bluetooth Service (factory test)");
             } else if (!context.getPackageManager().hasSystemFeature
                     (PackageManager.FEATURE_BLUETOOTH)) {
@@ -1167,9 +1162,12 @@
         if (!mOnlyCore) {
             traceBeginAndSlog("UpdatePackagesIfNeeded");
             try {
+                Watchdog.getInstance().pauseWatchingCurrentThread("dexopt");
                 mPackageManagerService.updatePackagesIfNeeded();
             } catch (Throwable e) {
                 reportWtf("update packages", e);
+            } finally {
+                Watchdog.getInstance().resumeWatchingCurrentThread("dexopt");
             }
             traceEnd();
         }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index f7edf65..18c524a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -183,15 +183,188 @@
         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
 
-        advanceElapsedClock(45 * MINUTE_IN_MILLIS); // now + 55 minutes
+        advanceElapsedClock(20 * MINUTE_IN_MILLIS); // now + 30 minutes
 
         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
 
+        advanceElapsedClock(25 * MINUTE_IN_MILLIS); // now + 55 minutes
+
+        rescheduledJob = mService.getRescheduleJobForPeriodic(job);
+        // Shifted because it's close to the end of the window.
+        assertEquals(nextWindowStartTime + 5 * MINUTE_IN_MILLIS,
+                rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
         advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 59 minutes
 
         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
+        // Shifted because it's close to the end of the window.
+        assertEquals(nextWindowStartTime + 9 * MINUTE_IN_MILLIS,
+                rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+    }
+
+    /**
+     * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
+     * with an extra delay and correct deadline constraint if the periodic job is completed near the
+     * end of its expected running window.
+     */
+    @Test
+    public void testGetRescheduleJobForPeriodic_closeToEndOfWindow() {
+        JobStatus frequentJob = createJobStatus(
+                "testGetRescheduleJobForPeriodic_closeToEndOfWindow",
+                createJobInfo().setPeriodic(15 * MINUTE_IN_MILLIS));
+        long now = sElapsedRealtimeClock.millis();
+        long nextWindowStartTime = now + 15 * MINUTE_IN_MILLIS;
+        long nextWindowEndTime = now + 30 * MINUTE_IN_MILLIS;
+
+        // At the beginning of the window. Next window should be unaffected.
+        JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // Halfway through window. Next window should be unaffected.
+        advanceElapsedClock((long) (7.5 * MINUTE_IN_MILLIS));
+        rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // In last 1/6 of window. Next window start time should be shifted slightly.
+        advanceElapsedClock(6 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
+        assertEquals(nextWindowStartTime + MINUTE_IN_MILLIS,
+                rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        JobStatus mediumJob = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
+                createJobInfo().setPeriodic(HOUR_IN_MILLIS));
+        now = sElapsedRealtimeClock.millis();
+        nextWindowStartTime = now + HOUR_IN_MILLIS;
+        nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
+
+        // At the beginning of the window. Next window should be unaffected.
+        rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // Halfway through window. Next window should be unaffected.
+        advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // At the edge 1/6 of window. Next window should be unaffected.
+        advanceElapsedClock(20 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // In last 1/6 of window. Next window start time should be shifted slightly.
+        advanceElapsedClock(6 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
+        assertEquals(nextWindowStartTime + (6 * MINUTE_IN_MILLIS),
+                rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        JobStatus longJob = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
+                createJobInfo().setPeriodic(6 * HOUR_IN_MILLIS));
+        now = sElapsedRealtimeClock.millis();
+        nextWindowStartTime = now + 6 * HOUR_IN_MILLIS;
+        nextWindowEndTime = now + 12 * HOUR_IN_MILLIS;
+
+        // At the beginning of the window. Next window should be unaffected.
+        rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // Halfway through window. Next window should be unaffected.
+        advanceElapsedClock(3 * HOUR_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // At the edge 1/6 of window. Next window should be unaffected.
+        advanceElapsedClock(2 * HOUR_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // In last 1/6 of window. Next window should be unaffected since we're over the shift cap.
+        advanceElapsedClock(15 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // In last 1/6 of window. Next window start time should be shifted slightly.
+        advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
+        assertEquals(nextWindowStartTime + (30 * MINUTE_IN_MILLIS),
+                rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // Flex duration close to period duration.
+        JobStatus gameyFlex = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
+                createJobInfo().setPeriodic(HOUR_IN_MILLIS, 59 * MINUTE_IN_MILLIS));
+        now = sElapsedRealtimeClock.millis();
+        nextWindowStartTime = now + HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+        nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+
+        // At the beginning of the window. Next window should be unaffected.
+        rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // Halfway through window. Next window should be unaffected.
+        advanceElapsedClock(29 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // At the edge 1/6 of window. Next window should be unaffected.
+        advanceElapsedClock(20 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // In last 1/6 of window. Next window start time should be shifted slightly.
+        advanceElapsedClock(6 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
+        assertEquals(nextWindowStartTime + (5 * MINUTE_IN_MILLIS),
+                rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // Very short flex duration compared to period duration.
+        JobStatus superFlex = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
+                createJobInfo().setPeriodic(HOUR_IN_MILLIS, 10 * MINUTE_IN_MILLIS));
+        now = sElapsedRealtimeClock.millis();
+        nextWindowStartTime = now + HOUR_IN_MILLIS + 50 * MINUTE_IN_MILLIS;
+        nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+
+        // At the beginning of the window. Next window should be unaffected.
+        rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // Halfway through window. Next window should be unaffected.
+        advanceElapsedClock(29 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // At the edge 1/6 of window. Next window should be unaffected.
+        advanceElapsedClock(20 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+
+        // In last 1/6 of window. Next window should be unaffected since the flex duration pushes
+        // the next window start time far enough away.
+        advanceElapsedClock(6 * MINUTE_IN_MILLIS);
+        rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
     }
@@ -265,7 +438,9 @@
         advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 55 minutes
 
         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
-        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        // Shifted because it's close to the end of the window.
+        assertEquals(nextWindowStartTime + 5 * MINUTE_IN_MILLIS,
+                rescheduledJob.getEarliestRunTime());
         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
 
         advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 57 minutes
@@ -273,7 +448,9 @@
         advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 59 minutes
 
         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
-        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        // Shifted because it's close to the end of the window.
+        assertEquals(nextWindowStartTime + 9 * MINUTE_IN_MILLIS,
+                rescheduledJob.getEarliestRunTime());
         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index 09e20e0..1f5ebe4 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -235,6 +235,10 @@
         mPasswordSlotManager.cleanup();
     }
 
+    protected void flushHandlerTasks() {
+        mService.mHandler.runWithScissors(() -> { }, 0 /*now*/); // Flush runnables on handler
+    }
+
     protected void assertNotEquals(long expected, long actual) {
         assertTrue(expected != actual);
     }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index 10fb3ba..f85e2cc 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -110,7 +110,7 @@
         }
 
         @Override
-        public boolean hasBiometrics() {
+        public boolean hasEnrolledBiometrics() {
             return false;
         }
 
@@ -163,5 +163,4 @@
         }
         return storedData;
     }
-
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 0273f76..1cd590c 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -232,7 +232,7 @@
 
         reset(mAuthSecretService);
         mService.onUnlockUser(PRIMARY_USER_ID);
-        mService.mHandler.runWithScissors(() -> {}, 0 /*now*/); // Flush runnables on handler
+        flushHandlerTasks();
         verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
     }
 
@@ -242,7 +242,7 @@
 
         reset(mAuthSecretService);
         mService.onUnlockUser(PRIMARY_USER_ID);
-        mService.mHandler.runWithScissors(() -> {}, 0 /*now*/); // Flush runnables on handler
+        flushHandlerTasks();
         verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
     }
 
@@ -254,7 +254,7 @@
 
         reset(mAuthSecretService);
         mService.onUnlockUser(PRIMARY_USER_ID);
-        mService.mHandler.runWithScissors(() -> {}, 0 /*now*/); // Flush runnables on handler
+        flushHandlerTasks();
         verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class));
     }
 
@@ -357,7 +357,7 @@
                 handle, token, PASSWORD_QUALITY_SOMETHING, PRIMARY_USER_ID);
 
         // Verify DPM gets notified about new device lock
-        mService.mHandler.runWithScissors(() -> {}, 0 /*now*/); // Flush runnables on handler
+        flushHandlerTasks();
         final PasswordMetrics metric = PasswordMetrics.computeForCredential(
                 LockPatternUtils.CREDENTIAL_TYPE_PATTERN, pattern);
         verify(mDevicePolicyManager).setActivePasswordState(metric, PRIMARY_USER_ID);
@@ -384,6 +384,7 @@
 
         mLocalService.setLockCredentialWithToken(null, LockPatternUtils.CREDENTIAL_TYPE_NONE,
                 handle, token, PASSWORD_QUALITY_UNSPECIFIED, PRIMARY_USER_ID);
+        flushHandlerTasks(); // flush the unlockUser() call before changing password again
         mLocalService.setLockCredentialWithToken(pattern, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
                 handle, token, PASSWORD_QUALITY_SOMETHING, PRIMARY_USER_ID);
 
diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index faf6ee2..0f3050f 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -120,6 +120,7 @@
             @Override
             public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
                 switch (vol.type) {
+                    case VolumeInfo.TYPE_PUBLIC:
                     case VolumeInfo.TYPE_PRIVATE:
                     case VolumeInfo.TYPE_EMULATED:
                         if (newState == VolumeInfo.STATE_MOUNTED) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index e1ffb0f..d77ea6e 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -77,6 +77,7 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
+import com.android.server.SystemServerInitThreadPool;
 import com.android.server.SystemService;
 import com.android.server.UiThread;
 import com.android.server.soundtrigger.SoundTriggerInternal;
@@ -1283,7 +1284,9 @@
                 mRm.addOnRoleHoldersChangedListenerAsUser(executor, this, UserHandle.ALL);
                 UserHandle currentUser = UserHandle.of(LocalServices.getService(
                         ActivityManagerInternal.class).getCurrentUserId());
-                onRoleHoldersChanged(RoleManager.ROLE_ASSISTANT, currentUser);
+                SystemServerInitThreadPool.get().submit(() -> onRoleHoldersChanged(
+                        RoleManager.ROLE_ASSISTANT, currentUser),
+                        "VoiceInteractionManagerService RoleObserver initialization");
             }
 
             private @NonNull String getDefaultRecognizer(@NonNull UserHandle user) {
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 93c1b21..eefaf47 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -1451,6 +1451,9 @@
      * foreground call is ended.
      * <p>
      * Requires permission {@link android.Manifest.permission#ANSWER_PHONE_CALLS}.
+     * <p>
+     * Note: this method CANNOT be used to end ongoing emergency calls and will return {@code false}
+     * if an attempt is made to end an emergency call.
      *
      * @return {@code true} if there is a call which will be rejected or terminated, {@code false}
      * otherwise.
@@ -1458,8 +1461,6 @@
      * instead.  Apps performing call screening should use the {@link CallScreeningService} API
      * instead.
      */
-
-
     @RequiresPermission(Manifest.permission.ANSWER_PHONE_CALLS)
     @Deprecated
     public boolean endCall() {
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index d00341b..6ba359b 100755
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -836,13 +836,6 @@
             "carrier_metered_roaming_apn_types_strings";
 
     /**
-     * Default APN types that are metered on IWLAN by the carrier
-     * @hide
-     */
-    public static final String KEY_CARRIER_METERED_IWLAN_APN_TYPES_STRINGS =
-            "carrier_metered_iwlan_apn_types_strings";
-
-    /**
      * CDMA carrier ERI (Enhanced Roaming Indicator) file name
      * @hide
      */
@@ -3116,15 +3109,6 @@
                 new String[]{"default", "mms", "dun", "supl"});
         sDefaults.putStringArray(KEY_CARRIER_METERED_ROAMING_APN_TYPES_STRINGS,
                 new String[]{"default", "mms", "dun", "supl"});
-        // By default all APNs should be unmetered if the device is on IWLAN. However, we add
-        // default APN as metered here as a workaround for P because in some cases, a data
-        // connection was brought up on cellular, but later on the device camped on IWLAN. That
-        // data connection was incorrectly treated as unmetered due to the current RAT IWLAN.
-        // Marking it as metered for now can workaround the issue.
-        // Todo: This will be fixed in Q when IWLAN full refactoring is completed.
-        sDefaults.putStringArray(KEY_CARRIER_METERED_IWLAN_APN_TYPES_STRINGS,
-                new String[]{"default"});
-
         sDefaults.putIntArray(KEY_ONLY_SINGLE_DC_ALLOWED_INT_ARRAY,
                 new int[]{
                     4, /* IS95A */
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index c57f9e6..f31ac2e 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -173,6 +173,11 @@
     private ParcelUuid mGroupUUID;
 
     /**
+     * A package name that specifies who created the group. Null if mGroupUUID is null.
+     */
+    private String mGroupOwner;
+
+    /**
      * Whether group of the subscription is disabled.
      * This is only useful if it's a grouped opportunistic subscription. In this case, if all
      * primary (non-opportunistic) subscriptions in the group are deactivated (unplugged pSIM
@@ -203,9 +208,10 @@
             Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded,
             @Nullable UiccAccessRule[] accessRules, String cardString) {
         this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number,
-                roaming, icon, mcc, mnc, countryIso, isEmbedded, accessRules, cardString,
-                false, null, TelephonyManager.UNKNOWN_CARRIER_ID,
-                SubscriptionManager.PROFILE_CLASS_DEFAULT);
+                roaming, icon, mcc, mnc, countryIso, isEmbedded, accessRules, cardString, -1,
+                false, null, false, TelephonyManager.UNKNOWN_CARRIER_ID,
+                SubscriptionManager.PROFILE_CLASS_DEFAULT,
+                SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM, null);
     }
 
     /**
@@ -219,7 +225,7 @@
         this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number,
                 roaming, icon, mcc, mnc, countryIso, isEmbedded, accessRules, cardString, -1,
                 isOpportunistic, groupUUID, false, carrierId, profileClass,
-                SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM);
+                SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM, null);
     }
 
     /**
@@ -229,8 +235,8 @@
             CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
             Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded,
             @Nullable UiccAccessRule[] accessRules, String cardString, int cardId,
-            boolean isOpportunistic, @Nullable String groupUUID,
-            boolean isGroupDisabled, int carrierId, int profileClass, int subType) {
+            boolean isOpportunistic, @Nullable String groupUUID, boolean isGroupDisabled,
+            int carrierId, int profileClass, int subType, @Nullable String groupOwner) {
         this.mId = id;
         this.mIccId = iccId;
         this.mSimSlotIndex = simSlotIndex;
@@ -254,6 +260,7 @@
         this.mCarrierId = carrierId;
         this.mProfileClass = profileClass;
         this.mSubscriptionType = subType;
+        this.mGroupOwner = groupOwner;
     }
 
     /**
@@ -500,6 +507,15 @@
     }
 
     /**
+     * Return owner package of group the subscription belongs to.
+     *
+     * @hide
+     */
+    public @Nullable String getGroupOwner() {
+        return mGroupOwner;
+    }
+
+    /**
      * @return the profile class of this subscription.
      * @hide
      */
@@ -646,11 +662,12 @@
             int subType = source.readInt();
             String[] ehplmns = source.readStringArray();
             String[] hplmns = source.readStringArray();
+            String groupOwner = source.readString();
 
             SubscriptionInfo info = new SubscriptionInfo(id, iccId, simSlotIndex, displayName,
                     carrierName, nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc,
                     countryIso, isEmbedded, accessRules, cardString, cardId, isOpportunistic,
-                    groupUUID, isGroupDisabled, carrierid, profileClass, subType);
+                    groupUUID, isGroupDisabled, carrierid, profileClass, subType, groupOwner);
             info.setAssociatedPlmns(ehplmns, hplmns);
             return info;
         }
@@ -688,6 +705,7 @@
         dest.writeInt(mSubscriptionType);
         dest.writeStringArray(mEhplmns);
         dest.writeStringArray(mHplmns);
+        dest.writeString(mGroupOwner);
     }
 
     @Override
@@ -727,7 +745,8 @@
                 + " profileClass=" + mProfileClass
                 + " ehplmns = " + Arrays.toString(mEhplmns)
                 + " hplmns = " + Arrays.toString(mHplmns)
-                + " subscriptionType=" + mSubscriptionType + "}";
+                + " subscriptionType=" + mSubscriptionType
+                + " mGroupOwner=" + mGroupOwner + "}";
     }
 
     @Override
@@ -735,7 +754,7 @@
         return Objects.hash(mId, mSimSlotIndex, mNameSource, mIconTint, mDataRoaming, mIsEmbedded,
                 mIsOpportunistic, mGroupUUID, mIccId, mNumber, mMcc, mMnc,
                 mCountryIso, mCardString, mCardId, mDisplayName, mCarrierName, mAccessRules,
-                mIsGroupDisabled, mCarrierId, mProfileClass);
+                mIsGroupDisabled, mCarrierId, mProfileClass, mGroupOwner);
     }
 
     @Override
@@ -767,6 +786,7 @@
                 && Objects.equals(mCountryIso, toCompare.mCountryIso)
                 && Objects.equals(mCardString, toCompare.mCardString)
                 && Objects.equals(mCardId, toCompare.mCardId)
+                && Objects.equals(mGroupOwner, toCompare.mGroupOwner)
                 && TextUtils.equals(mDisplayName, toCompare.mDisplayName)
                 && TextUtils.equals(mCarrierName, toCompare.mCarrierName)
                 && Arrays.equals(mAccessRules, toCompare.mAccessRules)
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 32105ad..addd9e0 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -396,6 +396,12 @@
     public static final int NAME_SOURCE_USER_INPUT = 2;
 
     /**
+     * The name_source is carrier (carrier app, carrier config, etc.)
+     * @hide
+     */
+    public static final int NAME_SOURCE_CARRIER = 3;
+
+    /**
      * TelephonyProvider column name for the color of a SIM.
      * <P>Type: INTEGER (int)</P>
      */
@@ -686,6 +692,15 @@
      * @hide
      */
     public static final String GROUP_UUID = "group_uuid";
+
+    /**
+     * TelephonyProvider column name for group owner. It's the package name who created
+     * the subscription group.
+     *
+     * @hide
+     */
+    public static final String GROUP_OWNER = "group_owner";
+
     /**
      * TelephonyProvider column name for whether a subscription is metered or not, that is, whether
      * the network it connects to charges for subscription or not. For example, paid CBRS or unpaid.
diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java
index a86fda4..a78bae4 100644
--- a/telephony/java/android/telephony/data/ApnSetting.java
+++ b/telephony/java/android/telephony/data/ApnSetting.java
@@ -1273,6 +1273,23 @@
     }
 
     /**
+     * Get supported APN types
+     *
+     * @return list of APN types
+     * @hide
+     */
+    @ApnType
+    public List<Integer> getApnTypes() {
+        List<Integer> types = new ArrayList<>();
+        for (Integer type : APN_TYPE_INT_MAP.keySet()) {
+            if ((mApnTypeBitmask & type) == type) {
+                types.add(type);
+            }
+        }
+        return types;
+    }
+
+    /**
      * @param apnTypeBitmask bitmask of APN types.
      * @return comma delimited list of APN types.
      * @hide
diff --git a/telephony/java/com/android/internal/telephony/TelephonyPermissions.java b/telephony/java/com/android/internal/telephony/TelephonyPermissions.java
index 64c1830..2552f04 100644
--- a/telephony/java/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/java/com/android/internal/telephony/TelephonyPermissions.java
@@ -209,18 +209,9 @@
                 context.enforcePermission(
                         android.Manifest.permission.READ_PHONE_STATE, pid, uid, message);
             } catch (SecurityException phoneStateException) {
-                SubscriptionManager sm = (SubscriptionManager) context.getSystemService(
-                        Context.TELEPHONY_SUBSCRIPTION_SERVICE);
-                int[] activeSubIds = sm.getActiveSubscriptionIdList();
-                for (int activeSubId : activeSubIds) {
-                    // If we don't have the runtime permission, but do have carrier privileges, that
-                    // suffices for reading phone state.
-                    if (getCarrierPrivilegeStatus(telephonySupplier, activeSubId, uid)
-                            == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
-                        return true;
-                    }
-                }
-                return false;
+                // If we don't have the runtime permission, but do have carrier privileges, that
+                // suffices for reading phone state.
+                return checkCarrierPrivilegeForAnySubId(context, telephonySupplier, uid);
             }
         }
 
@@ -236,9 +227,9 @@
      *
      * <p>This method behaves in one of the following ways:
      * <ul>
-     *   <li>return true: if the caller has the READ_PRIVILEGED_PHONE_STATE permission or the
-     *       calling package passes a DevicePolicyManager Device Owner / Profile Owner device
-     *       identifier access check,
+     *   <li>return true: if the caller has the READ_PRIVILEGED_PHONE_STATE permission, the calling
+     *       package passes a DevicePolicyManager Device Owner / Profile Owner device identifier
+     *       access check, or the calling package has carrier privileges.
      *   <li>throw SecurityException: if the caller does not meet any of the requirements and is
      *       targeting Q or is targeting pre-Q and does not have the READ_PHONE_STATE permission.
      *   <li>return false: if the caller is targeting pre-Q and does have the READ_PHONE_STATE
@@ -258,9 +249,9 @@
      *
      * <p>This method behaves in one of the following ways:
      * <ul>
-     *   <li>return true: if the caller has the READ_PRIVILEGED_PHONE_STATE permission or the
-     *       calling package passes a DevicePolicyManager Device Owner / Profile Owner device
-     *       identifier access check,
+     *   <li>return true: if the caller has the READ_PRIVILEGED_PHONE_STATE permission, the calling
+     *       package passes a DevicePolicyManager Device Owner / Profile Owner device identifier
+     *       access check, or the calling package has carrier privileges.
      *   <li>throw SecurityException: if the caller does not meet any of the requirements and is
      *       targeting Q or is targeting pre-Q and does not have the READ_PHONE_STATE permission
      *       or carrier privileges.
@@ -272,23 +263,8 @@
      */
     public static boolean checkCallingOrSelfReadDeviceIdentifiers(Context context, int subId,
             String callingPackage, String message) {
-        int pid = Binder.getCallingPid();
-        int uid = Binder.getCallingUid();
-        // if the device identifier check completes successfully then grant access.
-        if (checkReadDeviceIdentifiers(context, pid, uid, callingPackage)) {
-            return true;
-        }
-        // Calling packages with carrier privileges will also have access to device identifiers, but
-        // this may be removed in a future release.
-        if (checkCarrierPrivilegeForSubId(subId)) {
-            return true;
-        }
-        // else the calling package is not authorized to access the device identifiers; call
-        // a central method to report the failure based on the target SDK and if the calling package
-        // has the READ_PHONE_STATE permission or carrier privileges that were previously required
-        // to access the identifiers.
-        return reportAccessDeniedToReadIdentifiers(context, subId, pid, uid, callingPackage,
-                message);
+        return checkReadDeviceIdentifiers(context, TELEPHONY_SUPPLIER, subId,
+                Binder.getCallingPid(), Binder.getCallingUid(), callingPackage, message);
     }
 
     /**
@@ -298,7 +274,7 @@
      * <ul>
      *   <li>return true: if the caller has the READ_PRIVILEGED_PHONE_STATE permission, the calling
      *       package passes a DevicePolicyManager Device Owner / Profile Owner device identifier
-     *       access check, or the calling package has carrier privleges.
+     *       access check, or the calling package has carrier privileges.
      *   <li>throw SecurityException: if the caller does not meet any of the requirements and is
      *       targeting Q or is targeting pre-Q and does not have the READ_PHONE_STATE permission.
      *   <li>return false: if the caller is targeting pre-Q and does have the READ_PHONE_STATE
@@ -309,19 +285,8 @@
      */
     public static boolean checkCallingOrSelfReadSubscriberIdentifiers(Context context, int subId,
             String callingPackage, String message) {
-        int pid = Binder.getCallingPid();
-        int uid = Binder.getCallingUid();
-        // if the device identifiers can be read then grant access to the subscriber identifiers
-        if (checkReadDeviceIdentifiers(context, pid, uid, callingPackage)) {
-            return true;
-        }
-        // If the calling package has carrier privileges then allow access to the subscriber
-        // identifiers.
-        if (checkCarrierPrivilegeForSubId(subId)) {
-            return true;
-        }
-        return reportAccessDeniedToReadIdentifiers(context, subId, pid, uid, callingPackage,
-                message);
+        return checkReadDeviceIdentifiers(context, TELEPHONY_SUPPLIER, subId,
+                Binder.getCallingPid(), Binder.getCallingUid(), callingPackage, message);
     }
 
     /**
@@ -331,8 +296,10 @@
      * package passes a DevicePolicyManager Device Owner / Profile Owner device identifier access
      * check.
      */
-    private static boolean checkReadDeviceIdentifiers(Context context, int pid, int uid,
-            String callingPackage) {
+    @VisibleForTesting
+    public static boolean checkReadDeviceIdentifiers(Context context,
+            Supplier<ITelephony> telephonySupplier, int subId, int pid, int uid,
+            String callingPackage, String message) {
         // Allow system and root access to the device identifiers.
         final int appId = UserHandle.getAppId(uid);
         if (appId == Process.SYSTEM_UID || appId == Process.ROOT_UID) {
@@ -343,31 +310,36 @@
                 uid) == PackageManager.PERMISSION_GRANTED) {
             return true;
         }
-        // if the calling package is null then return now as there's no way to perform the
-        // DevicePolicyManager device / profile owner and AppOp checks
-        if (callingPackage == null) {
-            return false;
-        }
-        // Allow access to an app that has been granted the READ_DEVICE_IDENTIFIERS app op.
-        long token = Binder.clearCallingIdentity();
-        AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(
-                Context.APP_OPS_SERVICE);
-        try {
-            if (appOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_READ_DEVICE_IDENTIFIERS, uid,
-                    callingPackage) == AppOpsManager.MODE_ALLOWED) {
-                return true;
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-        // Allow access to a device / profile owner app.
-        DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(
-                Context.DEVICE_POLICY_SERVICE);
-        if (devicePolicyManager != null && devicePolicyManager.checkDeviceIdentifierAccess(
-                callingPackage, pid, uid)) {
+        // If the calling package has carrier privileges for any subscription then allow access.
+        if (checkCarrierPrivilegeForAnySubId(context, telephonySupplier, uid)) {
             return true;
         }
-        return false;
+        // if the calling package is not null then perform the DevicePolicyManager device /
+        // profile owner and Appop checks.
+        if (callingPackage != null) {
+            // Allow access to an app that has been granted the READ_DEVICE_IDENTIFIERS app op.
+            long token = Binder.clearCallingIdentity();
+            AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(
+                    Context.APP_OPS_SERVICE);
+            try {
+                if (appOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_READ_DEVICE_IDENTIFIERS, uid,
+                        callingPackage) == AppOpsManager.MODE_ALLOWED) {
+                    return true;
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+            // Allow access to a device / profile owner app.
+            DevicePolicyManager devicePolicyManager =
+                    (DevicePolicyManager) context.getSystemService(
+                            Context.DEVICE_POLICY_SERVICE);
+            if (devicePolicyManager != null && devicePolicyManager.checkDeviceIdentifierAccess(
+                    callingPackage, pid, uid)) {
+                return true;
+            }
+        }
+        return reportAccessDeniedToReadIdentifiers(context, subId, pid, uid, callingPackage,
+            message);
     }
 
     /**
@@ -403,10 +375,12 @@
         try {
             callingPackageInfo = context.getPackageManager().getApplicationInfoAsUser(
                     callingPackage, 0, UserHandle.getUserId(uid));
-            if (callingPackageInfo.isSystemApp()) {
-                isPreinstalled = true;
-                if (callingPackageInfo.isPrivilegedApp()) {
-                    isPrivApp = true;
+            if (callingPackageInfo != null) {
+                if (callingPackageInfo.isSystemApp()) {
+                    isPreinstalled = true;
+                    if (callingPackageInfo.isPrivilegedApp()) {
+                        isPrivApp = true;
+                    }
                 }
             }
         } catch (PackageManager.NameNotFoundException e) {
@@ -651,6 +625,26 @@
         }
     }
 
+    /**
+     * Returns whether the provided uid has carrier privileges for any active subscription ID.
+     */
+    private static boolean checkCarrierPrivilegeForAnySubId(Context context,
+            Supplier<ITelephony> telephonySupplier, int uid) {
+        SubscriptionManager sm = (SubscriptionManager) context.getSystemService(
+                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        int[] activeSubIds = sm.getActiveSubscriptionIdList();
+        if (activeSubIds != null) {
+            for (int activeSubId : activeSubIds) {
+                if (getCarrierPrivilegeStatus(telephonySupplier, activeSubId, uid)
+                        == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+
     private static int getCarrierPrivilegeStatus(
             Supplier<ITelephony> telephonySupplier, int subId, int uid) {
         ITelephony telephony = telephonySupplier.get();
diff --git a/tests/net/java/android/net/IpMemoryStoreTest.java b/tests/net/java/android/net/IpMemoryStoreTest.java
index 8ff2de9..6e69b34 100644
--- a/tests/net/java/android/net/IpMemoryStoreTest.java
+++ b/tests/net/java/android/net/IpMemoryStoreTest.java
@@ -111,13 +111,12 @@
 
     @Test
     public void testNetworkAttributes() throws Exception {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         final String l2Key = "fakeKey";
 
         mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         verify(mMockService, times(1)).storeNetworkAttributes(eq(l2Key),
                 mNapCaptor.capture(), any());
         assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue()));
@@ -135,7 +134,7 @@
 
     @Test
     public void testPrivateData() throws RemoteException {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         final Blob b = new Blob();
         b.data = TEST_BLOB_DATA;
         final String l2Key = "fakeKey";
@@ -162,7 +161,7 @@
     @Test
     public void testFindL2Key()
             throws UnknownHostException, RemoteException, Exception {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         final String l2Key = "fakeKey";
 
         mStore.findL2Key(TEST_NETWORK_ATTRIBUTES,
@@ -177,7 +176,7 @@
 
     @Test
     public void testIsSameNetwork() throws UnknownHostException, RemoteException {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         final String l2Key1 = "fakeKey1";
         final String l2Key2 = "fakeKey2";
 
@@ -193,7 +192,7 @@
 
     @Test
     public void testEnqueuedIpMsRequests() throws Exception {
-        startIpMemoryStore(false);
+        startIpMemoryStore(false /* supplyService */);
 
         final Blob b = new Blob();
         b.data = TEST_BLOB_DATA;
@@ -201,9 +200,8 @@
 
         // enqueue multiple ipms requests
         mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveNetworkAttributes(l2Key,
                 (status, key, attr) -> {
                     assertTrue("Retrieve network attributes not successful : "
@@ -212,9 +210,8 @@
                     assertEquals(TEST_NETWORK_ATTRIBUTES, attr);
                 });
         mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME,
                 (status, key, name, data) -> {
                     assertTrue("Retrieve blob status not successful : " + status.resultCode,
@@ -240,7 +237,7 @@
 
     @Test
     public void testEnqueuedIpMsRequestsWithException() throws Exception {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         doThrow(RemoteException.class).when(mMockService).retrieveNetworkAttributes(any(), any());
 
         final Blob b = new Blob();
@@ -249,9 +246,8 @@
 
         // enqueue multiple ipms requests
         mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveNetworkAttributes(l2Key,
                 (status, key, attr) -> {
                     assertTrue("Retrieve network attributes not successful : "
@@ -260,9 +256,8 @@
                     assertEquals(TEST_NETWORK_ATTRIBUTES, attr);
                 });
         mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME,
                 (status, key, name, data) -> {
                     assertTrue("Retrieve blob status not successful : " + status.resultCode,
@@ -286,7 +281,7 @@
 
     @Test
     public void testEnqueuedIpMsRequestsCallbackFunctionWithException() throws Exception {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
 
         final Blob b = new Blob();
         b.data = TEST_BLOB_DATA;
@@ -294,9 +289,8 @@
 
         // enqueue multiple ipms requests
         mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveNetworkAttributes(l2Key,
                 (status, key, attr) -> {
                     throw new RuntimeException("retrieveNetworkAttributes test");
@@ -320,6 +314,7 @@
 
         inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(),
                 any());
+        inOrder.verify(mMockService).retrieveNetworkAttributes(eq(l2Key), any());
         inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME),
                 eq(b), any());
         inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID),
diff --git a/tests/net/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/net/java/com/android/server/connectivity/PermissionMonitorTest.java
index 62a4718..df1f57f 100644
--- a/tests/net/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/net/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -524,7 +524,7 @@
 
         SparseIntArray netdPermissionsAppIds = new SparseIntArray();
         netdPermissionsAppIds.put(MOCK_UID1, INetd.PERMISSION_INTERNET);
-        netdPermissionsAppIds.put(MOCK_UID2, INetd.NO_PERMISSIONS);
+        netdPermissionsAppIds.put(MOCK_UID2, INetd.PERMISSION_NONE);
         netdPermissionsAppIds.put(SYSTEM_UID1, INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS);
         netdPermissionsAppIds.put(SYSTEM_UID2, INetd.PERMISSION_UPDATE_DEVICE_STATS);
@@ -534,7 +534,7 @@
 
         mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET,
                 new int[]{MOCK_UID1});
-        mNetdServiceMonitor.expectPermission(INetd.NO_PERMISSIONS, new int[]{MOCK_UID2});
+        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID2});
         mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{SYSTEM_UID1});
         mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UPDATE_DEVICE_STATS,
@@ -553,8 +553,8 @@
         mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{SYSTEM_UID2});
 
         // Revoke permission from SYSTEM_UID1, expect no permission stored.
-        mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID1, INetd.NO_PERMISSIONS);
-        mNetdServiceMonitor.expectPermission(INetd.NO_PERMISSIONS, new int[]{SYSTEM_UID1});
+        mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID1, INetd.PERMISSION_NONE);
+        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{SYSTEM_UID1});
     }
 
     private PackageInfo addPackage(String packageName, int uid, String[] permissions)