Merge "Subtle improvements to notification logging." into tm-qpr-dev
diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
index c43c832..9b64edf 100644
--- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -164,6 +164,13 @@
             @ElapsedRealtimeLong long elapsedRealtime);
 
     /**
+     * Puts the list of apps in the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RARE}
+     * bucket.
+     * @param restoredApps the list of restored apps
+     */
+    void restoreAppsToRare(@NonNull Set<String> restoredApps, int userId);
+
+    /**
      * Put the specified app in the
      * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
      * bucket. If it has been used by the user recently, the restriction will delayed until an
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 9e3e355..5d9f335 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -23,6 +23,7 @@
 import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED;
 import static android.app.usage.UsageStatsManager.REASON_MAIN_TIMEOUT;
 import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE;
+import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_APP_RESTORED;
 import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_APP_UPDATE;
 import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY;
 import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_USER_FLAG_INTERACTION;
@@ -1605,6 +1606,26 @@
     }
 
     @Override
+    public void restoreAppsToRare(Set<String> restoredApps, int userId) {
+        final int reason = REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_RESTORED;
+        final long nowElapsed = mInjector.elapsedRealtime();
+        for (String packageName : restoredApps) {
+            // If the package is not installed, don't allow the bucket to be set.
+            if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
+                Slog.e(TAG, "Tried to restore bucket for uninstalled app: " + packageName);
+                continue;
+            }
+
+            final int standbyBucket = getAppStandbyBucket(packageName, userId, nowElapsed, false);
+            // Only update the standby bucket to RARE if the app is still in the NEVER bucket.
+            if (standbyBucket == STANDBY_BUCKET_NEVER) {
+                setAppStandbyBucket(packageName, userId, STANDBY_BUCKET_RARE, reason,
+                        nowElapsed, false);
+            }
+        }
+    }
+
+    @Override
     public void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId,
             int callingUid, int callingPid) {
         setAppStandbyBuckets(
diff --git a/core/java/android/app/usage/UsageStats.java b/core/java/android/app/usage/UsageStats.java
index d61abc6..e213c93 100644
--- a/core/java/android/app/usage/UsageStats.java
+++ b/core/java/android/app/usage/UsageStats.java
@@ -293,6 +293,17 @@
     }
 
     /**
+     * Returns the last time the package was used - defined by the latest of
+     * mLastTimeUsed, mLastTimeVisible, mLastTimeForegroundServiceUsed, or mLastTimeComponentUsed.
+     * @hide
+     */
+    public long getLastTimePackageUsed() {
+        return Math.max(mLastTimeUsed,
+                        Math.max(mLastTimeVisible,
+                                 Math.max(mLastTimeForegroundServiceUsed, mLastTimeComponentUsed)));
+    }
+
+    /**
      * Returns the number of times the app was launched as an activity from outside of the app.
      * Excludes intra-app activity transitions.
      * @hide
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index c013fcd..1dfc7d4 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -220,6 +220,11 @@
      */
     public static final int REASON_SUB_DEFAULT_APP_UPDATE = 0x0001;
     /**
+     * The app was restored.
+     * @hide
+     */
+    public static final int REASON_SUB_DEFAULT_APP_RESTORED = 0x0002;
+    /**
      * The app was interacted with in some way by the system.
      * @hide
      */
@@ -1209,6 +1214,9 @@
                     case REASON_SUB_DEFAULT_APP_UPDATE:
                         sb.append("-au");
                         break;
+                    case REASON_SUB_DEFAULT_APP_RESTORED:
+                        sb.append("-ar");
+                        break;
                 }
                 break;
             case REASON_MAIN_FORCED_BY_SYSTEM:
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 20a4fdf..10d6f2d 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -542,14 +542,17 @@
 
                 int minVer = DEFAULT_MIN_SDK_VERSION;
                 String minCode = null;
+                boolean minAssigned = false;
                 int targetVer = DEFAULT_TARGET_SDK_VERSION;
                 String targetCode = null;
 
                 if (!TextUtils.isEmpty(minSdkVersionString)) {
                     try {
                         minVer = Integer.parseInt(minSdkVersionString);
+                        minAssigned = true;
                     } catch (NumberFormatException ignored) {
                         minCode = minSdkVersionString;
+                        minAssigned = !TextUtils.isEmpty(minCode);
                     }
                 }
 
@@ -558,7 +561,7 @@
                         targetVer = Integer.parseInt(targetSdkVersionString);
                     } catch (NumberFormatException ignored) {
                         targetCode = targetSdkVersionString;
-                        if (minCode == null) {
+                        if (!minAssigned) {
                             minCode = targetCode;
                         }
                     }
diff --git a/core/java/android/debug/AdbManagerInternal.java b/core/java/android/debug/AdbManagerInternal.java
index d730129..e448706 100644
--- a/core/java/android/debug/AdbManagerInternal.java
+++ b/core/java/android/debug/AdbManagerInternal.java
@@ -55,6 +55,12 @@
     public abstract File getAdbTempKeysFile();
 
     /**
+     * Notify the AdbManager that the key files have changed and any in-memory state should be
+     * reloaded.
+     */
+    public abstract void notifyKeyFilesUpdated();
+
+    /**
      * Starts adbd for a transport.
      */
     public abstract void startAdbdForTransport(byte transportType);
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index 3a042a5..e8e4fc9 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -26,7 +26,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -173,38 +172,63 @@
         }
     }
 
-    void apply(@NonNull Chunk chunk) {
+    void apply(Chunk chunk) {
+        List<ProgramSelector.Identifier> removedList = new ArrayList<>();
+        List<ProgramSelector.Identifier> changedList = new ArrayList<>();
+        List<ProgramList.ListCallback> listCallbacksCopied;
+        List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>();
         synchronized (mLock) {
             if (mIsClosed) return;
 
             mIsComplete = false;
+            listCallbacksCopied = new ArrayList<>(mListCallbacks);
 
             if (chunk.isPurge()) {
-                new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id));
+                for (ProgramSelector.Identifier id : mPrograms.keySet()) {
+                    removeLocked(id, removedList);
+                }
             }
 
-            chunk.getRemoved().stream().forEach(id -> removeLocked(id));
-            chunk.getModified().stream().forEach(info -> putLocked(info));
+            chunk.getRemoved().stream().forEach(id -> removeLocked(id, removedList));
+            chunk.getModified().stream().forEach(info -> putLocked(info, changedList));
 
             if (chunk.isComplete()) {
                 mIsComplete = true;
-                mOnCompleteListeners.forEach(cb -> cb.onComplete());
+                onCompleteListenersCopied = new ArrayList<>(mOnCompleteListeners);
+            }
+        }
+
+        for (int i = 0; i < removedList.size(); i++) {
+            for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
+                listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i));
+            }
+        }
+        for (int i = 0; i < changedList.size(); i++) {
+            for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
+                listCallbacksCopied.get(cbIndex).onItemChanged(changedList.get(i));
+            }
+        }
+        if (chunk.isComplete()) {
+            for (int cbIndex = 0; cbIndex < onCompleteListenersCopied.size(); cbIndex++) {
+                onCompleteListenersCopied.get(cbIndex).onComplete();
             }
         }
     }
 
-    private void putLocked(@NonNull RadioManager.ProgramInfo value) {
+    private void putLocked(RadioManager.ProgramInfo value,
+            List<ProgramSelector.Identifier> changedIdentifierList) {
         ProgramSelector.Identifier key = value.getSelector().getPrimaryId();
         mPrograms.put(Objects.requireNonNull(key), value);
         ProgramSelector.Identifier sel = value.getSelector().getPrimaryId();
-        mListCallbacks.forEach(cb -> cb.onItemChanged(sel));
+        changedIdentifierList.add(sel);
     }
 
-    private void removeLocked(@NonNull ProgramSelector.Identifier key) {
+    private void removeLocked(ProgramSelector.Identifier key,
+            List<ProgramSelector.Identifier> removedIdentifierList) {
         RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key));
         if (removed == null) return;
         ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId();
-        mListCallbacks.forEach(cb -> cb.onItemRemoved(sel));
+        removedIdentifierList.add(sel);
     }
 
     /**
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index 22ddbcc..c70f1f0 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -59,6 +59,18 @@
     },
     {
       "file_patterns": [
+        "Parcel\\.java",
+        "[^/]*Bundle[^/]*\\.java"
+      ],
+      "name": "FrameworksMockingCoreTests",
+      "options": [
+        { "include-filter":  "android.os.BundleRecyclingTest"},
+        { "exclude-annotation": "androidx.test.filters.FlakyTest" },
+        { "exclude-annotation": "org.junit.Ignore" }
+      ]
+    },
+    {
+      "file_patterns": [
         "BatteryUsageStats[^/]*\\.java",
         "PowerComponents\\.java",
         "[^/]*BatteryConsumer[^/]*\\.java"
diff --git a/core/java/android/view/IRemoteAnimationRunner.aidl b/core/java/android/view/IRemoteAnimationRunner.aidl
index 1f64fb8..1981c9d 100644
--- a/core/java/android/view/IRemoteAnimationRunner.aidl
+++ b/core/java/android/view/IRemoteAnimationRunner.aidl
@@ -46,5 +46,5 @@
      * won't have any effect anymore.
      */
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
-    void onAnimationCancelled();
+    void onAnimationCancelled(boolean isKeyguardOccluded);
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 64151d9..9a3957c 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2816,10 +2816,6 @@
         // Execute enqueued actions on every traversal in case a detached view enqueued an action
         getRunQueue().executeActions(mAttachInfo.mHandler);
 
-        if (mApplyInsetsRequested) {
-            dispatchApplyInsets(host);
-        }
-
         if (mFirst) {
             // make sure touch mode code executes by setting cached value
             // to opposite of the added touch mode.
@@ -2883,6 +2879,18 @@
             }
         }
 
+        if (mApplyInsetsRequested) {
+            dispatchApplyInsets(host);
+            if (mLayoutRequested) {
+                // Short-circuit catching a new layout request here, so
+                // we don't need to go through two layout passes when things
+                // change due to fitting system windows, which can happen a lot.
+                windowSizeMayChange |= measureHierarchy(host, lp,
+                        mView.getContext().getResources(),
+                        desiredWindowWidth, desiredWindowHeight);
+            }
+        }
+
         if (layoutRequested) {
             // Clear this now, so that if anything requests a layout in the
             // rest of this function we will catch it and re-run a full
diff --git a/core/java/android/window/SizeConfigurationBuckets.java b/core/java/android/window/SizeConfigurationBuckets.java
index f474f0a..998bec0 100644
--- a/core/java/android/window/SizeConfigurationBuckets.java
+++ b/core/java/android/window/SizeConfigurationBuckets.java
@@ -104,24 +104,15 @@
     /**
      * Get the changes between two configurations but don't count changes in sizes if they don't
      * cross boundaries that are important to the app.
-     *
-     * This is a static helper to deal with null `buckets`. When no buckets have been specified,
-     * this actually filters out all 3 size-configs. This is legacy behavior.
      */
     public static int filterDiff(int diff, @NonNull Configuration oldConfig,
             @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) {
+        if (buckets == null) {
+            return diff;
+        }
+
         final boolean nonSizeLayoutFieldsUnchanged =
                 areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout);
-        if (buckets == null) {
-            // Only unflip CONFIG_SCREEN_LAYOUT if non-size-related  attributes of screen layout do
-            // not change.
-            if (nonSizeLayoutFieldsUnchanged) {
-                return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE
-                        | CONFIG_SCREEN_LAYOUT);
-            } else {
-                return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE);
-            }
-        }
         if ((diff & CONFIG_SCREEN_SIZE) != 0) {
             final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp,
                     newConfig.screenWidthDp)
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 52fd7fe..22340c6 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -75,6 +75,8 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_LOADING_TO_SHOW_INFO_WITH_ACTIONS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_SHOW_FUNCTION_SCREEN_WITH_ACTIONS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__UNFOLD_ANIM;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_DIALOG_OPEN;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH;
@@ -206,6 +208,8 @@
     public static final int CUJ_SETTINGS_TOGGLE = 57;
     public static final int CUJ_SHADE_DIALOG_OPEN = 58;
     public static final int CUJ_USER_DIALOG_OPEN = 59;
+    public static final int CUJ_TASKBAR_EXPAND = 60;
+    public static final int CUJ_TASKBAR_COLLAPSE = 61;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -274,6 +278,8 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_TOGGLE,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_DIALOG_OPEN,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_DIALOG_OPEN,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE,
     };
 
     private static volatile InteractionJankMonitor sInstance;
@@ -354,6 +360,8 @@
             CUJ_SETTINGS_TOGGLE,
             CUJ_SHADE_DIALOG_OPEN,
             CUJ_USER_DIALOG_OPEN,
+            CUJ_TASKBAR_EXPAND,
+            CUJ_TASKBAR_COLLAPSE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -792,6 +800,10 @@
                 return "SHADE_DIALOG_OPEN";
             case CUJ_USER_DIALOG_OPEN:
                 return "USER_DIALOG_OPEN";
+            case CUJ_TASKBAR_EXPAND:
+                return "TASKBAR_EXPAND";
+            case CUJ_TASKBAR_COLLAPSE:
+                return "TASKBAR_COLLAPSE";
         }
         return "UNKNOWN";
     }
diff --git a/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java b/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java
new file mode 100644
index 0000000..7c76498
--- /dev/null
+++ b/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2022 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 android.os;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Test for verifying {@link android.os.Bundle} recycles the underlying parcel where needed.
+ *
+ * <p>Build/Install/Run:
+ *  atest FrameworksMockingCoreTests:android.os.BundleRecyclingTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class BundleRecyclingTest {
+    private Parcel mParcelSpy;
+    private Bundle mBundle;
+
+    @Before
+    public void setUp() throws Exception {
+        setUpBundle(/* hasLazy */ true);
+    }
+
+    @Test
+    public void bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce() {
+        setUpBundle(/* hasLazy */ false);
+        // Will unparcel and immediately recycle parcel
+        assertNotNull(mBundle.getString("key"));
+        verify(mParcelSpy, times(1)).recycle();
+        assertFalse(mBundle.isDefinitelyEmpty());
+
+        // Should not recycle again
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+        assertTrue(mBundle.isDefinitelyEmpty());
+    }
+
+    @Test
+    public void bundleClear_whenParcelled_recyclesParcel() {
+        assertTrue(mBundle.isParcelled());
+        verify(mParcelSpy, times(0)).recycle();
+
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        // Should not recycle again
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+    }
+
+    @Test
+    public void bundleClear_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel() {
+        // Will unparcel with a lazy value, and immediately unwrap the lazy value,
+        // with no lazy values left at the end of getParcelable
+        assertNotNull(mBundle.getParcelable("key", CustomParcelable.class));
+        verify(mParcelSpy, times(0)).recycle();
+
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        // Should not recycle again
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+    }
+
+    @Test
+    public void bundleClear_whenUnparcelledWithLazy_recyclesParcel() {
+        // Will unparcel but keep the CustomParcelable lazy
+        assertFalse(mBundle.isEmpty());
+        verify(mParcelSpy, times(0)).recycle();
+
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        // Should not recycle again
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+    }
+
+    @Test
+    public void bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel() {
+        Bundle copy = new Bundle();
+        copy.putAll(mBundle);
+
+        mBundle.clear();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        copy.clear();
+        assertTrue(copy.isDefinitelyEmpty());
+
+        verify(mParcelSpy, never()).recycle();
+    }
+
+    @Test
+    public void bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel() {
+        // Will unparcel but keep the CustomParcelable lazy
+        assertFalse(mBundle.isEmpty());
+
+        Bundle copy = mBundle.deepCopy();
+        copy.putAll(mBundle);
+
+        mBundle.clear();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        copy.clear();
+        assertTrue(copy.isDefinitelyEmpty());
+
+        verify(mParcelSpy, never()).recycle();
+    }
+
+    private void setUpBundle(boolean hasLazy) {
+        AtomicReference<Parcel> parcel = new AtomicReference<>();
+        StaticMockitoSession session = mockitoSession()
+                .strictness(Strictness.STRICT_STUBS)
+                .spyStatic(Parcel.class)
+                .startMocking();
+        doAnswer((Answer<Parcel>) invocationOnSpy -> {
+            Parcel spy = (Parcel) invocationOnSpy.callRealMethod();
+            spyOn(spy);
+            parcel.set(spy);
+            return spy;
+        }).when(() -> Parcel.obtain());
+
+        Bundle bundle = new Bundle();
+        bundle.setClassLoader(getClass().getClassLoader());
+        Parcel p = createBundle(hasLazy);
+        bundle.readFromParcel(p);
+        p.recycle();
+
+        session.finishMocking();
+
+        mParcelSpy = parcel.get();
+        mBundle = bundle;
+    }
+
+    /**
+     * Create a test bundle, parcel it and return the parcel.
+     */
+    private Parcel createBundle(boolean hasLazy) {
+        final Bundle source = new Bundle();
+        if (hasLazy) {
+            source.putParcelable("key", new CustomParcelable(13, "Tiramisu"));
+        } else {
+            source.putString("key", "tiramisu");
+        }
+        return getParcelledBundle(source);
+    }
+
+    /**
+     * Take a bundle, write it to a parcel and return the parcel.
+     */
+    private Parcel getParcelledBundle(Bundle bundle) {
+        final Parcel p = Parcel.obtain();
+        // Don't use p.writeParcelabe(), which would write the creator, which we don't need.
+        bundle.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        return p;
+    }
+
+    private static class CustomParcelable implements Parcelable {
+        public final int integer;
+        public final String string;
+
+        CustomParcelable(int integer, String string) {
+            this.integer = integer;
+            this.string = string;
+        }
+
+        protected CustomParcelable(Parcel in) {
+            integer = in.readInt();
+            string = in.readString();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(integer);
+            out.writeString(string);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof CustomParcelable)) {
+                return false;
+            }
+            CustomParcelable that = (CustomParcelable) other;
+            return integer == that.integer && string.equals(that.string);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(integer, string);
+        }
+
+        public static final Creator<CustomParcelable> CREATOR = new Creator<CustomParcelable>() {
+            @Override
+            public CustomParcelable createFromParcel(Parcel in) {
+                return new CustomParcelable(in);
+            }
+            @Override
+            public CustomParcelable[] newArray(int size) {
+                return new CustomParcelable[size];
+            }
+        };
+    }
+}
diff --git a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java
index fa4aa80..ed857e8 100644
--- a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java
+++ b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java
@@ -88,26 +88,15 @@
     }
 
     /**
-     * Tests that null size configuration buckets unflips the correct configuration flags.
+     * Tests that {@code null} size configuration buckets do not unflip the configuration flags.
      */
     @Test
     public void testNullSizeConfigurationBuckets() {
-        // Check that all 3 size configurations are filtered out of the diff if the buckets are null
-        // and non-size attributes of screen layout are unchanged. Add a non-size related config
-        // change (i.e. CONFIG_LOCALE) to test that the diff is not set to zero.
         final int diff = CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE | CONFIG_SCREEN_LAYOUT
                 | CONFIG_LOCALE;
         final int filteredDiffNonSizeLayoutUnchanged = SizeConfigurationBuckets.filterDiff(diff,
                 Configuration.EMPTY, Configuration.EMPTY, null);
-        assertEquals(CONFIG_LOCALE, filteredDiffNonSizeLayoutUnchanged);
-
-        // Check that only screen size and smallest screen size are filtered out of the diff if the
-        // buckets are null and non-size attributes of screen layout are changed.
-        final Configuration newConfig = new Configuration();
-        newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES;
-        final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff,
-                Configuration.EMPTY, newConfig, null);
-        assertEquals(CONFIG_SCREEN_LAYOUT | CONFIG_LOCALE, filteredDiffNonSizeLayoutChanged);
+        assertEquals(diff, filteredDiffNonSizeLayoutUnchanged);
     }
 
     /**
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index f8da95d..6706e4e 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -475,12 +475,6 @@
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/ResetTargetTaskHelper.java"
     },
-    "-1635750891": {
-      "message": "Received remote change for Display[%d], applied: [%dx%d, rot = %d]",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_CONFIGURATION",
-      "at": "com\/android\/server\/wm\/RemoteDisplayChangeController.java"
-    },
     "-1633115609": {
       "message": "Key dispatch not paused for screen off",
       "level": "VERBOSE",
@@ -1711,6 +1705,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
+    "-417730399": {
+      "message": "Preparing to sync a window that was already in the sync, so try dropping buffer. win=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_SYNC_ENGINE",
+      "at": "com\/android\/server\/wm\/WindowState.java"
+    },
     "-415865166": {
       "message": "findFocusedWindow: Found new focus @ %s",
       "level": "VERBOSE",
@@ -2137,6 +2137,12 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimation.java"
     },
+    "-4263657": {
+      "message": "Got a buffer for request id=%d but latest request is id=%d. Since the buffer is out-of-date, drop it. win=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_SYNC_ENGINE",
+      "at": "com\/android\/server\/wm\/WindowState.java"
+    },
     "3593205": {
       "message": "commitVisibility: %s: visible=%b mVisibleRequested=%b",
       "level": "VERBOSE",
@@ -2599,6 +2605,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
+    "385237117": {
+      "message": "moveFocusableActivityToTop: already on top and focused, activity=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_FOCUS",
+      "at": "com\/android\/server\/wm\/ActivityRecord.java"
+    },
     "385595355": {
       "message": "Starting animation on %s: type=%d, anim=%s",
       "level": "VERBOSE",
@@ -3403,6 +3415,12 @@
       "group": "WM_DEBUG_BOOT",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1239439010": {
+      "message": "moveFocusableActivityToTop: set focused, activity=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_FOCUS",
+      "at": "com\/android\/server\/wm\/ActivityRecord.java"
+    },
     "1252594551": {
       "message": "Window types in WindowContext and LayoutParams.type should match! Type from LayoutParams is %d, but type from WindowContext is %d",
       "level": "WARN",
@@ -3877,12 +3895,6 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/WindowStateAnimator.java"
     },
-    "1764619787": {
-      "message": "Remote change for Display[%d]: timeout reached",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_CONFIGURATION",
-      "at": "com\/android\/server\/wm\/RemoteDisplayChangeController.java"
-    },
     "1774661765": {
       "message": "Devices still not ready after waiting %d milliseconds before attempting to detect safe mode.",
       "level": "WARN",
@@ -3991,12 +4003,6 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "1856211951": {
-      "message": "moveFocusableActivityToTop: already on top, activity=%s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_FOCUS",
-      "at": "com\/android\/server\/wm\/ActivityRecord.java"
-    },
     "1856783490": {
       "message": "resumeTopActivity: Restarting %s",
       "level": "DEBUG",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index 1ac3317..c4f3709 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -83,9 +83,9 @@
     }
 
     @Override
-    public void onAnimationCancelled() {
+    public void onAnimationCancelled(boolean isKeyguardOccluded) {
         if (TaskFragmentAnimationController.DEBUG) {
-            Log.v(TAG, "onAnimationCancelled");
+            Log.v(TAG, "onAnimationCancelled: isKeyguardOccluded=" + isKeyguardOccluded);
         }
         mHandler.post(this::cancelAnimation);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 4eba169..cf2734c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -591,7 +591,7 @@
                         final Rect insets = computeInsets(fraction);
                         getSurfaceTransactionHelper().scaleAndCrop(tx, leash,
                                 sourceHintRect, initialSourceValue, bounds, insets,
-                                isInPipDirection);
+                                isInPipDirection, fraction);
                         if (shouldApplyCornerRadius()) {
                             final Rect sourceBounds = new Rect(initialContainerRect);
                             sourceBounds.inset(insets);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index a017a26..c0bc108 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -104,7 +104,7 @@
     public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx,
             SurfaceControl leash, Rect sourceRectHint,
             Rect sourceBounds, Rect destinationBounds, Rect insets,
-            boolean isInPipDirection) {
+            boolean isInPipDirection, float fraction) {
         mTmpDestinationRect.set(sourceBounds);
         // Similar to {@link #scale}, we want to position the surface relative to the screen
         // coordinates so offset the bounds to 0,0
@@ -116,9 +116,13 @@
         if (isInPipDirection
                 && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
             // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only.
-            scale = sourceBounds.width() <= sourceBounds.height()
+            final float endScale = sourceBounds.width() <= sourceBounds.height()
                     ? (float) destinationBounds.width() / sourceRectHint.width()
                     : (float) destinationBounds.height() / sourceRectHint.height();
+            final float startScale = sourceBounds.width() <= sourceBounds.height()
+                    ? (float) destinationBounds.width() / sourceBounds.width()
+                    : (float) destinationBounds.height() / sourceBounds.height();
+            scale = (1 - fraction) * startScale + fraction * endScale;
         } else {
             scale = sourceBounds.width() <= sourceBounds.height()
                     ? (float) destinationBounds.width() / sourceBounds.width()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 37c0c9b..22b0ccb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1472,6 +1472,11 @@
                     "%s: Abort animation, invalid leash", TAG);
             return null;
         }
+        if (isInPipDirection(direction)
+                && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) {
+            // The given source rect hint is too small for enter PiP animation, reset it to null.
+            sourceHintRect = null;
+        }
         final int rotationDelta = mWaitForFixedRotation
                 ? deltaRotation(mCurrentRotation, mNextRotation)
                 : Surface.ROTATION_0;
@@ -1546,6 +1551,20 @@
     }
 
     /**
+     * This is a situation in which the source rect hint on at least one axis is smaller
+     * than the destination bounds, which represents a problem because we would have to scale
+     * up that axis to fit the bounds. So instead, just fallback to the non-source hint
+     * animation in this case.
+     *
+     * @return {@code false} if the given source is too small to use for the entering animation.
+     */
+    private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) {
+        return sourceRectHint != null
+                && sourceRectHint.width() > destinationBounds.width()
+                && sourceRectHint.height() > destinationBounds.height();
+    }
+
+    /**
      * Sync with {@link SplitScreenController} on destination bounds if PiP is going to
      * split screen.
      *
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 4e7b20e..59b0afe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -457,10 +457,10 @@
             }
 
             @Override
-            public void onAnimationCancelled() {
+            public void onAnimationCancelled(boolean isKeyguardOccluded) {
                 onRemoteAnimationFinishedOrCancelled(evictWct);
                 try {
-                    adapter.getRunner().onAnimationCancelled();
+                    adapter.getRunner().onAnimationCancelled(isKeyguardOccluded);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error starting remote animation", e);
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 05e5b8e..dcd6277 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -866,13 +866,19 @@
             });
         };
         va.addListener(new AnimatorListenerAdapter() {
+            private boolean mFinished = false;
+
             @Override
             public void onAnimationEnd(Animator animation) {
+                if (mFinished) return;
+                mFinished = true;
                 finisher.run();
             }
 
             @Override
             public void onAnimationCancel(Animator animation) {
+                if (mFinished) return;
+                mFinished = true;
                 finisher.run();
             }
         });
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
index 61e11e8..61e92f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
@@ -107,7 +107,7 @@
             }
 
             @Override
-            public void onAnimationCancelled() throws RemoteException {
+            public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
                 mCancelled = true;
                 mApps = mWallpapers = mNonApps = null;
                 checkApply();
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
index d4298b8..49eca63 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
@@ -25,11 +25,13 @@
 import androidx.test.uiautomator.BySelector
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
+import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
 import com.android.wm.shell.flicker.testapp.Components
+import org.junit.Assert
 
 class SplitScreenHelper(
     instrumentation: Instrumentation,
@@ -187,5 +189,23 @@
                 SystemClock.sleep(GESTURE_STEP_MS)
             }
         }
+
+        fun createShortcutOnHotseatIfNotExist(
+            taplInstrumentation: LauncherInstrumentation,
+            appName: String
+        ) {
+            taplInstrumentation.workspace
+                .deleteAppIcon(taplInstrumentation.workspace.getHotseatAppIcon(0))
+            val allApps = taplInstrumentation.workspace.switchToAllApps()
+            allApps.freeze()
+            try {
+                val appIconSrc = allApps.getAppIcon(appName)
+                Assert.assertNotNull("Unable to find app icon", appIconSrc)
+                val appIconDest = appIconSrc.dragToHotseat(0)
+                Assert.assertNotNull("Unable to drag app icon on hotseat", appIconDest)
+            } finally {
+                allApps.unfreeze()
+            }
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
new file mode 100644
index 0000000..05c6e24
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 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.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen by dragging app icon from taskbar.
+ * This test is only for large screen devices.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromTaskbar`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class EnterSplitScreenByDragFromTaskbar(
+    testSpec: FlickerTestParameter
+) : SplitScreenBase(testSpec) {
+
+    @Before
+    fun before() {
+        Assume.assumeTrue(taplInstrumentation.isTablet)
+    }
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                eachRun {
+                    taplInstrumentation.goHome()
+                    SplitScreenHelper.createShortcutOnHotseatIfNotExist(
+                        taplInstrumentation, secondaryApp.appName
+                    )
+                    primaryApp.launchViaIntent(wmHelper)
+                }
+            }
+            transitions {
+                taplInstrumentation.launchedAppState.taskbar
+                    .getAppIcon(secondaryApp.appName)
+                    .dragToSplitscreen(
+                        secondaryApp.component.packageName,
+                        primaryApp.component.packageName
+                    )
+            }
+        }
+
+    @Presubmit
+    @Test
+    fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+
+    @Presubmit
+    @Test
+    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component)
+
+    @Presubmit
+    @Test
+    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp.component)
+
+    @Presubmit
+    @Test
+    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        testSpec.endRotation, primaryApp.component, false /* splitLeftTop */
+    )
+
+    @Presubmit
+    @Test
+    fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
+        testSpec.endRotation, secondaryApp.component, true /* splitLeftTop */
+    )
+
+    @Presubmit
+    @Test
+    fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component)
+
+    @Presubmit
+    @Test
+    fun secondaryAppWindowBecomesVisible() =
+        testSpec.appWindowBecomesVisible(secondaryApp.component)
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+                repetitions = SplitScreenHelper.TEST_REPETITIONS,
+                supportedNavigationModes =
+                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+            )
+        }
+    }
+}
diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl
index b136d5b..2bdd5c8 100644
--- a/media/java/android/media/projection/IMediaProjection.aidl
+++ b/media/java/android/media/projection/IMediaProjection.aidl
@@ -17,7 +17,7 @@
 package android.media.projection;
 
 import android.media.projection.IMediaProjectionCallback;
-import android.window.WindowContainerToken;
+import android.os.IBinder;
 
 /** {@hide} */
 interface IMediaProjection {
@@ -31,14 +31,14 @@
     void unregisterCallback(IMediaProjectionCallback callback);
 
     /**
-     * Returns the {@link android.window.WindowContainerToken} identifying the task to record, or
-     * {@code null} if there is none.
+     * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if
+     * there is none.
      */
-    WindowContainerToken getTaskRecordingWindowContainerToken();
+    IBinder getLaunchCookie();
 
     /**
-     * Updates the {@link android.window.WindowContainerToken} identifying the task to record, or
-     * {@code null} if there is none.
+     * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if
+     * there is none.
      */
-    void setTaskRecordingWindowContainerToken(in WindowContainerToken token);
+    void setLaunchCookie(in IBinder launchCookie);
 }
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index ba7bf3f..ae44fc5 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -25,13 +25,13 @@
 import android.hardware.display.VirtualDisplay;
 import android.hardware.display.VirtualDisplayConfig;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.view.ContentRecordingSession;
 import android.view.Surface;
-import android.window.WindowContainerToken;
 
 import java.util.Map;
 
@@ -172,18 +172,16 @@
             @NonNull VirtualDisplayConfig.Builder virtualDisplayConfig,
             @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
         try {
-            final WindowContainerToken taskWindowContainerToken =
-                    mImpl.getTaskRecordingWindowContainerToken();
+            final IBinder launchCookie = mImpl.getLaunchCookie();
             Context windowContext = null;
             ContentRecordingSession session;
-            if (taskWindowContainerToken == null) {
+            if (launchCookie == null) {
                 windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(),
                         TYPE_APPLICATION, null /* options */);
                 session = ContentRecordingSession.createDisplaySession(
                         windowContext.getWindowContextToken());
             } else {
-                session = ContentRecordingSession.createTaskSession(
-                        taskWindowContainerToken.asBinder());
+                session = ContentRecordingSession.createTaskSession(launchCookie);
             }
             virtualDisplayConfig.setWindowManagerMirroring(true);
             final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index f05c1e2..fa87de2 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -93,6 +93,7 @@
         "SystemUISharedLib",
         "SystemUI-statsd",
         "SettingsLib",
+        "androidx.core_core-ktx",
         "androidx.viewpager2_viewpager2",
         "androidx.legacy_legacy-support-v4",
         "androidx.recyclerview_recyclerview",
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 3bd6d51..f9a9ef6 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -52,6 +52,17 @@
       ]
     },
     {
+      "name": "SystemUIGoogleScreenshotTests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
       // Permission indicators
       "name": "CtsPermission4TestCases",
       "options": [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index 5b47ae5..fbe3356 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -656,7 +656,7 @@
             controller.onLaunchAnimationCancelled()
         }
 
-        override fun onAnimationCancelled() {
+        override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {
             if (timedOut) {
                 return
             }
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 12dfa10..8f8993f 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -59,6 +59,26 @@
 
     </LinearLayout>
 
+    <com.android.systemui.statusbar.KeyguardAffordanceView
+        android:id="@+id/camera_button"
+        android:layout_height="@dimen/keyguard_affordance_height"
+        android:layout_width="@dimen/keyguard_affordance_width"
+        android:layout_gravity="bottom|end"
+        android:src="@drawable/ic_camera_alt_24dp"
+        android:scaleType="center"
+        android:contentDescription="@string/accessibility_camera_button"
+        android:tint="?attr/wallpaperTextColor" />
+
+    <com.android.systemui.statusbar.KeyguardAffordanceView
+        android:id="@+id/left_button"
+        android:layout_height="@dimen/keyguard_affordance_height"
+        android:layout_width="@dimen/keyguard_affordance_width"
+        android:layout_gravity="bottom|start"
+        android:src="@*android:drawable/ic_phone"
+        android:scaleType="center"
+        android:contentDescription="@string/accessibility_phone_button"
+        android:tint="?attr/wallpaperTextColor" />
+
     <ImageView
         android:id="@+id/wallet_button"
         android:layout_height="@dimen/keyguard_affordance_fixed_height"
diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
index 5e8b892..2b3d11b 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
@@ -14,20 +14,19 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<!-- TODO(b/203800646): layout_marginTop doesn't seem to work on some large screens. -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/media_ttt_receiver_chip"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@drawable/media_ttt_chip_background_receiver"
     >
 
     <com.android.internal.widget.CachingIconView
         android:id="@+id/app_icon"
         android:layout_width="@dimen/media_ttt_icon_size_receiver"
         android:layout_height="@dimen/media_ttt_icon_size_receiver"
-        android:layout_gravity="center"
+        android:layout_gravity="center|bottom"
+        android:alpha="0.0"
         />
 
 </FrameLayout>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 771973c..eff4e00 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -37,6 +37,12 @@
         <item>400</item>
     </integer-array>
 
+    <!-- Show mic or phone affordance on Keyguard -->
+    <bool name="config_keyguardShowLeftAffordance">false</bool>
+
+    <!-- Show camera affordance on Keyguard -->
+    <bool name="config_keyguardShowCameraAffordance">false</bool>
+
     <!-- decay duration (from size_max -> size), in ms -->
     <integer name="navigation_bar_deadzone_hold">333</integer>
     <integer name="navigation_bar_deadzone_decay">333</integer>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7f7b871..36a2d64 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -676,6 +676,9 @@
     <!-- The minimum background radius when swiping to a side for the camera / phone affordances. -->
     <dimen name="keyguard_affordance_min_background_radius">30dp</dimen>
 
+    <!-- The size of the touch targets on the keyguard for the affordances. -->
+    <dimen name="keyguard_affordance_touch_target_size">120dp</dimen>
+
     <!-- The grow amount for the camera and phone circles when hinting -->
     <dimen name="hint_grow_amount_sideways">60dp</dimen>
 
@@ -1057,6 +1060,7 @@
     <!-- Since the generic icon isn't circular, we need to scale it down so it still fits within
          the circular chip. -->
     <dimen name="media_ttt_generic_icon_size_receiver">70dp</dimen>
+    <dimen name="media_ttt_receiver_vert_translation">20dp</dimen>
 
     <!-- Window magnification -->
     <dimen name="magnification_border_drag_size">35dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ef672f3..343ec4f6 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -313,6 +313,13 @@
     <string name="accessibility_scanning_face">Scanning face</string>
     <!-- Click action label for accessibility for the smart reply buttons (not shown on-screen).". [CHAR LIMIT=NONE] -->
     <string name="accessibility_send_smart_reply">Send</string>
+    <!-- Content description of the manage notification button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
+    <!-- Click action label for accessibility for the phone button. [CHAR LIMIT=NONE] -->
+    <string name="phone_label">open phone</string>
+    <!-- Click action label for accessibility for the voice assist button. This is not shown on-screen and is an accessibility label for the icon which launches the voice assist from the lock screen.[CHAR LIMIT=NONE] -->
+    <string name="voice_assist_label">open voice assist</string>
+    <!-- Click action label for accessibility for the phone button. [CHAR LIMIT=NONE] -->
+    <string name="camera_label">open camera</string>
     <!-- Button name for "Cancel". [CHAR LIMIT=NONE] -->
     <string name="cancel">Cancel</string>
 
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
index 2c3ff2c..35812e3 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.testing.screenshot
 
 import android.app.Activity
+import android.app.Dialog
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams
@@ -23,20 +24,20 @@
     }
 
     /**
-     * Compare the content of [view] with the golden image identified by [goldenIdentifier] in the
-     * context of [testSpec].
+     * Compare the content of the view provided by [viewProvider] with the golden image identified
+     * by [goldenIdentifier] in the context of [testSpec].
      */
     fun screenshotTest(
         goldenIdentifier: String,
         layoutParams: LayoutParams =
             LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT),
-        view: (Activity) -> View,
+        viewProvider: (Activity) -> View,
     ) {
         activityRule.scenario.onActivity { activity ->
             // Make sure that the activity draws full screen and fits the whole display instead of
             // the system bars.
             activity.window.setDecorFitsSystemWindows(false)
-            activity.setContentView(view(activity), layoutParams)
+            activity.setContentView(viewProvider(activity), layoutParams)
         }
 
         // We call onActivity again because it will make sure that our Activity is done measuring,
@@ -48,4 +49,32 @@
             screenshotRule.screenshotTest(goldenIdentifier, content.getChildAt(0))
         }
     }
+
+    /**
+     * Compare the content of the dialog provided by [dialogProvider] with the golden image
+     * identified by [goldenIdentifier] in the context of [testSpec].
+     */
+    fun dialogScreenshotTest(
+        goldenIdentifier: String,
+        dialogProvider: (Activity) -> Dialog,
+    ) {
+        var dialog: Dialog? = null
+        activityRule.scenario.onActivity { activity ->
+            // Make sure that the dialog draws full screen and fits the whole display instead of the
+            // system bars.
+            dialog =
+                dialogProvider(activity).apply {
+                    window.setDecorFitsSystemWindows(false)
+                    show()
+                }
+        }
+
+        // We call onActivity again because it will make sure that our Dialog is done measuring,
+        // laying out and drawing its content (that we set in the previous onActivity lambda).
+        activityRule.scenario.onActivity {
+            // Check that the content is what we expected.
+            val dialog = dialog ?: error("dialog is null")
+            screenshotRule.screenshotTest(goldenIdentifier, dialog.window.decorView)
+        }
+    }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
index 88fe034..203b236 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
@@ -80,7 +80,8 @@
 
     public PictureInPictureSurfaceTransaction scaleAndCrop(
             SurfaceControl.Transaction tx, SurfaceControl leash,
-            Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets) {
+            Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets,
+            float progress) {
         mTmpSourceRectF.set(sourceBounds);
         mTmpDestinationRect.set(sourceBounds);
         mTmpDestinationRect.inset(insets);
@@ -93,9 +94,13 @@
                     : (float) destinationBounds.height() / sourceBounds.height();
         } else {
             // scale by sourceRectHint if it's not edge-to-edge
-            scale = sourceRectHint.width() <= sourceRectHint.height()
+            final float endScale = sourceRectHint.width() <= sourceRectHint.height()
                     ? (float) destinationBounds.width() / sourceRectHint.width()
                     : (float) destinationBounds.height() / sourceRectHint.height();
+            final float startScale = sourceRectHint.width() <= sourceRectHint.height()
+                    ? (float) destinationBounds.width() / sourceBounds.width()
+                    : (float) destinationBounds.height() / sourceBounds.height();
+            scale = (1 - progress) * startScale + progress * endScale;
         }
         final float left = destinationBounds.left - (insets.left + sourceBounds.left) * scale;
         final float top = destinationBounds.top - (insets.top + sourceBounds.top) * scale;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
index 302ba84..9265f07 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -105,7 +105,7 @@
             }
 
             @Override
-            public void onAnimationCancelled() {
+            public void onAnimationCancelled(boolean isKeyguardOccluded) {
                 remoteAnimationAdapter.onAnimationCancelled();
             }
         };
@@ -220,9 +220,9 @@
                         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                             info.getChanges().get(i).getLeash().release();
                         }
-                        for (int i = leashMap.size() - 1; i >= 0; --i) {
-                            leashMap.valueAt(i).release();
-                        }
+                        // Don't release here since launcher might still be using them. Instead
+                        // let launcher release them (eg. via RemoteAnimationTargets)
+                        leashMap.clear();
                         try {
                             finishCallback.onTransitionFinished(null /* wct */, finishTransaction);
                         } catch (RemoteException e) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index cd8ca05..afc58ef 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -45,6 +45,7 @@
 import android.content.res.Resources;
 import android.hardware.SensorManager;
 import android.hardware.SensorPrivacyManager;
+import android.hardware.camera2.CameraManager;
 import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.hardware.display.ColorDisplayManager;
@@ -553,4 +554,10 @@
     static SafetyCenterManager provideSafetyCenterManager(Context context) {
         return context.getSystemService(SafetyCenterManager.class);
     }
+
+    @Provides
+    @Singleton
+    static CameraManager provideCameraManager(Context context) {
+        return context.getSystemService(CameraManager.class);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index a54f4c2..47a68bbb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -155,7 +155,7 @@
 
     /***************************************/
     // 900 - media
-    public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, false);
+    public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, true);
     public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, false);
     public static final BooleanFlag MEDIA_NEARBY_DEVICES = new BooleanFlag(903, true);
     public static final BooleanFlag MEDIA_MUTE_AWAIT = new BooleanFlag(904, true);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index d71956d..95b3b3f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -263,7 +263,7 @@
                         // already finished (or not started yet), so do nothing.
                         return;
                     }
-                    runner.onAnimationCancelled();
+                    runner.onAnimationCancelled(false /* isKeyguardOccluded */);
                     origFinishCB.onTransitionFinished(null /* wct */, null /* t */);
                 } catch (RemoteException e) {
                     // nothing, we'll just let it finish on its own I guess.
@@ -396,7 +396,7 @@
         }
 
         @Override // Binder interface
-        public void onAnimationCancelled() {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) {
             mKeyguardViewMediator.cancelKeyguardExitAnimation();
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index be1725b..340cde1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -843,7 +843,6 @@
 
                 @Override
                 public void onLaunchAnimationCancelled() {
-                    setOccluded(true /* occluded */, false /* animate */);
                     Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: "
                             + mOccluded);
                 }
@@ -911,12 +910,12 @@
                 private final Matrix mUnoccludeMatrix = new Matrix();
 
                 @Override
-                public void onAnimationCancelled() {
+                public void onAnimationCancelled(boolean isKeyguardOccluded) {
                     if (mUnoccludeAnimator != null) {
                         mUnoccludeAnimator.cancel();
                     }
 
-                    setOccluded(false /* isOccluded */, false /* animate */);
+                    setOccluded(isKeyguardOccluded /* isOccluded */, false /* animate */);
                     Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: "
                             + mOccluded);
                 }
@@ -3175,9 +3174,9 @@
         }
 
         @Override
-        public void onAnimationCancelled() throws RemoteException {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
             if (mRunner != null) {
-                mRunner.onAnimationCancelled();
+                mRunner.onAnimationCancelled(isKeyguardOccluded);
             }
         }
 
@@ -3218,9 +3217,12 @@
         }
 
         @Override
-        public void onAnimationCancelled() throws RemoteException {
-            super.onAnimationCancelled();
-            Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded);
+        public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
+            super.onAnimationCancelled(isKeyguardOccluded);
+            setOccluded(isKeyguardOccluded /* occluded */, false /* animate */);
+
+            Log.d(TAG, "Occlude animation cancelled by WM. "
+                    + "Setting occluded state to: " + mOccluded);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
index fe1ac80..0f1cdcc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
@@ -25,17 +25,14 @@
 import android.os.PowerManager
 import android.os.SystemClock
 import android.util.Log
-import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.MotionEvent
-import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
-import android.widget.LinearLayout
 import com.android.internal.widget.CachingIconView
 import com.android.settingslib.Utils
 import com.android.systemui.R
@@ -65,12 +62,15 @@
     private val powerManager: PowerManager,
     @LayoutRes private val chipLayoutRes: Int
 ) {
-    /** The window layout parameters we'll use when attaching the view to a window. */
+
+    /**
+     * Window layout params that will be used as a starting point for the [windowLayoutParams] of
+     * all subclasses.
+     */
     @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY
-    private val windowLayoutParams = WindowManager.LayoutParams().apply {
+    internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply {
         width = WindowManager.LayoutParams.WRAP_CONTENT
         height = WindowManager.LayoutParams.WRAP_CONTENT
-        gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
         type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
         flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
         title = WINDOW_TITLE
@@ -78,6 +78,14 @@
         setTrustedOverlay()
     }
 
+    /**
+     * The window layout parameters we'll use when attaching the view to a window.
+     *
+     * Subclasses must override this to provide their specific layout params, and they should use
+     * [commonWindowLayoutParams] as part of their layout params.
+     */
+    internal abstract val windowLayoutParams: WindowManager.LayoutParams
+
     /** The chip view currently being displayed. Null if the chip is not being displayed. */
     private var chipView: ViewGroup? = null
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index a5d763c..f9818f0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -24,6 +24,8 @@
 import android.os.Handler
 import android.os.PowerManager
 import android.util.Log
+import android.view.Gravity
+import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
@@ -36,6 +38,7 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.gesture.TapGestureDetector
+import com.android.systemui.util.animation.AnimationUtil.Companion.frames
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
 import javax.inject.Inject
@@ -69,6 +72,11 @@
     powerManager,
     R.layout.media_ttt_chip_receiver
 ) {
+    override val windowLayoutParams = commonWindowLayoutParams.apply {
+        height = getWindowHeight()
+        gravity = Gravity.BOTTOM.or(Gravity.CENTER_HORIZONTAL)
+    }
+
     private val commandQueueCallbacks = object : CommandQueue.Callbacks {
         override fun updateMediaTapToTransferReceiverDisplay(
             @StatusBarManager.MediaTransferReceiverState displayState: Int,
@@ -131,6 +139,19 @@
         )
     }
 
+    override fun animateChipIn(chipView: ViewGroup) {
+        val appIconView = chipView.requireViewById<View>(R.id.app_icon)
+        appIconView.animate()
+                .translationYBy(-1 * getTranslationAmount().toFloat())
+                .setDuration(30.frames)
+                .start()
+        appIconView.animate()
+                .alpha(1f)
+                .setDuration(5.frames)
+                .start()
+
+    }
+
     override fun getIconSize(isAppIcon: Boolean): Int? =
         context.resources.getDimensionPixelSize(
             if (isAppIcon) {
@@ -139,6 +160,17 @@
                 R.dimen.media_ttt_generic_icon_size_receiver
             }
         )
+
+    private fun getWindowHeight(): Int {
+        return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver) +
+                // Make the window large enough to accommodate the animation amount
+                getTranslationAmount()
+    }
+
+    /** Returns the amount that the chip will be translated by in its intro animation. */
+    private fun getTranslationAmount(): Int {
+        return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation)
+    }
 }
 
 data class ChipReceiverInfo(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 943604c..797a770 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -21,6 +21,7 @@
 import android.media.MediaRoute2Info
 import android.os.PowerManager
 import android.util.Log
+import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
@@ -69,6 +70,10 @@
     powerManager,
     R.layout.media_ttt_chip
 ) {
+    override val windowLayoutParams = commonWindowLayoutParams.apply {
+        gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
+    }
+
     private var currentlyDisplayedChipState: ChipStateSender? = null
 
     private val commandQueueCallbacks = object : CommandQueue.Callbacks {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index fbdabc7..a1c66b3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -396,7 +396,6 @@
     }
 
     void saveTilesToSettings(List<String> tileSpecs) {
-        if (tileSpecs.contains("work")) Log.wtfStack(TAG, "Saving work tile");
         mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs),
                 null /* tag */, false /* default */, mCurrentUser,
                 true /* overrideable by restore */);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 5b6e5ce..c213f19 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -142,7 +142,7 @@
                 }
 
                 @Override
-                public void onAnimationCancelled() {
+                public void onAnimationCancelled(boolean isKeyguardOccluded) {
                 }
             };
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
index 5df593b..558bcac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
@@ -61,7 +61,7 @@
     private val systemClock: SystemClock,
     private val uiEventLogger: UiEventLogger
 ) {
-    private var pluggedIn: Boolean? = null
+    private var pluggedIn: Boolean = false
     private val rippleEnabled: Boolean = featureFlags.isEnabled(Flags.CHARGING_RIPPLE) &&
             !SystemProperties.getBoolean("persist.debug.suppress-charging-ripple", false)
     private var normalizedPortPosX: Float = context.resources.getFloat(
@@ -99,15 +99,17 @@
                 nowPluggedIn: Boolean,
                 charging: Boolean
             ) {
-                // Suppresses the ripple when the state change comes from wireless charging.
-                if (batteryController.isPluggedInWireless) {
+                // Suppresses the ripple when the state change comes from wireless charging or
+                // its dock.
+                if (batteryController.isPluggedInWireless ||
+                        batteryController.isChargingSourceDock) {
                     return
                 }
-                val wasPluggedIn = pluggedIn
-                pluggedIn = nowPluggedIn
-                if ((wasPluggedIn == null || !wasPluggedIn) && nowPluggedIn) {
+
+                if (!pluggedIn && nowPluggedIn) {
                     startRippleWithDebounce()
                 }
+                pluggedIn = nowPluggedIn
             }
         }
         batteryController.addCallback(batteryStateChangeCallback)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt
index 2397005..52dcf02 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt
@@ -17,18 +17,32 @@
 package com.android.systemui.statusbar.notification
 
 import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
 import com.android.systemui.log.LogLevel.DEBUG
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 /** Logger for [NotificationEntryManager]. */
 class NotificationEntryManagerLogger @Inject constructor(
+    notifPipelineFlags: NotifPipelineFlags,
     @NotificationLog private val buffer: LogBuffer
 ) {
+    private val devLoggingEnabled by lazy { notifPipelineFlags.isDevLoggingEnabled() }
+
+    private inline fun devLog(
+        level: LogLevel,
+        initializer: LogMessage.() -> Unit,
+        noinline printer: LogMessage.() -> String
+    ) {
+        if (Compile.IS_DEBUG && devLoggingEnabled) buffer.log(TAG, level, initializer, printer)
+    }
+
     fun logNotifAdded(key: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
         }, {
             "NOTIF ADDED $str1"
@@ -36,7 +50,7 @@
     }
 
     fun logNotifUpdated(key: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
         }, {
             "NOTIF UPDATED $str1"
@@ -44,7 +58,7 @@
     }
 
     fun logInflationAborted(key: String, status: String, reason: String) {
-        buffer.log(TAG, DEBUG, {
+        devLog(DEBUG, {
             str1 = key
             str2 = status
             str3 = reason
@@ -54,7 +68,7 @@
     }
 
     fun logNotifInflated(key: String, isNew: Boolean) {
-        buffer.log(TAG, DEBUG, {
+        devLog(DEBUG, {
             str1 = key
             bool1 = isNew
         }, {
@@ -63,7 +77,7 @@
     }
 
     fun logRemovalIntercepted(key: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
         }, {
             "NOTIF REMOVE INTERCEPTED for $str1"
@@ -71,7 +85,7 @@
     }
 
     fun logLifetimeExtended(key: String, extenderName: String, status: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
             str2 = extenderName
             str3 = status
@@ -81,7 +95,7 @@
     }
 
     fun logNotifRemoved(key: String, removedByUser: Boolean) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
             bool1 = removedByUser
         }, {
@@ -90,7 +104,7 @@
     }
 
     fun logFilterAndSort(reason: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = reason
         }, {
             "FILTER AND SORT reason=$str1"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index aedbd1b..0a16fb6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -175,6 +175,10 @@
     public boolean mRemoteEditImeAnimatingAway;
     public boolean mRemoteEditImeVisible;
     private boolean mExpandAnimationRunning;
+    /**
+     * Flag to determine if the entry is blockable by DnD filters
+     */
+    private boolean mBlockable;
 
     /**
      * @param sbn the StatusBarNotification from system server
@@ -253,6 +257,7 @@
         }
 
         mRanking = ranking.withAudiblyAlertedInfo(mRanking);
+        updateIsBlockable();
     }
 
     /*
@@ -781,15 +786,20 @@
      * or is not in an allowList).
      */
     public boolean isBlockable() {
+        return mBlockable;
+    }
+
+    private void updateIsBlockable() {
         if (getChannel() == null) {
-            return false;
+            mBlockable = false;
+            return;
         }
         if (getChannel().isImportanceLockedByCriticalDeviceFunction()
                 && !getChannel().isBlockable()) {
-            return false;
+            mBlockable = false;
+            return;
         }
-
-        return true;
+        mBlockable = true;
     }
 
     private boolean shouldSuppressVisualEffect(int effect) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
index 3501b44..38e3d49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
@@ -19,20 +19,24 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 class NodeSpecBuilderLogger @Inject constructor(
+    notifPipelineFlags: NotifPipelineFlags,
     @NotificationLog private val buffer: LogBuffer
 ) {
+    private val devLoggingEnabled by lazy { notifPipelineFlags.isDevLoggingEnabled() }
+
     fun logBuildNodeSpec(
         oldSections: Set<NotifSection?>,
         newHeaders: Map<NotifSection?, NodeController?>,
         newCounts: Map<NotifSection?, Int>,
         newSectionOrder: List<NotifSection?>
     ) {
-        if (!Compile.IS_DEBUG)
+        if (!(Compile.IS_DEBUG && devLoggingEnabled))
             return
 
         buffer.log(TAG, LogLevel.DEBUG, {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index d25bbbd..2c22bc6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -427,6 +427,12 @@
 
     void onHintFinished();
 
+    void onCameraHintStarted();
+
+    void onVoiceAssistHintStarted();
+
+    void onPhoneHintStarted();
+
     void onTrackingStopped(boolean expand);
 
     // TODO: Figure out way to remove these.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 38c37f0..9060d5f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -388,7 +388,8 @@
                 if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
                     mStatusBarKeyguardViewManager.reset(true /* hide */);
                 }
-                mNotificationPanelViewController.launchCamera(source);
+                mNotificationPanelViewController.launchCamera(
+                        mCentralSurfaces.isDeviceInteractive() /* animate */, source);
                 mCentralSurfaces.updateScrimController();
             } else {
                 // We need to defer the camera launch until the screen comes on, since otherwise
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 4f99dea..5181af7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1285,6 +1285,8 @@
             backdrop.setScaleY(scale);
         });
 
+        mNotificationPanelViewController.setUserSetupComplete(mUserSetup);
+
         // Set up the quick settings tile panel
         final View container = mNotificationShadeWindowView.findViewById(R.id.qs_frame);
         if (container != null) {
@@ -3022,7 +3024,8 @@
 
     @Override
     public boolean isInLaunchTransition() {
-        return mNotificationPanelViewController.isLaunchTransitionFinished();
+        return mNotificationPanelViewController.isLaunchTransitionRunning()
+                || mNotificationPanelViewController.isLaunchTransitionFinished();
     }
 
     /**
@@ -3054,7 +3057,11 @@
             mCommandQueue.appTransitionStarting(mDisplayId, SystemClock.uptimeMillis(),
                     LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION, true);
         };
-        hideRunnable.run();
+        if (mNotificationPanelViewController.isLaunchTransitionRunning()) {
+            mNotificationPanelViewController.setLaunchTransitionEndRunnable(hideRunnable);
+        } else {
+            hideRunnable.run();
+        }
     }
 
     private void cancelAfterLaunchTransitionRunnables() {
@@ -3063,6 +3070,7 @@
         }
         mLaunchTransitionEndRunnable = null;
         mLaunchTransitionCancelRunnable = null;
+        mNotificationPanelViewController.setLaunchTransitionEndRunnable(null);
     }
 
     /**
@@ -3491,6 +3499,24 @@
     }
 
     @Override
+    public void onCameraHintStarted() {
+        mFalsingCollector.onCameraHintStarted();
+        mKeyguardIndicationController.showTransientIndication(R.string.camera_hint);
+    }
+
+    @Override
+    public void onVoiceAssistHintStarted() {
+        mFalsingCollector.onLeftAffordanceHintStarted();
+        mKeyguardIndicationController.showTransientIndication(R.string.voice_hint);
+    }
+
+    @Override
+    public void onPhoneHintStarted() {
+        mFalsingCollector.onLeftAffordanceHintStarted();
+        mKeyguardIndicationController.showTransientIndication(R.string.phone_hint);
+    }
+
+    @Override
     public void onTrackingStopped(boolean expand) {
     }
 
@@ -3674,7 +3700,8 @@
             mWakeUpCoordinator.setFullyAwake(true);
             mWakeUpCoordinator.setWakingUp(false);
             if (mLaunchCameraWhenFinishedWaking) {
-                mNotificationPanelViewController.launchCamera(mLastCameraLaunchSource);
+                mNotificationPanelViewController.launchCamera(
+                        false /* animate */, mLastCameraLaunchSource);
                 mLaunchCameraWhenFinishedWaking = false;
             }
             if (mLaunchEmergencyActionWhenFinishedWaking) {
@@ -4316,6 +4343,9 @@
                 if (!mUserSetup) {
                     animateCollapseQuickSettings();
                 }
+                if (mNotificationPanelViewController != null) {
+                    mNotificationPanelViewController.setUserSetupComplete(mUserSetup);
+                }
                 updateQsExpansionEnabled();
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
new file mode 100644
index 0000000..2922b4c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.R;
+import com.android.systemui.animation.Interpolators;
+import com.android.systemui.classifier.Classifier;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.wm.shell.animation.FlingAnimationUtils;
+
+/**
+ * A touch handler of the keyguard which is responsible for launching phone and camera affordances.
+ */
+public class KeyguardAffordanceHelper {
+
+    public static final long HINT_PHASE1_DURATION = 200;
+    private static final long HINT_PHASE2_DURATION = 350;
+    private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f;
+    private static final int HINT_CIRCLE_OPEN_DURATION = 500;
+
+    private final Context mContext;
+    private final Callback mCallback;
+
+    private FlingAnimationUtils mFlingAnimationUtils;
+    private VelocityTracker mVelocityTracker;
+    private boolean mSwipingInProgress;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private float mTranslation;
+    private float mTranslationOnDown;
+    private int mTouchSlop;
+    private int mMinTranslationAmount;
+    private int mMinFlingVelocity;
+    private int mHintGrowAmount;
+    private KeyguardAffordanceView mLeftIcon;
+    private KeyguardAffordanceView mRightIcon;
+    private Animator mSwipeAnimator;
+    private final FalsingManager mFalsingManager;
+    private int mMinBackgroundRadius;
+    private boolean mMotionCancelled;
+    private int mTouchTargetSize;
+    private View mTargetedView;
+    private boolean mTouchSlopExeeded;
+    private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mSwipeAnimator = null;
+            mSwipingInProgress = false;
+            mTargetedView = null;
+        }
+    };
+    private Runnable mAnimationEndRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mCallback.onAnimationToSideEnded();
+        }
+    };
+
+    KeyguardAffordanceHelper(Callback callback, Context context, FalsingManager falsingManager) {
+        mContext = context;
+        mCallback = callback;
+        initIcons();
+        updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false);
+        updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false);
+        mFalsingManager = falsingManager;
+        initDimens();
+    }
+
+    private void initDimens() {
+        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+        mTouchSlop = configuration.getScaledPagingTouchSlop();
+        mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMinTranslationAmount = mContext.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_min_swipe_amount);
+        mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_affordance_min_background_radius);
+        mTouchTargetSize = mContext.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_affordance_touch_target_size);
+        mHintGrowAmount =
+                mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
+        mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(),
+                0.4f);
+    }
+
+    private void initIcons() {
+        mLeftIcon = mCallback.getLeftIcon();
+        mRightIcon = mCallback.getRightIcon();
+        updatePreviews();
+    }
+
+    public void updatePreviews() {
+        mLeftIcon.setPreviewView(mCallback.getLeftPreview());
+        mRightIcon.setPreviewView(mCallback.getRightPreview());
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        int action = event.getActionMasked();
+        if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) {
+            return false;
+        }
+        final float y = event.getY();
+        final float x = event.getX();
+
+        boolean isUp = false;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                View targetView = getIconAtPosition(x, y);
+                if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) {
+                    mMotionCancelled = true;
+                    return false;
+                }
+                if (mTargetedView != null) {
+                    cancelAnimation();
+                } else {
+                    mTouchSlopExeeded = false;
+                }
+                startSwiping(targetView);
+                mInitialTouchX = x;
+                mInitialTouchY = y;
+                mTranslationOnDown = mTranslation;
+                initVelocityTracker();
+                trackMovement(event);
+                mMotionCancelled = false;
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                mMotionCancelled = true;
+                endMotion(true /* forceSnapBack */, x, y);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                trackMovement(event);
+                float xDist = x - mInitialTouchX;
+                float yDist = y - mInitialTouchY;
+                float distance = (float) Math.hypot(xDist, yDist);
+                if (!mTouchSlopExeeded && distance > mTouchSlop) {
+                    mTouchSlopExeeded = true;
+                }
+                if (mSwipingInProgress) {
+                    if (mTargetedView == mRightIcon) {
+                        distance = mTranslationOnDown - distance;
+                        distance = Math.min(0, distance);
+                    } else {
+                        distance = mTranslationOnDown + distance;
+                        distance = Math.max(0, distance);
+                    }
+                    setTranslation(distance, false /* isReset */, false /* animateReset */);
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+                isUp = true;
+            case MotionEvent.ACTION_CANCEL:
+                boolean hintOnTheRight = mTargetedView == mRightIcon;
+                trackMovement(event);
+                endMotion(!isUp, x, y);
+                if (!mTouchSlopExeeded && isUp) {
+                    mCallback.onIconClicked(hintOnTheRight);
+                }
+                break;
+        }
+        return true;
+    }
+
+    private void startSwiping(View targetView) {
+        mCallback.onSwipingStarted(targetView == mRightIcon);
+        mSwipingInProgress = true;
+        mTargetedView = targetView;
+    }
+
+    private View getIconAtPosition(float x, float y) {
+        if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) {
+            return mLeftIcon;
+        }
+        if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) {
+            return mRightIcon;
+        }
+        return null;
+    }
+
+    public boolean isOnAffordanceIcon(float x, float y) {
+        return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y);
+    }
+
+    private boolean isOnIcon(View icon, float x, float y) {
+        float iconX = icon.getX() + icon.getWidth() / 2.0f;
+        float iconY = icon.getY() + icon.getHeight() / 2.0f;
+        double distance = Math.hypot(x - iconX, y - iconY);
+        return distance <= mTouchTargetSize / 2;
+    }
+
+    private void endMotion(boolean forceSnapBack, float lastX, float lastY) {
+        if (mSwipingInProgress) {
+            flingWithCurrentVelocity(forceSnapBack, lastX, lastY);
+        } else {
+            mTargetedView = null;
+        }
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private boolean rightSwipePossible() {
+        return mRightIcon.getVisibility() == View.VISIBLE;
+    }
+
+    private boolean leftSwipePossible() {
+        return mLeftIcon.getVisibility() == View.VISIBLE;
+    }
+
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    public void startHintAnimation(boolean right,
+            Runnable onFinishedListener) {
+        cancelAnimation();
+        startHintAnimationPhase1(right, onFinishedListener);
+    }
+
+    private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) {
+        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+        ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount);
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCancelled) {
+                    mSwipeAnimator = null;
+                    mTargetedView = null;
+                    onFinishedListener.run();
+                } else {
+                    startUnlockHintAnimationPhase2(right, onFinishedListener);
+                }
+            }
+        });
+        animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        animator.setDuration(HINT_PHASE1_DURATION);
+        animator.start();
+        mSwipeAnimator = animator;
+        mTargetedView = targetView;
+    }
+
+    /**
+     * Phase 2: Move back.
+     */
+    private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) {
+        ValueAnimator animator = getAnimatorToRadius(right, 0);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mSwipeAnimator = null;
+                mTargetedView = null;
+                onFinishedListener.run();
+            }
+        });
+        animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+        animator.setDuration(HINT_PHASE2_DURATION);
+        animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
+        animator.start();
+        mSwipeAnimator = animator;
+    }
+
+    private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
+        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+        ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float newRadius = (float) animation.getAnimatedValue();
+                targetView.setCircleRadiusWithoutAnimation(newRadius);
+                float translation = getTranslationFromRadius(newRadius);
+                mTranslation = right ? -translation : translation;
+                updateIconsFromTranslation(targetView);
+            }
+        });
+        return animator;
+    }
+
+    private void cancelAnimation() {
+        if (mSwipeAnimator != null) {
+            mSwipeAnimator.cancel();
+        }
+    }
+
+    private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) {
+        float vel = getCurrentVelocity(lastX, lastY);
+
+        // We snap back if the current translation is not far enough
+        boolean snapBack = false;
+        if (mCallback.needsAntiFalsing()) {
+            snapBack = snapBack || mFalsingManager.isFalseTouch(
+                    mTargetedView == mRightIcon
+                            ? Classifier.RIGHT_AFFORDANCE : Classifier.LEFT_AFFORDANCE);
+        }
+        snapBack = snapBack || isBelowFalsingThreshold();
+
+        // or if the velocity is in the opposite direction.
+        boolean velIsInWrongDirection = vel * mTranslation < 0;
+        snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection;
+        vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
+        fling(vel, snapBack || forceSnapBack, mTranslation < 0);
+    }
+
+    private boolean isBelowFalsingThreshold() {
+        return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
+    }
+
+    private int getMinTranslationAmount() {
+        float factor = mCallback.getAffordanceFalsingFactor();
+        return (int) (mMinTranslationAmount * factor);
+    }
+
+    private void fling(float vel, final boolean snapBack, boolean right) {
+        float target = right ? -mCallback.getMaxTranslationDistance()
+                : mCallback.getMaxTranslationDistance();
+        target = snapBack ? 0 : target;
+
+        ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
+        mFlingAnimationUtils.apply(animator, mTranslation, target, vel);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mTranslation = (float) animation.getAnimatedValue();
+            }
+        });
+        animator.addListener(mFlingEndListener);
+        if (!snapBack) {
+            startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right);
+            mCallback.onAnimationToSideStarted(right, mTranslation, vel);
+        } else {
+            reset(true);
+        }
+        animator.start();
+        mSwipeAnimator = animator;
+        if (snapBack) {
+            mCallback.onSwipingAborted();
+        }
+    }
+
+    private void startFinishingCircleAnimation(float velocity, Runnable animationEndRunnable,
+            boolean right) {
+        KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+        targetView.finishAnimation(velocity, animationEndRunnable);
+    }
+
+    private void setTranslation(float translation, boolean isReset, boolean animateReset) {
+        translation = rightSwipePossible() ? translation : Math.max(0, translation);
+        translation = leftSwipePossible() ? translation : Math.min(0, translation);
+        float absTranslation = Math.abs(translation);
+        if (translation != mTranslation || isReset) {
+            KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon;
+            KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon;
+            float alpha = absTranslation / getMinTranslationAmount();
+
+            // We interpolate the alpha of the other icons to 0
+            float fadeOutAlpha = 1.0f - alpha;
+            fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f);
+
+            boolean animateIcons = isReset && animateReset;
+            boolean forceNoCircleAnimation = isReset && !animateReset;
+            float radius = getRadiusFromTranslation(absTranslation);
+            boolean slowAnimation = isReset && isBelowFalsingThreshold();
+            if (!isReset) {
+                updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(),
+                        false, false, false, false);
+            } else {
+                updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(),
+                        animateIcons, slowAnimation, true /* isReset */, forceNoCircleAnimation);
+            }
+            updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(),
+                    animateIcons, slowAnimation, isReset, forceNoCircleAnimation);
+
+            mTranslation = translation;
+        }
+    }
+
+    private void updateIconsFromTranslation(KeyguardAffordanceView targetView) {
+        float absTranslation = Math.abs(mTranslation);
+        float alpha = absTranslation / getMinTranslationAmount();
+
+        // We interpolate the alpha of the other icons to 0
+        float fadeOutAlpha =  1.0f - alpha;
+        fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
+
+        // We interpolate the alpha of the targetView to 1
+        KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon;
+        updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false);
+        updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false);
+    }
+
+    private float getTranslationFromRadius(float circleSize) {
+        float translation = (circleSize - mMinBackgroundRadius)
+                / BACKGROUND_RADIUS_SCALE_FACTOR;
+        return translation > 0.0f ? translation + mTouchSlop : 0.0f;
+    }
+
+    private float getRadiusFromTranslation(float translation) {
+        if (translation <= mTouchSlop) {
+            return 0.0f;
+        }
+        return (translation - mTouchSlop)  * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius;
+    }
+
+    public void animateHideLeftRightIcon() {
+        cancelAnimation();
+        updateIcon(mRightIcon, 0f, 0f, true, false, false, false);
+        updateIcon(mLeftIcon, 0f, 0f, true, false, false, false);
+    }
+
+    private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha,
+                            boolean animate, boolean slowRadiusAnimation, boolean force,
+                            boolean forceNoCircleAnimation) {
+        if (view.getVisibility() != View.VISIBLE && !force) {
+            return;
+        }
+        if (forceNoCircleAnimation) {
+            view.setCircleRadiusWithoutAnimation(circleRadius);
+        } else {
+            view.setCircleRadius(circleRadius, slowRadiusAnimation);
+        }
+        updateIconAlpha(view, alpha, animate);
+    }
+
+    private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) {
+        float scale = getScale(alpha, view);
+        alpha = Math.min(1.0f, alpha);
+        view.setImageAlpha(alpha, animate);
+        view.setImageScale(scale, animate);
+    }
+
+    private float getScale(float alpha, KeyguardAffordanceView icon) {
+        float scale = alpha / icon.getRestingAlpha() * 0.2f +
+                KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT;
+        return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT);
+    }
+
+    private void trackMovement(MotionEvent event) {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.addMovement(event);
+        }
+    }
+
+    private void initVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+        }
+        mVelocityTracker = VelocityTracker.obtain();
+    }
+
+    private float getCurrentVelocity(float lastX, float lastY) {
+        if (mVelocityTracker == null) {
+            return 0;
+        }
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float aX = mVelocityTracker.getXVelocity();
+        float aY = mVelocityTracker.getYVelocity();
+        float bX = lastX - mInitialTouchX;
+        float bY = lastY - mInitialTouchY;
+        float bLen = (float) Math.hypot(bX, bY);
+        // Project the velocity onto the distance vector: a * b / |b|
+        float projectedVelocity = (aX * bX + aY * bY) / bLen;
+        if (mTargetedView == mRightIcon) {
+            projectedVelocity = -projectedVelocity;
+        }
+        return projectedVelocity;
+    }
+
+    public void onConfigurationChanged() {
+        initDimens();
+        initIcons();
+    }
+
+    public void onRtlPropertiesChanged() {
+        initIcons();
+    }
+
+    public void reset(boolean animate) {
+        cancelAnimation();
+        setTranslation(0.0f, true /* isReset */, animate);
+        mMotionCancelled = true;
+        if (mSwipingInProgress) {
+            mCallback.onSwipingAborted();
+            mSwipingInProgress = false;
+        }
+    }
+
+    public boolean isSwipingInProgress() {
+        return mSwipingInProgress;
+    }
+
+    public void launchAffordance(boolean animate, boolean left) {
+        if (mSwipingInProgress) {
+            // We don't want to mess with the state if the user is actually swiping already.
+            return;
+        }
+        KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon;
+        KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon;
+        startSwiping(targetView);
+
+        // Do not animate the circle expanding if the affordance isn't visible,
+        // otherwise the circle will be meaningless.
+        if (targetView.getVisibility() != View.VISIBLE) {
+            animate = false;
+        }
+
+        if (animate) {
+            fling(0, false, !left);
+            updateIcon(otherView, 0.0f, 0, true, false, true, false);
+        } else {
+            mCallback.onAnimationToSideStarted(!left, mTranslation, 0);
+            mTranslation = left ? mCallback.getMaxTranslationDistance()
+                    : mCallback.getMaxTranslationDistance();
+            updateIcon(otherView, 0.0f, 0.0f, false, false, true, false);
+            targetView.instantFinishAnimation();
+            mFlingEndListener.onAnimationEnd(null);
+            mAnimationEndRunnable.run();
+        }
+    }
+
+    public interface Callback {
+
+        /**
+         * Notifies the callback when an animation to a side page was started.
+         *
+         * @param rightPage Is the page animated to the right page?
+         */
+        void onAnimationToSideStarted(boolean rightPage, float translation, float vel);
+
+        /**
+         * Notifies the callback the animation to a side page has ended.
+         */
+        void onAnimationToSideEnded();
+
+        float getMaxTranslationDistance();
+
+        void onSwipingStarted(boolean rightIcon);
+
+        void onSwipingAborted();
+
+        void onIconClicked(boolean rightIcon);
+
+        KeyguardAffordanceView getLeftIcon();
+
+        KeyguardAffordanceView getRightIcon();
+
+        View getLeftPreview();
+
+        View getRightPreview();
+
+        /**
+         * @return The factor the minimum swipe amount should be multiplied with.
+         */
+        float getAffordanceFalsingFactor();
+
+        boolean needsAntiFalsing();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 61113a0..93b2e41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -16,30 +16,47 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_BUTTON;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_UNLOCK;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_BUTTON;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_UNLOCK;
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE;
 
+import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.service.quickaccesswallet.GetWalletCardsError;
 import android.service.quickaccesswallet.GetWalletCardsResponse;
 import android.service.quickaccesswallet.QuickAccessWalletClient;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -47,35 +64,81 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.settingslib.Utils;
+import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.camera.CameraIntents;
+import com.android.systemui.controls.ControlsServiceInfo;
 import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.controls.management.ControlsListingController;
 import com.android.systemui.controls.ui.ControlsActivity;
 import com.android.systemui.controls.ui.ControlsUiController;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.IntentButtonProvider;
+import com.android.systemui.plugins.IntentButtonProvider.IntentButton;
+import com.android.systemui.plugins.IntentButtonProvider.IntentButton.IconState;
 import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.policy.AccessibilityController;
+import com.android.systemui.statusbar.policy.ExtensionController;
+import com.android.systemui.statusbar.policy.ExtensionController.Extension;
+import com.android.systemui.statusbar.policy.FlashlightController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.PreviewInflater;
+import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory;
+import com.android.systemui.tuner.TunerService;
 import com.android.systemui.wallet.controller.QuickAccessWalletController;
 
+import java.util.List;
+
 /**
  * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status
  * text.
  */
-public class KeyguardBottomAreaView extends FrameLayout {
+public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickListener,
+        KeyguardStateController.Callback,
+        AccessibilityController.AccessibilityStateChangedCallback {
 
-    private static final String TAG = "CentralSurfaces/KeyguardBottomAreaView";
+    final static String TAG = "CentralSurfaces/KeyguardBottomAreaView";
+
+    public static final String CAMERA_LAUNCH_SOURCE_AFFORDANCE = "lockscreen_affordance";
+    public static final String CAMERA_LAUNCH_SOURCE_WIGGLE = "wiggle_gesture";
+    public static final String CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = "power_double_tap";
+    public static final String CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = "lift_to_launch_ml";
+
+    public static final String EXTRA_CAMERA_LAUNCH_SOURCE
+            = "com.android.systemui.camera_launch_source";
+
+    private static final String LEFT_BUTTON_PLUGIN
+            = "com.android.systemui.action.PLUGIN_LOCKSCREEN_LEFT_BUTTON";
+    private static final String RIGHT_BUTTON_PLUGIN
+            = "com.android.systemui.action.PLUGIN_LOCKSCREEN_RIGHT_BUTTON";
+
+    private static final Intent PHONE_INTENT = new Intent(Intent.ACTION_DIAL);
+    private static final int DOZE_ANIMATION_STAGGER_DELAY = 48;
     private static final int DOZE_ANIMATION_ELEMENT_DURATION = 250;
 
+    // TODO(b/179494051): May no longer be needed
+    private final boolean mShowLeftAffordance;
+    private final boolean mShowCameraAffordance;
+
+    private KeyguardAffordanceView mRightAffordanceView;
+    private KeyguardAffordanceView mLeftAffordanceView;
+
     private ImageView mWalletButton;
     private ImageView mQRCodeScannerButton;
     private ImageView mControlsButton;
     private boolean mHasCard = false;
-    private final WalletCardRetriever mCardRetriever = new WalletCardRetriever();
+    private WalletCardRetriever mCardRetriever = new WalletCardRetriever();
     private QuickAccessWalletController mQuickAccessWalletController;
     private QRCodeScannerController mQRCodeScannerController;
     private ControlsComponent mControlsComponent;
@@ -85,42 +148,54 @@
     private ViewGroup mIndicationArea;
     private TextView mIndicationText;
     private TextView mIndicationTextBottom;
+    private ViewGroup mPreviewContainer;
     private ViewGroup mOverlayContainer;
 
+    private View mLeftPreview;
+    private View mCameraPreview;
+
     private ActivityStarter mActivityStarter;
     private KeyguardStateController mKeyguardStateController;
+    private FlashlightController mFlashlightController;
+    private PreviewInflater mPreviewInflater;
+    private AccessibilityController mAccessibilityController;
     private CentralSurfaces mCentralSurfaces;
+    private KeyguardAffordanceHelper mAffordanceHelper;
     private FalsingManager mFalsingManager;
+    private boolean mUserSetupComplete;
 
+    private boolean mLeftIsVoiceAssist;
+    private Drawable mLeftAssistIcon;
+
+    private IntentButton mRightButton = new DefaultRightButton();
+    private Extension<IntentButton> mRightExtension;
+    private String mRightButtonStr;
+    private IntentButton mLeftButton = new DefaultLeftButton();
+    private Extension<IntentButton> mLeftExtension;
+    private String mLeftButtonStr;
     private boolean mDozing;
     private int mIndicationBottomMargin;
     private int mIndicationPadding;
     private float mDarkAmount;
     private int mBurnInXOffset;
     private int mBurnInYOffset;
+    private ActivityIntentHelper mActivityIntentHelper;
+    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
 
-    private final ControlsListingController.ControlsListingCallback mListingCallback =
-            serviceInfos -> post(() -> {
-                boolean available = !serviceInfos.isEmpty();
+    private ControlsListingController.ControlsListingCallback mListingCallback =
+            new ControlsListingController.ControlsListingCallback() {
+                public void onServicesUpdated(List<ControlsServiceInfo> serviceInfos) {
+                    post(() -> {
+                        boolean available = !serviceInfos.isEmpty();
 
-                if (available != mControlServicesAvailable) {
-                    mControlServicesAvailable = available;
-                    updateControlsVisibility();
-                    updateAffordanceColors();
+                        if (available != mControlServicesAvailable) {
+                            mControlServicesAvailable = available;
+                            updateControlsVisibility();
+                            updateAffordanceColors();
+                        }
+                    });
                 }
-            });
-
-    private final KeyguardStateController.Callback mKeyguardStateCallback =
-            new KeyguardStateController.Callback() {
-        @Override
-        public void onKeyguardShowingChanged() {
-            if (mKeyguardStateController.isShowing()) {
-                if (mQuickAccessWalletController != null) {
-                    mQuickAccessWalletController.queryWalletCards(mCardRetriever);
-                }
-            }
-        }
-    };
+            };
 
     public KeyguardBottomAreaView(Context context) {
         this(context, null);
@@ -137,8 +212,43 @@
     public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
+        mShowLeftAffordance = getResources().getBoolean(R.bool.config_keyguardShowLeftAffordance);
+        mShowCameraAffordance = getResources()
+                .getBoolean(R.bool.config_keyguardShowCameraAffordance);
     }
 
+    private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            String label = null;
+            if (host == mRightAffordanceView) {
+                label = getResources().getString(R.string.camera_label);
+            } else if (host == mLeftAffordanceView) {
+                if (mLeftIsVoiceAssist) {
+                    label = getResources().getString(R.string.voice_assist_label);
+                } else {
+                    label = getResources().getString(R.string.phone_label);
+                }
+            }
+            info.addAction(new AccessibilityAction(ACTION_CLICK, label));
+        }
+
+        @Override
+        public boolean performAccessibilityAction(View host, int action, Bundle args) {
+            if (action == ACTION_CLICK) {
+                if (host == mRightAffordanceView) {
+                    launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE);
+                    return true;
+                } else if (host == mLeftAffordanceView) {
+                    launchLeftAffordance();
+                    return true;
+                }
+            }
+            return super.performAccessibilityAction(host, action, args);
+        }
+    };
+
     public void initFrom(KeyguardBottomAreaView oldBottomArea) {
         setCentralSurfaces(oldBottomArea.mCentralSurfaces);
 
@@ -169,7 +279,11 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        mPreviewInflater = new PreviewInflater(mContext, new LockPatternUtils(mContext),
+                new ActivityIntentHelper(mContext));
         mOverlayContainer = findViewById(R.id.overlay_container);
+        mRightAffordanceView = findViewById(R.id.camera_button);
+        mLeftAffordanceView = findViewById(R.id.left_button);
         mWalletButton = findViewById(R.id.wallet_button);
         mQRCodeScannerButton = findViewById(R.id.qr_code_scanner_button);
         mControlsButton = findViewById(R.id.controls_button);
@@ -181,11 +295,18 @@
                 R.dimen.keyguard_indication_margin_bottom);
         mBurnInYOffset = getResources().getDimensionPixelSize(
                 R.dimen.default_burn_in_prevention_offset);
+        updateCameraVisibility();
         mKeyguardStateController = Dependency.get(KeyguardStateController.class);
-        mKeyguardStateController.addCallback(mKeyguardStateCallback);
+        mKeyguardStateController.addCallback(this);
         setClipChildren(false);
         setClipToPadding(false);
+        mRightAffordanceView.setOnClickListener(this);
+        mLeftAffordanceView.setOnClickListener(this);
+        initAccessibility();
         mActivityStarter = Dependency.get(ActivityStarter.class);
+        mFlashlightController = Dependency.get(FlashlightController.class);
+        mAccessibilityController = Dependency.get(AccessibilityController.class);
+        mActivityIntentHelper = new ActivityIntentHelper(getContext());
 
         mIndicationPadding = getResources().getDimensionPixelSize(
                 R.dimen.keyguard_indication_area_padding);
@@ -194,18 +315,51 @@
         updateControlsVisibility();
     }
 
+    /**
+     * Set the container where the previews are rendered.
+     */
+    public void setPreviewContainer(ViewGroup previewContainer) {
+        mPreviewContainer = previewContainer;
+        inflateCameraPreview();
+        updateLeftAffordance();
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
+        mAccessibilityController.addStateChangedCallback(this);
+        mRightExtension = Dependency.get(ExtensionController.class).newExtension(IntentButton.class)
+                .withPlugin(IntentButtonProvider.class, RIGHT_BUTTON_PLUGIN,
+                        p -> p.getIntentButton())
+                .withTunerFactory(new LockButtonFactory(mContext, LOCKSCREEN_RIGHT_BUTTON))
+                .withDefault(() -> new DefaultRightButton())
+                .withCallback(button -> setRightButton(button))
+                .build();
+        mLeftExtension = Dependency.get(ExtensionController.class).newExtension(IntentButton.class)
+                .withPlugin(IntentButtonProvider.class, LEFT_BUTTON_PLUGIN,
+                        p -> p.getIntentButton())
+                .withTunerFactory(new LockButtonFactory(mContext, LOCKSCREEN_LEFT_BUTTON))
+                .withDefault(() -> new DefaultLeftButton())
+                .withCallback(button -> setLeftButton(button))
+                .build();
         final IntentFilter filter = new IntentFilter();
         filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
-        mKeyguardStateController.addCallback(mKeyguardStateCallback);
+        getContext().registerReceiverAsUser(mDevicePolicyReceiver,
+                UserHandle.ALL, filter, null, null);
+        mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
+        mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback);
+        mKeyguardStateController.addCallback(this);
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mKeyguardStateController.removeCallback(mKeyguardStateCallback);
+        mKeyguardStateController.removeCallback(this);
+        mAccessibilityController.removeStateChangedCallback(this);
+        mRightExtension.destroy();
+        mLeftExtension.destroy();
+        getContext().unregisterReceiver(mDevicePolicyReceiver);
+        mKeyguardUpdateMonitor.removeCallback(mUpdateMonitorCallback);
 
         if (mQuickAccessWalletController != null) {
             mQuickAccessWalletController.unregisterWalletChangeObservers(
@@ -224,6 +378,11 @@
         }
     }
 
+    private void initAccessibility() {
+        mLeftAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate);
+        mRightAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate);
+    }
+
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
@@ -245,7 +404,19 @@
                 getResources().getDimensionPixelSize(
                         com.android.internal.R.dimen.text_size_small_material));
 
-        ViewGroup.LayoutParams lp = mWalletButton.getLayoutParams();
+        ViewGroup.LayoutParams lp = mRightAffordanceView.getLayoutParams();
+        lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width);
+        lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height);
+        mRightAffordanceView.setLayoutParams(lp);
+        updateRightAffordanceIcon();
+
+        lp = mLeftAffordanceView.getLayoutParams();
+        lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width);
+        lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height);
+        mLeftAffordanceView.setLayoutParams(lp);
+        updateLeftAffordanceIcon();
+
+        lp = mWalletButton.getLayoutParams();
         lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width);
         lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height);
         mWalletButton.setLayoutParams(lp);
@@ -268,8 +439,74 @@
         updateAffordanceColors();
     }
 
+    private void updateRightAffordanceIcon() {
+        IconState state = mRightButton.getIcon();
+        mRightAffordanceView.setVisibility(!mDozing && state.isVisible ? View.VISIBLE : View.GONE);
+        if (state.drawable != mRightAffordanceView.getDrawable()
+                || state.tint != mRightAffordanceView.shouldTint()) {
+            mRightAffordanceView.setImageDrawable(state.drawable, state.tint);
+        }
+        mRightAffordanceView.setContentDescription(state.contentDescription);
+    }
+
     public void setCentralSurfaces(CentralSurfaces centralSurfaces) {
         mCentralSurfaces = centralSurfaces;
+        updateCameraVisibility(); // in case onFinishInflate() was called too early
+    }
+
+    public void setAffordanceHelper(KeyguardAffordanceHelper affordanceHelper) {
+        mAffordanceHelper = affordanceHelper;
+    }
+
+    public void setUserSetupComplete(boolean userSetupComplete) {
+        mUserSetupComplete = userSetupComplete;
+        updateCameraVisibility();
+        updateLeftAffordanceIcon();
+    }
+
+    private Intent getCameraIntent() {
+        return mRightButton.getIntent();
+    }
+
+    /**
+     * Resolves the intent to launch the camera application.
+     */
+    public ResolveInfo resolveCameraIntent() {
+        return mContext.getPackageManager().resolveActivityAsUser(getCameraIntent(),
+                PackageManager.MATCH_DEFAULT_ONLY,
+                KeyguardUpdateMonitor.getCurrentUser());
+    }
+
+    private void updateCameraVisibility() {
+        if (mRightAffordanceView == null) {
+            // Things are not set up yet; reply hazy, ask again later
+            return;
+        }
+        mRightAffordanceView.setVisibility(!mDozing && mShowCameraAffordance
+                && mRightButton.getIcon().isVisible ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * Set an alternate icon for the left assist affordance (replace the mic icon)
+     */
+    public void setLeftAssistIcon(Drawable drawable) {
+        mLeftAssistIcon = drawable;
+        updateLeftAffordanceIcon();
+    }
+
+    private void updateLeftAffordanceIcon() {
+        if (!mShowLeftAffordance || mDozing) {
+            mLeftAffordanceView.setVisibility(GONE);
+            return;
+        }
+
+        IconState state = mLeftButton.getIcon();
+        mLeftAffordanceView.setVisibility(state.isVisible ? View.VISIBLE : View.GONE);
+        if (state.drawable != mLeftAffordanceView.getDrawable()
+                || state.tint != mLeftAffordanceView.shouldTint()) {
+            mLeftAffordanceView.setImageDrawable(state.drawable, state.tint);
+        }
+        mLeftAffordanceView.setContentDescription(state.contentDescription);
     }
 
     private void updateWalletVisibility() {
@@ -315,6 +552,73 @@
         }
     }
 
+    public boolean isLeftVoiceAssist() {
+        return mLeftIsVoiceAssist;
+    }
+
+    private boolean isPhoneVisible() {
+        PackageManager pm = mContext.getPackageManager();
+        return pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+                && pm.resolveActivity(PHONE_INTENT, 0) != null;
+    }
+
+    @Override
+    public void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled) {
+        mRightAffordanceView.setClickable(touchExplorationEnabled);
+        mLeftAffordanceView.setClickable(touchExplorationEnabled);
+        mRightAffordanceView.setFocusable(accessibilityEnabled);
+        mLeftAffordanceView.setFocusable(accessibilityEnabled);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v == mRightAffordanceView) {
+            launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE);
+        } else if (v == mLeftAffordanceView) {
+            launchLeftAffordance();
+        }
+    }
+
+    public void launchCamera(String source) {
+        final Intent intent = getCameraIntent();
+        intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source);
+        boolean wouldLaunchResolverActivity = mActivityIntentHelper.wouldLaunchResolverActivity(
+                intent, KeyguardUpdateMonitor.getCurrentUser());
+        if (CameraIntents.isSecureCameraIntent(intent) && !wouldLaunchResolverActivity) {
+            AsyncTask.execute(new Runnable() {
+                @Override
+                public void run() {
+                    // Normally an activity will set it's requested rotation
+                    // animation on its window. However when launching an activity
+                    // causes the orientation to change this is too late. In these cases
+                    // the default animation is used. This doesn't look good for
+                    // the camera (as it rotates the camera contents out of sync
+                    // with physical reality). So, we ask the WindowManager to
+                    // force the crossfade animation if an orientation change
+                    // happens to occur during the launch.
+                    ActivityOptions o = ActivityOptions.makeBasic();
+                    o.setDisallowEnterPictureInPictureWhileLaunching(true);
+                    o.setRotationAnimationHint(
+                            WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS);
+                    try {
+                        ActivityTaskManager.getService().startActivityAsUser(
+                                null, getContext().getBasePackageName(),
+                                getContext().getAttributionTag(), intent,
+                                intent.resolveTypeIfNeeded(getContext().getContentResolver()),
+                                null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, o.toBundle(),
+                                UserHandle.CURRENT.getIdentifier());
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "Unable to start camera activity", e);
+                    }
+                }
+            });
+        } else {
+            // We need to delay starting the activity because ResolverActivity finishes itself if
+            // launched behind lockscreen.
+            mActivityStarter.startActivity(intent, false /* dismissShade */);
+        }
+    }
+
     public void setDarkAmount(float darkAmount) {
         if (darkAmount == mDarkAmount) {
             return;
@@ -323,6 +627,77 @@
         dozeTimeTick();
     }
 
+    public void launchLeftAffordance() {
+        if (mLeftIsVoiceAssist) {
+            launchVoiceAssist();
+        } else {
+            launchPhone();
+        }
+    }
+
+    @VisibleForTesting
+    void launchVoiceAssist() {
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                Dependency.get(AssistManager.class).launchVoiceAssistFromKeyguard();
+            }
+        };
+        if (!mKeyguardStateController.canDismissLockScreen()) {
+            Dependency.get(Dependency.BACKGROUND_EXECUTOR).execute(runnable);
+        } else {
+            boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr)
+                    && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0;
+            mCentralSurfaces.executeRunnableDismissingKeyguard(runnable, null /* cancelAction */,
+                    dismissShade, false /* afterKeyguardGone */, true /* deferred */);
+        }
+    }
+
+    private boolean canLaunchVoiceAssist() {
+        return Dependency.get(AssistManager.class).canVoiceAssistBeLaunchedFromKeyguard();
+    }
+
+    private void launchPhone() {
+        final TelecomManager tm = TelecomManager.from(mContext);
+        if (tm.isInCall()) {
+            AsyncTask.execute(new Runnable() {
+                @Override
+                public void run() {
+                    tm.showInCallScreen(false /* showDialpad */);
+                }
+            });
+        } else {
+            boolean dismissShade = !TextUtils.isEmpty(mLeftButtonStr)
+                    && Dependency.get(TunerService.class).getValue(LOCKSCREEN_LEFT_UNLOCK, 1) != 0;
+            mActivityStarter.startActivity(mLeftButton.getIntent(), dismissShade);
+        }
+    }
+
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        if (changedView == this && visibility == VISIBLE) {
+            updateCameraVisibility();
+        }
+    }
+
+    public KeyguardAffordanceView getLeftView() {
+        return mLeftAffordanceView;
+    }
+
+    public KeyguardAffordanceView getRightView() {
+        return mRightAffordanceView;
+    }
+
+    public View getLeftPreview() {
+        return mLeftPreview;
+    }
+
+    public View getRightPreview() {
+        return mCameraPreview;
+    }
+
     public View getIndicationArea() {
         return mIndicationArea;
     }
@@ -332,6 +707,66 @@
         return false;
     }
 
+    @Override
+    public void onUnlockedChanged() {
+        updateCameraVisibility();
+    }
+
+    @Override
+    public void onKeyguardShowingChanged() {
+        if (mKeyguardStateController.isShowing()) {
+            if (mQuickAccessWalletController != null) {
+                mQuickAccessWalletController.queryWalletCards(mCardRetriever);
+            }
+        }
+    }
+
+    private void inflateCameraPreview() {
+        if (mPreviewContainer == null) {
+            return;
+        }
+        View previewBefore = mCameraPreview;
+        boolean visibleBefore = false;
+        if (previewBefore != null) {
+            mPreviewContainer.removeView(previewBefore);
+            visibleBefore = previewBefore.getVisibility() == View.VISIBLE;
+        }
+        mCameraPreview = mPreviewInflater.inflatePreview(getCameraIntent());
+        if (mCameraPreview != null) {
+            mPreviewContainer.addView(mCameraPreview);
+            mCameraPreview.setVisibility(visibleBefore ? View.VISIBLE : View.INVISIBLE);
+        }
+        if (mAffordanceHelper != null) {
+            mAffordanceHelper.updatePreviews();
+        }
+    }
+
+    private void updateLeftPreview() {
+        if (mPreviewContainer == null) {
+            return;
+        }
+        View previewBefore = mLeftPreview;
+        if (previewBefore != null) {
+            mPreviewContainer.removeView(previewBefore);
+        }
+
+        if (mLeftIsVoiceAssist) {
+            if (Dependency.get(AssistManager.class).getVoiceInteractorComponentName() != null) {
+                mLeftPreview = mPreviewInflater.inflatePreviewFromService(
+                        Dependency.get(AssistManager.class).getVoiceInteractorComponentName());
+            }
+        } else {
+            mLeftPreview = mPreviewInflater.inflatePreview(mLeftButton.getIntent());
+        }
+        if (mLeftPreview != null) {
+            mPreviewContainer.addView(mLeftPreview);
+            mLeftPreview.setVisibility(View.INVISIBLE);
+        }
+        if (mAffordanceHelper != null) {
+            mAffordanceHelper.updatePreviews();
+        }
+    }
+
     public void startFinishDozeAnimation() {
         long delay = 0;
         if (mWalletButton.getVisibility() == View.VISIBLE) {
@@ -343,6 +778,13 @@
         if (mControlsButton.getVisibility() == View.VISIBLE) {
             startFinishDozeAnimationElement(mControlsButton, delay);
         }
+        if (mLeftAffordanceView.getVisibility() == View.VISIBLE) {
+            startFinishDozeAnimationElement(mLeftAffordanceView, delay);
+            delay += DOZE_ANIMATION_STAGGER_DELAY;
+        }
+        if (mRightAffordanceView.getVisibility() == View.VISIBLE) {
+            startFinishDozeAnimationElement(mRightAffordanceView, delay);
+        }
     }
 
     private void startFinishDozeAnimationElement(View element, long delay) {
@@ -356,9 +798,58 @@
                 .setDuration(DOZE_ANIMATION_ELEMENT_DURATION);
     }
 
+    private final BroadcastReceiver mDevicePolicyReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    updateCameraVisibility();
+                }
+            });
+        }
+    };
+
+    private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
+            new KeyguardUpdateMonitorCallback() {
+                @Override
+                public void onUserSwitchComplete(int userId) {
+                    updateCameraVisibility();
+                }
+
+                @Override
+                public void onUserUnlocked() {
+                    inflateCameraPreview();
+                    updateCameraVisibility();
+                    updateLeftAffordance();
+                }
+            };
+
+    public void updateLeftAffordance() {
+        updateLeftAffordanceIcon();
+        updateLeftPreview();
+    }
+
+    private void setRightButton(IntentButton button) {
+        mRightButton = button;
+        updateRightAffordanceIcon();
+        updateCameraVisibility();
+        inflateCameraPreview();
+    }
+
+    private void setLeftButton(IntentButton button) {
+        mLeftButton = button;
+        if (!(mLeftButton instanceof DefaultLeftButton)) {
+            mLeftIsVoiceAssist = false;
+        }
+        updateLeftAffordance();
+    }
+
     public void setDozing(boolean dozing, boolean animate) {
         mDozing = dozing;
 
+        updateCameraVisibility();
+        updateLeftAffordanceIcon();
         updateWalletVisibility();
         updateControlsVisibility();
         updateQRCodeButtonVisibility();
@@ -397,12 +888,77 @@
      * Sets the alpha of the indication areas and affordances, excluding the lock icon.
      */
     public void setAffordanceAlpha(float alpha) {
+        mLeftAffordanceView.setAlpha(alpha);
+        mRightAffordanceView.setAlpha(alpha);
         mIndicationArea.setAlpha(alpha);
         mWalletButton.setAlpha(alpha);
         mQRCodeScannerButton.setAlpha(alpha);
         mControlsButton.setAlpha(alpha);
     }
 
+    private class DefaultLeftButton implements IntentButton {
+
+        private IconState mIconState = new IconState();
+
+        @Override
+        public IconState getIcon() {
+            mLeftIsVoiceAssist = canLaunchVoiceAssist();
+            if (mLeftIsVoiceAssist) {
+                mIconState.isVisible = mUserSetupComplete && mShowLeftAffordance;
+                if (mLeftAssistIcon == null) {
+                    mIconState.drawable = mContext.getDrawable(R.drawable.ic_mic_26dp);
+                } else {
+                    mIconState.drawable = mLeftAssistIcon;
+                }
+                mIconState.contentDescription = mContext.getString(
+                        R.string.accessibility_voice_assist_button);
+            } else {
+                mIconState.isVisible = mUserSetupComplete && mShowLeftAffordance
+                        && isPhoneVisible();
+                mIconState.drawable = mContext.getDrawable(
+                        com.android.internal.R.drawable.ic_phone);
+                mIconState.contentDescription = mContext.getString(
+                        R.string.accessibility_phone_button);
+            }
+            return mIconState;
+        }
+
+        @Override
+        public Intent getIntent() {
+            return PHONE_INTENT;
+        }
+    }
+
+    private class DefaultRightButton implements IntentButton {
+
+        private IconState mIconState = new IconState();
+
+        @Override
+        public IconState getIcon() {
+            boolean isCameraDisabled = (mCentralSurfaces != null)
+                    && !mCentralSurfaces.isCameraAllowedByAdmin();
+            mIconState.isVisible = !isCameraDisabled
+                    && mShowCameraAffordance
+                    && mUserSetupComplete
+                    && resolveCameraIntent() != null;
+            mIconState.drawable = mContext.getDrawable(R.drawable.ic_camera_alt_24dp);
+            mIconState.contentDescription =
+                    mContext.getString(R.string.accessibility_camera_button);
+            return mIconState;
+        }
+
+        @Override
+        public Intent getIntent() {
+            boolean canDismissLs = mKeyguardStateController.canDismissLockScreen();
+            boolean secure = mKeyguardStateController.isMethodSecure();
+            if (secure && !canDismissLs) {
+                return CameraIntents.getSecureCameraIntent(getContext());
+            } else {
+                return CameraIntents.getInsecureCameraIntent(getContext());
+            }
+        }
+    }
+
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         int bottom = insets.getDisplayCutout() != null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index c757cba..fbbb587 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -50,6 +50,8 @@
 import android.app.Fragment;
 import android.app.StatusBarManager;
 import android.content.ContentResolver;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Canvas;
@@ -143,6 +145,7 @@
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.GestureRecorder;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -249,6 +252,9 @@
     private final OnOverscrollTopChangedListener
             mOnOverscrollTopChangedListener =
             new OnOverscrollTopChangedListener();
+    private final KeyguardAffordanceHelperCallback
+            mKeyguardAffordanceHelperCallback =
+            new KeyguardAffordanceHelperCallback();
     private final OnEmptySpaceClickListener
             mOnEmptySpaceClickListener =
             new OnEmptySpaceClickListener();
@@ -332,6 +338,8 @@
     // Current max allowed keyguard notifications determined by measuring the panel
     private int mMaxAllowedKeyguardNotifications;
 
+    private ViewGroup mPreviewContainer;
+    private KeyguardAffordanceHelper mAffordanceHelper;
     private KeyguardQsUserSwitchController mKeyguardQsUserSwitchController;
     private KeyguardUserSwitcherController mKeyguardUserSwitcherController;
     private KeyguardStatusBarView mKeyguardStatusBar;
@@ -429,6 +437,8 @@
      */
     private boolean mQsAnimatorExpand;
     private boolean mIsLaunchTransitionFinished;
+    private boolean mIsLaunchTransitionRunning;
+    private Runnable mLaunchAnimationEndRunnable;
     private boolean mOnlyAffordanceInThisMotion;
     private ValueAnimator mQsSizeChangeAnimator;
 
@@ -445,8 +455,10 @@
     private boolean mClosingWithAlphaFadeOut;
     private boolean mHeadsUpAnimatingAway;
     private boolean mLaunchingAffordance;
+    private boolean mAffordanceHasPreview;
     private final FalsingManager mFalsingManager;
     private final FalsingCollector mFalsingCollector;
+    private String mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
 
     private Runnable mHeadsUpExistenceChangedRunnable = () -> {
         setHeadsUpAnimatingAway(false);
@@ -479,6 +491,7 @@
     private float mLinearDarkAmount;
 
     private boolean mPulsing;
+    private boolean mUserSetupComplete;
     private boolean mHideIconsDuringLaunchAnimation = true;
     private int mStackScrollerMeasuringPass;
     /**
@@ -995,6 +1008,8 @@
                 mOnEmptySpaceClickListener);
         addTrackingHeadsUpListener(mNotificationStackScrollLayoutController::setTrackingHeadsUp);
         mKeyguardBottomArea = mView.findViewById(R.id.keyguard_bottom_area);
+        mPreviewContainer = mView.findViewById(R.id.preview_container);
+        mKeyguardBottomArea.setPreviewContainer(mPreviewContainer);
 
         initBottomArea();
 
@@ -1018,6 +1033,7 @@
 
         mView.setRtlChangeListener(layoutDirection -> {
             if (layoutDirection != mOldLayoutDirection) {
+                mAffordanceHelper.onRtlPropertiesChanged();
                 mOldLayoutDirection = layoutDirection;
             }
         });
@@ -1251,6 +1267,7 @@
         mKeyguardBottomArea = (KeyguardBottomAreaView) mLayoutInflater.inflate(
                 R.layout.keyguard_bottom_area, mView, false);
         mKeyguardBottomArea.initFrom(oldBottomArea);
+        mKeyguardBottomArea.setPreviewContainer(mPreviewContainer);
         mView.addView(mKeyguardBottomArea, index);
         initBottomArea();
         mKeyguardIndicationController.setIndicationArea(mKeyguardBottomArea);
@@ -1287,7 +1304,11 @@
     }
 
     private void initBottomArea() {
+        mAffordanceHelper = new KeyguardAffordanceHelper(
+                mKeyguardAffordanceHelperCallback, mView.getContext(), mFalsingManager);
+        mKeyguardBottomArea.setAffordanceHelper(mAffordanceHelper);
         mKeyguardBottomArea.setCentralSurfaces(mCentralSurfaces);
+        mKeyguardBottomArea.setUserSetupComplete(mUserSetupComplete);
         mKeyguardBottomArea.setFalsingManager(mFalsingManager);
         mKeyguardBottomArea.initWallet(mQuickAccessWalletController);
         mKeyguardBottomArea.initControls(mControlsComponent);
@@ -1652,6 +1673,10 @@
     public void resetViews(boolean animate) {
         mIsLaunchTransitionFinished = false;
         mBlockTouches = false;
+        if (!mLaunchingAffordance) {
+            mAffordanceHelper.reset(false);
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
+        }
         mCentralSurfaces.getGutsManager().closeAndSaveGuts(true /* leavebehind */, true /* force */,
                 true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
         if (animate && !isFullyCollapsed()) {
@@ -2194,6 +2219,11 @@
         return isFullyCollapsed() || mBarState != StatusBarState.SHADE;
     }
 
+    @Override
+    protected boolean shouldGestureIgnoreXTouchSlop(float x, float y) {
+        return !mAffordanceHelper.isOnAffordanceIcon(x, y);
+    }
+
     private void onQsTouch(MotionEvent event) {
         int pointerIndex = event.findPointerIndex(mTrackingPointer);
         if (pointerIndex < 0) {
@@ -3358,6 +3388,9 @@
             mQsExpandImmediate = true;
             setShowShelfOnly(true);
         }
+        if (mBarState == KEYGUARD || mBarState == StatusBarState.SHADE_LOCKED) {
+            mAffordanceHelper.animateHideLeftRightIcon();
+        }
         mNotificationStackScrollLayoutController.onPanelTrackingStarted();
         cancelPendingPanelCollapse();
     }
@@ -3371,6 +3404,12 @@
                     true /* animate */);
         }
         mNotificationStackScrollLayoutController.onPanelTrackingStopped();
+        if (expand && (mBarState == KEYGUARD
+                || mBarState == StatusBarState.SHADE_LOCKED)) {
+            if (!mHintAnimationRunning) {
+                mAffordanceHelper.reset(true);
+            }
+        }
 
         // If we unlocked from a swipe, the user's finger might still be down after the
         // unlock animation ends. We need to wait until ACTION_UP to enable blurs again.
@@ -3443,6 +3482,10 @@
         return mIsLaunchTransitionFinished;
     }
 
+    public boolean isLaunchTransitionRunning() {
+        return mIsLaunchTransitionRunning;
+    }
+
     @Override
     public void setIsLaunchAnimationRunning(boolean running) {
         boolean wasRunning = mIsLaunchAnimationRunning;
@@ -3461,6 +3504,10 @@
         }
     }
 
+    public void setLaunchTransitionEndRunnable(Runnable r) {
+        mLaunchAnimationEndRunnable = r;
+    }
+
     private void updateDozingVisibilities(boolean animate) {
         mKeyguardBottomArea.setDozing(mDozing, animate);
         if (!mDozing && animate) {
@@ -3639,8 +3686,30 @@
                 && mBarState == StatusBarState.SHADE;
     }
 
-    /** Launches the camera. */
-    public void launchCamera(int source) {
+    public void launchCamera(boolean animate, int source) {
+        if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) {
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP;
+        } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE) {
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_WIGGLE;
+        } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER) {
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER;
+        } else {
+
+            // Default.
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
+        }
+
+        // If we are launching it when we are occluded already we don't want it to animate,
+        // nor setting these flags, since the occluded state doesn't change anymore, hence it's
+        // never reset.
+        if (!isFullyCollapsed()) {
+            setLaunchingAffordance(true);
+        } else {
+            animate = false;
+        }
+        mAffordanceHasPreview = mKeyguardBottomArea.getRightPreview() != null;
+        mAffordanceHelper.launchAffordance(
+                animate, mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
     }
 
     public void onAffordanceLaunchEnded() {
@@ -3653,6 +3722,9 @@
      */
     private void setLaunchingAffordance(boolean launchingAffordance) {
         mLaunchingAffordance = launchingAffordance;
+        mKeyguardAffordanceHelperCallback.getLeftIcon().setLaunchingAffordance(launchingAffordance);
+        mKeyguardAffordanceHelperCallback.getRightIcon().setLaunchingAffordance(
+                launchingAffordance);
         mKeyguardBypassController.setLaunchingAffordance(launchingAffordance);
     }
 
@@ -3660,14 +3732,24 @@
      * Return true when a bottom affordance is launching an occluded activity with a splash screen.
      */
     public boolean isLaunchingAffordanceWithPreview() {
-        return mLaunchingAffordance;
+        return mLaunchingAffordance && mAffordanceHasPreview;
     }
 
     /**
      * Whether the camera application can be launched for the camera launch gesture.
      */
     public boolean canCameraGestureBeLaunched() {
-        return false;
+        if (!mCentralSurfaces.isCameraAllowedByAdmin()) {
+            return false;
+        }
+
+        ResolveInfo resolveInfo = mKeyguardBottomArea.resolveCameraIntent();
+        String
+                packageToLaunch =
+                (resolveInfo == null || resolveInfo.activityInfo == null) ? null
+                        : resolveInfo.activityInfo.packageName;
+        return packageToLaunch != null && (mBarState != StatusBarState.SHADE || !isForegroundApp(
+                packageToLaunch)) && !mAffordanceHelper.isSwipingInProgress();
     }
 
     /**
@@ -3756,6 +3838,9 @@
     @Override
     public void setTouchAndAnimationDisabled(boolean disabled) {
         super.setTouchAndAnimationDisabled(disabled);
+        if (disabled && mAffordanceHelper.isSwipingInProgress() && !mIsLaunchTransitionRunning) {
+            mAffordanceHelper.reset(false /* animate */);
+        }
         mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled);
     }
 
@@ -3838,6 +3923,11 @@
         return mKeyguardBottomArea;
     }
 
+    public void setUserSetupComplete(boolean userSetupComplete) {
+        mUserSetupComplete = userSetupComplete;
+        mKeyguardBottomArea.setUserSetupComplete(userSetupComplete);
+    }
+
     public void applyLaunchAnimationProgress(float linearProgress) {
         boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS,
                 linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
@@ -4191,6 +4281,10 @@
                     mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
                 }
                 boolean handled = false;
+                if ((!mIsExpanding || mHintAnimationRunning) && !mQsExpanded
+                        && mBarState != StatusBarState.SHADE && !mDozing) {
+                    handled |= mAffordanceHelper.onTouchEvent(event);
+                }
                 if (mOnlyAffordanceInThisMotion) {
                     return true;
                 }
@@ -4433,6 +4527,139 @@
         }
     }
 
+    private class KeyguardAffordanceHelperCallback implements KeyguardAffordanceHelper.Callback {
+        @Override
+        public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) {
+            boolean
+                    start =
+                    mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? rightPage
+                            : !rightPage;
+            mIsLaunchTransitionRunning = true;
+            mLaunchAnimationEndRunnable = null;
+            float displayDensity = mCentralSurfaces.getDisplayDensity();
+            int lengthDp = Math.abs((int) (translation / displayDensity));
+            int velocityDp = Math.abs((int) (vel / displayDensity));
+            if (start) {
+                mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_DIALER, lengthDp, velocityDp);
+                mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_DIALER);
+                mFalsingCollector.onLeftAffordanceOn();
+                if (mFalsingCollector.shouldEnforceBouncer()) {
+                    mCentralSurfaces.executeRunnableDismissingKeyguard(
+                            () -> mKeyguardBottomArea.launchLeftAffordance(), null,
+                            true /* dismissShade */, false /* afterKeyguardGone */,
+                            true /* deferred */);
+                } else {
+                    mKeyguardBottomArea.launchLeftAffordance();
+                }
+            } else {
+                if (KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE.equals(
+                        mLastCameraLaunchSource)) {
+                    mLockscreenGestureLogger.write(
+                            MetricsEvent.ACTION_LS_CAMERA, lengthDp, velocityDp);
+                    mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_CAMERA);
+                }
+                mFalsingCollector.onCameraOn();
+                if (mFalsingCollector.shouldEnforceBouncer()) {
+                    mCentralSurfaces.executeRunnableDismissingKeyguard(
+                            () -> mKeyguardBottomArea.launchCamera(mLastCameraLaunchSource), null,
+                            true /* dismissShade */, false /* afterKeyguardGone */,
+                            true /* deferred */);
+                } else {
+                    mKeyguardBottomArea.launchCamera(mLastCameraLaunchSource);
+                }
+            }
+            mCentralSurfaces.startLaunchTransitionTimeout();
+            mBlockTouches = true;
+        }
+
+        @Override
+        public void onAnimationToSideEnded() {
+            mIsLaunchTransitionRunning = false;
+            mIsLaunchTransitionFinished = true;
+            if (mLaunchAnimationEndRunnable != null) {
+                mLaunchAnimationEndRunnable.run();
+                mLaunchAnimationEndRunnable = null;
+            }
+            mCentralSurfaces.readyForKeyguardDone();
+        }
+
+        @Override
+        public float getMaxTranslationDistance() {
+            return (float) Math.hypot(mView.getWidth(), getHeight());
+        }
+
+        @Override
+        public void onSwipingStarted(boolean rightIcon) {
+            mFalsingCollector.onAffordanceSwipingStarted(rightIcon);
+            mView.requestDisallowInterceptTouchEvent(true);
+            mOnlyAffordanceInThisMotion = true;
+            mQsTracking = false;
+        }
+
+        @Override
+        public void onSwipingAborted() {
+            mFalsingCollector.onAffordanceSwipingAborted();
+        }
+
+        @Override
+        public void onIconClicked(boolean rightIcon) {
+            if (mHintAnimationRunning) {
+                return;
+            }
+            mHintAnimationRunning = true;
+            mAffordanceHelper.startHintAnimation(rightIcon, () -> {
+                mHintAnimationRunning = false;
+                mCentralSurfaces.onHintFinished();
+            });
+            rightIcon =
+                    mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? !rightIcon
+                            : rightIcon;
+            if (rightIcon) {
+                mCentralSurfaces.onCameraHintStarted();
+            } else {
+                if (mKeyguardBottomArea.isLeftVoiceAssist()) {
+                    mCentralSurfaces.onVoiceAssistHintStarted();
+                } else {
+                    mCentralSurfaces.onPhoneHintStarted();
+                }
+            }
+        }
+
+        @Override
+        public KeyguardAffordanceView getLeftIcon() {
+            return mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+                    ? mKeyguardBottomArea.getRightView() : mKeyguardBottomArea.getLeftView();
+        }
+
+        @Override
+        public KeyguardAffordanceView getRightIcon() {
+            return mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+                    ? mKeyguardBottomArea.getLeftView() : mKeyguardBottomArea.getRightView();
+        }
+
+        @Override
+        public View getLeftPreview() {
+            return mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+                    ? mKeyguardBottomArea.getRightPreview() : mKeyguardBottomArea.getLeftPreview();
+        }
+
+        @Override
+        public View getRightPreview() {
+            return mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+                    ? mKeyguardBottomArea.getLeftPreview() : mKeyguardBottomArea.getRightPreview();
+        }
+
+        @Override
+        public float getAffordanceFalsingFactor() {
+            return mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+        }
+
+        @Override
+        public boolean needsAntiFalsing() {
+            return mBarState == KEYGUARD;
+        }
+    }
+
     private class OnEmptySpaceClickListener implements
             NotificationStackScrollLayout.OnEmptySpaceClickListener {
         @Override
@@ -4891,6 +5118,15 @@
         }
     }
 
+    private class OnConfigurationChangedListener extends
+            PanelViewController.OnConfigurationChangedListener {
+        @Override
+        public void onConfigurationChanged(Configuration newConfig) {
+            super.onConfigurationChanged(newConfig);
+            mAffordanceHelper.onConfigurationChanged();
+        }
+    }
+
     private class OnApplyWindowInsetsListener implements View.OnApplyWindowInsetsListener {
         public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
             // the same types of insets that are handled in NotificationShadeWindowView
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
index d2fc1af..ed12b00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
@@ -484,6 +484,8 @@
 
     protected abstract boolean shouldGestureWaitForTouchSlop();
 
+    protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y);
+
     protected void onTrackingStopped(boolean expand) {
         mTracking = false;
         mCentralSurfaces.onTrackingStopped(expand);
@@ -1331,7 +1333,7 @@
 
             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                 mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
-                mIgnoreXTouchSlop = true;
+                mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
             }
 
             switch (event.getActionMasked()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index a89c128..753e940 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -119,6 +119,17 @@
     }
 
     /**
+     * Returns {@code true} if the charging source is
+     * {@link android.os.BatteryManager#BATTERY_PLUGGED_DOCK}.
+     *
+     * <P>Note that charging from dock is not considered as wireless charging. In other words,
+     * {@link BatteryController#isWirelessCharging()} and this are mutually exclusive.
+     */
+    default boolean isChargingSourceDock() {
+        return false;
+    }
+
+    /**
      * A listener that will be notified whenever a change in battery level or power save mode has
      * occurred.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index 917a5e0..33ddf7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -76,7 +76,7 @@
 
     protected int mLevel;
     protected boolean mPluggedIn;
-    private boolean mPluggedInWireless;
+    private int mPluggedChargingSource;
     protected boolean mCharging;
     private boolean mStateUnknown = false;
     private boolean mCharged;
@@ -195,10 +195,8 @@
             mLevel = (int)(100f
                     * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
                     / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100));
-            mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
-            mPluggedInWireless = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0)
-                    == BatteryManager.BATTERY_PLUGGED_WIRELESS;
-
+            mPluggedChargingSource = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
+            mPluggedIn = mPluggedChargingSource != 0;
             final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
                     BatteryManager.BATTERY_STATUS_UNKNOWN);
             mCharged = status == BatteryManager.BATTERY_STATUS_FULL;
@@ -284,7 +282,7 @@
 
     @Override
     public boolean isPluggedInWireless() {
-        return mPluggedInWireless;
+        return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS;
     }
 
     @Override
@@ -441,4 +439,9 @@
         registerReceiver();
         updatePowerSave();
     }
+
+    @Override
+    public boolean isChargingSourceDock() {
+        return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_DOCK;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
index 4cf1d2b..9946b4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -17,15 +17,11 @@
 package com.android.systemui.statusbar.policy;
 
 import android.annotation.WorkerThread;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.text.TextUtils;
@@ -33,12 +29,19 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.inject.Inject;
 
@@ -59,65 +62,88 @@
         "com.android.settings.flashlight.action.FLASHLIGHT_CHANGED";
 
     private final CameraManager mCameraManager;
-    private final Context mContext;
-    /** Call {@link #ensureHandler()} before using */
-    private Handler mHandler;
+    private final Executor mExecutor;
+    private final SecureSettings mSecureSettings;
+    private final DumpManager mDumpManager;
+    private final BroadcastSender mBroadcastSender;
 
-    /** Lock on mListeners when accessing */
+    private final boolean mHasFlashlight;
+
+    @GuardedBy("mListeners")
     private final ArrayList<WeakReference<FlashlightListener>> mListeners = new ArrayList<>(1);
 
-    /** Lock on {@code this} when accessing */
+    @GuardedBy("this")
     private boolean mFlashlightEnabled;
-
-    private String mCameraId;
+    @GuardedBy("this")
     private boolean mTorchAvailable;
 
-    @Inject
-    public FlashlightControllerImpl(Context context, DumpManager dumpManager) {
-        mContext = context;
-        mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+    private final AtomicReference<String> mCameraId;
+    private final AtomicBoolean mInitted = new AtomicBoolean(false);
 
-        dumpManager.registerDumpable(getClass().getSimpleName(), this);
-        tryInitCamera();
+    @Inject
+    public FlashlightControllerImpl(
+            DumpManager dumpManager,
+            CameraManager cameraManager,
+            @Background Executor bgExecutor,
+            SecureSettings secureSettings,
+            BroadcastSender broadcastSender,
+            PackageManager packageManager
+    ) {
+        mCameraManager = cameraManager;
+        mExecutor = bgExecutor;
+        mCameraId = new AtomicReference<>(null);
+        mSecureSettings = secureSettings;
+        mDumpManager = dumpManager;
+        mBroadcastSender = broadcastSender;
+
+        mHasFlashlight = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+        init();
     }
 
+    private void init() {
+        if (!mInitted.getAndSet(true)) {
+            mDumpManager.registerDumpable(getClass().getSimpleName(), this);
+            mExecutor.execute(this::tryInitCamera);
+        }
+    }
+
+    @WorkerThread
     private void tryInitCamera() {
+        if (!mHasFlashlight || mCameraId.get() != null) return;
         try {
-            mCameraId = getCameraId();
+            mCameraId.set(getCameraId());
         } catch (Throwable e) {
             Log.e(TAG, "Couldn't initialize.", e);
             return;
         }
 
-        if (mCameraId != null) {
-            ensureHandler();
-            mCameraManager.registerTorchCallback(mTorchCallback, mHandler);
+        if (mCameraId.get() != null) {
+            mCameraManager.registerTorchCallback(mExecutor, mTorchCallback);
         }
     }
 
     public void setFlashlight(boolean enabled) {
-        boolean pendingError = false;
-        synchronized (this) {
-            if (mCameraId == null) return;
-            if (mFlashlightEnabled != enabled) {
-                mFlashlightEnabled = enabled;
-                try {
-                    mCameraManager.setTorchMode(mCameraId, enabled);
-                } catch (CameraAccessException e) {
-                    Log.e(TAG, "Couldn't set torch mode", e);
-                    mFlashlightEnabled = false;
-                    pendingError = true;
+        if (!mHasFlashlight) return;
+        if (mCameraId.get() == null) {
+            mExecutor.execute(this::tryInitCamera);
+        }
+        mExecutor.execute(() -> {
+            if (mCameraId.get() == null) return;
+            synchronized (this) {
+                if (mFlashlightEnabled != enabled) {
+                    try {
+                        mCameraManager.setTorchMode(mCameraId.get(), enabled);
+                    } catch (CameraAccessException e) {
+                        Log.e(TAG, "Couldn't set torch mode", e);
+                        dispatchError();
+                    }
                 }
             }
-        }
-        dispatchModeChanged(mFlashlightEnabled);
-        if (pendingError) {
-            dispatchError();
-        }
+        });
     }
 
     public boolean hasFlashlight() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+        return mHasFlashlight;
     }
 
     public synchronized boolean isEnabled() {
@@ -131,13 +157,13 @@
     @Override
     public void addCallback(@NonNull FlashlightListener l) {
         synchronized (mListeners) {
-            if (mCameraId == null) {
-                tryInitCamera();
+            if (mCameraId.get() == null) {
+                mExecutor.execute(this::tryInitCamera);
             }
             cleanUpListenersLocked(l);
             mListeners.add(new WeakReference<>(l));
-            l.onFlashlightAvailabilityChanged(mTorchAvailable);
-            l.onFlashlightChanged(mFlashlightEnabled);
+            l.onFlashlightAvailabilityChanged(isAvailable());
+            l.onFlashlightChanged(isEnabled());
         }
     }
 
@@ -148,14 +174,7 @@
         }
     }
 
-    private synchronized void ensureHandler() {
-        if (mHandler == null) {
-            HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
-            thread.start();
-            mHandler = new Handler(thread.getLooper());
-        }
-    }
-
+    @WorkerThread
     private String getCameraId() throws CameraAccessException {
         String[] ids = mCameraManager.getCameraIdList();
         for (String id : ids) {
@@ -221,10 +240,9 @@
         @Override
         @WorkerThread
         public void onTorchModeUnavailable(String cameraId) {
-            if (TextUtils.equals(cameraId, mCameraId)) {
+            if (TextUtils.equals(cameraId, mCameraId.get())) {
                 setCameraAvailable(false);
-                Settings.Secure.putInt(
-                    mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 0);
+                mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 0);
 
             }
         }
@@ -232,14 +250,12 @@
         @Override
         @WorkerThread
         public void onTorchModeChanged(String cameraId, boolean enabled) {
-            if (TextUtils.equals(cameraId, mCameraId)) {
+            if (TextUtils.equals(cameraId, mCameraId.get())) {
                 setCameraAvailable(true);
                 setTorchMode(enabled);
-                Settings.Secure.putInt(
-                    mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 1);
-                Settings.Secure.putInt(
-                    mContext.getContentResolver(), Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0);
-                mContext.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED));
+                mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 1);
+                mSecureSettings.putInt(Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0);
+                mBroadcastSender.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED));
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java
new file mode 100644
index 0000000..3d31714
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.ActivityIntentHelper;
+import com.android.systemui.statusbar.phone.KeyguardPreviewContainer;
+
+import java.util.List;
+
+/**
+ * Utility class to inflate previews for phone and camera affordance.
+ */
+public class PreviewInflater {
+
+    private static final String TAG = "PreviewInflater";
+
+    private static final String META_DATA_KEYGUARD_LAYOUT = "com.android.keyguard.layout";
+    private final ActivityIntentHelper mActivityIntentHelper;
+
+    private Context mContext;
+    private LockPatternUtils mLockPatternUtils;
+
+    public PreviewInflater(Context context, LockPatternUtils lockPatternUtils,
+            ActivityIntentHelper activityIntentHelper) {
+        mContext = context;
+        mLockPatternUtils = lockPatternUtils;
+        mActivityIntentHelper = activityIntentHelper;
+    }
+
+    public View inflatePreview(Intent intent) {
+        WidgetInfo info = getWidgetInfo(intent);
+        return inflatePreview(info);
+    }
+
+    public View inflatePreviewFromService(ComponentName componentName) {
+        WidgetInfo info = getWidgetInfoFromService(componentName);
+        return inflatePreview(info);
+    }
+
+    private KeyguardPreviewContainer inflatePreview(WidgetInfo info) {
+        if (info == null) {
+            return null;
+        }
+        View v = inflateWidgetView(info);
+        if (v == null) {
+            return null;
+        }
+        KeyguardPreviewContainer container = new KeyguardPreviewContainer(mContext, null);
+        container.addView(v);
+        return container;
+    }
+
+    private View inflateWidgetView(WidgetInfo widgetInfo) {
+        View widgetView = null;
+        try {
+            Context appContext = mContext.createPackageContext(
+                    widgetInfo.contextPackage, Context.CONTEXT_RESTRICTED);
+            LayoutInflater appInflater = (LayoutInflater)
+                    appContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            appInflater = appInflater.cloneInContext(appContext);
+            widgetView = appInflater.inflate(widgetInfo.layoutId, null, false);
+        } catch (PackageManager.NameNotFoundException|RuntimeException e) {
+            Log.w(TAG, "Error creating widget view", e);
+        }
+        return widgetView;
+    }
+
+    private WidgetInfo getWidgetInfoFromService(ComponentName componentName) {
+        PackageManager packageManager = mContext.getPackageManager();
+        // Look for the preview specified in the service meta-data
+        try {
+            Bundle metaData = packageManager.getServiceInfo(
+                    componentName, PackageManager.GET_META_DATA).metaData;
+            return getWidgetInfoFromMetaData(componentName.getPackageName(), metaData);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Failed to load preview; " + componentName.flattenToShortString()
+                    + " not found", e);
+        }
+        return null;
+    }
+
+    private WidgetInfo getWidgetInfoFromMetaData(String contextPackage,
+            Bundle metaData) {
+        if (metaData == null) {
+            return null;
+        }
+        int layoutId = metaData.getInt(META_DATA_KEYGUARD_LAYOUT);
+        if (layoutId == 0) {
+            return null;
+        }
+        WidgetInfo info = new WidgetInfo();
+        info.contextPackage = contextPackage;
+        info.layoutId = layoutId;
+        return info;
+    }
+
+    private WidgetInfo getWidgetInfo(Intent intent) {
+        PackageManager packageManager = mContext.getPackageManager();
+        int flags = PackageManager.MATCH_DEFAULT_ONLY
+                | PackageManager.MATCH_DIRECT_BOOT_AWARE
+                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+        final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
+                intent, flags, KeyguardUpdateMonitor.getCurrentUser());
+        if (appList.size() == 0) {
+            return null;
+        }
+        ResolveInfo resolved = packageManager.resolveActivityAsUser(intent,
+                flags | PackageManager.GET_META_DATA,
+                KeyguardUpdateMonitor.getCurrentUser());
+        if (mActivityIntentHelper.wouldLaunchResolverActivity(resolved, appList)) {
+            return null;
+        }
+        if (resolved == null || resolved.activityInfo == null) {
+            return null;
+        }
+        return getWidgetInfoFromMetaData(resolved.activityInfo.packageName,
+                resolved.activityInfo.metaData);
+    }
+
+    private static class WidgetInfo {
+        String contextPackage;
+        int layoutId;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
index 4e9030f..dac8a0b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
@@ -92,7 +92,15 @@
     }
 
     private void updateConditionMetState(Condition condition) {
-        mConditions.get(condition).stream().forEach(token -> mSubscriptions.get(token).update());
+        final ArraySet<Subscription.Token> subscriptions = mConditions.get(condition);
+
+        // It's possible the condition was removed between the time the callback occurred and
+        // update was executed on the main thread.
+        if (subscriptions == null) {
+            return;
+        }
+
+        subscriptions.stream().forEach(token -> mSubscriptions.get(token).update());
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index d5df9fe..c48cbb1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -159,7 +159,7 @@
     @Test
     fun doesNotStartIfAnimationIsCancelled() {
         val runner = activityLaunchAnimator.createRunner(controller)
-        runner.onAnimationCancelled()
+        runner.onAnimationCancelled(false /* isKeyguardOccluded */)
         runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
 
         waitForIdleSync()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
index 1527f0d..2eb4783 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
@@ -371,11 +371,9 @@
         powerManager,
         R.layout.media_ttt_chip
     ) {
-        override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) {
-
-        }
-
-        override fun getIconSize(isAppIcon: Boolean): Int? = ICON_SIZE
+        override val windowLayoutParams = commonWindowLayoutParams
+        override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) {}
+        override fun getIconSize(isAppIcon: Boolean): Int = ICON_SIZE
     }
 
     inner class ChipInfo : ChipInfoCommon {
@@ -386,4 +384,4 @@
 private const val PACKAGE_NAME = "com.android.systemui"
 private const val APP_NAME = "Fake App Name"
 private const val TIMEOUT_MS = 10000L
-private const val ICON_SIZE = 47
\ No newline at end of file
+private const val ICON_SIZE = 47
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
index b4cae38..d0cf792 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
@@ -74,9 +74,9 @@
 
         // Verify ripple added to window manager.
         captor.value.onBatteryLevelChanged(
-                0 /* unusedBatteryLevel */,
-                true /* plugged in */,
-                false /* charging */)
+                /* unusedBatteryLevel= */ 0,
+                /* plugged in= */ true,
+                /* charging= */ false)
         val attachListenerCaptor =
                 ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
         verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture())
@@ -144,4 +144,22 @@
         // Verify that ripple is triggered.
         verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any())
     }
+
+    @Test
+    fun testRipple_whenDocked_doesNotPlayRipple() {
+        `when`(batteryController.isChargingSourceDock).thenReturn(true)
+        val captor = ArgumentCaptor
+                .forClass(BatteryController.BatteryStateChangeCallback::class.java)
+        verify(batteryController).addCallback(captor.capture())
+
+        captor.value.onBatteryLevelChanged(
+                /* unusedBatteryLevel= */ 0,
+                /* plugged in= */ true,
+                /* charging= */ false)
+
+        val attachListenerCaptor =
+                ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
+        verify(rippleView, never()).addOnAttachStateChangeListener(attachListenerCaptor.capture())
+        verify(windowManager, never()).addView(eq(rippleView), any<WindowManager.LayoutParams>())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
index 769143d..d4add75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -108,6 +108,7 @@
     @Test
     public void testBlockableEntryWhenCritical() {
         doReturn(true).when(mChannel).isBlockable();
+        mEntry.setRanking(mEntry.getRanking());
 
         assertTrue(mEntry.isBlockable());
     }
@@ -117,6 +118,7 @@
     public void testBlockableEntryWhenCriticalAndChannelNotBlockable() {
         doReturn(true).when(mChannel).isBlockable();
         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
+        mEntry.setRanking(mEntry.getRanking());
 
         assertTrue(mEntry.isBlockable());
     }
@@ -125,6 +127,7 @@
     public void testNonBlockableEntryWhenCriticalAndChannelNotBlockable() {
         doReturn(false).when(mChannel).isBlockable();
         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
+        mEntry.setRanking(mEntry.getRanking());
 
         assertFalse(mEntry.isBlockable());
     }
@@ -164,6 +167,9 @@
         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
         doReturn(false).when(mChannel).isBlockable();
 
+        mEntry.setRanking(mEntry.getRanking());
+
+        assertFalse(mEntry.isBlockable());
         assertTrue(mEntry.isExemptFromDndVisualSuppression());
         assertFalse(mEntry.shouldSuppressAmbient());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 266f0e9..2faff0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -848,6 +848,7 @@
         mCentralSurfaces.showKeyguardImpl();
 
         // Starting a pulse should change the scrim controller to the pulsing state
+        when(mNotificationPanelViewController.isLaunchTransitionRunning()).thenReturn(true);
         when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(true);
         mCentralSurfaces.updateScrimController();
         verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any());
@@ -884,6 +885,7 @@
         mCentralSurfaces.showKeyguardImpl();
 
         // Starting a pulse should change the scrim controller to the pulsing state
+        when(mNotificationPanelViewController.isLaunchTransitionRunning()).thenReturn(true);
         when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(false);
         mCentralSurfaces.updateScrimController();
         verify(mScrimController).transitionTo(eq(ScrimState.KEYGUARD));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
index 4b557dc..31465f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
@@ -12,12 +12,12 @@
 import com.android.systemui.statusbar.policy.FlashlightController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.tuner.TunerService
-import java.util.concurrent.Executor
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
+import java.util.concurrent.Executor
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -51,5 +51,6 @@
                 null, false) as KeyguardBottomAreaView
 
         other.initFrom(mKeyguardBottomArea)
+        other.launchVoiceAssist()
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
index 046af95..79c7e55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
@@ -106,6 +106,7 @@
 import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -400,6 +401,8 @@
         when(mNotificationStackScrollLayoutController.getHeadsUpCallback())
                 .thenReturn(mHeadsUpCallback);
         when(mView.findViewById(R.id.keyguard_bottom_area)).thenReturn(mKeyguardBottomArea);
+        when(mKeyguardBottomArea.getLeftView()).thenReturn(mock(KeyguardAffordanceView.class));
+        when(mKeyguardBottomArea.getRightView()).thenReturn(mock(KeyguardAffordanceView.class));
         when(mKeyguardBottomArea.animate()).thenReturn(mock(ViewPropertyAnimator.class));
         when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame);
         when(mView.findViewById(R.id.keyguard_status_view))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index fec2123..fda80a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Intent;
+import android.os.BatteryManager;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.PowerSaveState;
@@ -196,4 +197,26 @@
         TestableLooper.get(this).processAllMessages();
         // Should not throw an exception
     }
+
+    @Test
+    public void batteryStateChanged_withChargingSourceDock_isChargingSourceDockTrue() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING);
+        intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_DOCK);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertTrue(mBatteryController.isChargingSourceDock());
+    }
+
+    @Test
+    public void batteryStateChanged_withChargingSourceNotDock_isChargingSourceDockFalse() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING);
+        intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_WIRELESS);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertFalse(mBatteryController.isChargingSourceDock());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt
new file mode 100644
index 0000000..db0029a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.content.pm.PackageManager
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.impl.CameraMetadataNative
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class FlashlightControllerImplTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var dumpManager: DumpManager
+
+    @Mock
+    private lateinit var cameraManager: CameraManager
+
+    @Mock
+    private lateinit var broadcastSender: BroadcastSender
+
+    @Mock
+    private lateinit var packageManager: PackageManager
+
+    private lateinit var fakeSettings: FakeSettings
+    private lateinit var fakeSystemClock: FakeSystemClock
+    private lateinit var backgroundExecutor: FakeExecutor
+    private lateinit var controller: FlashlightControllerImpl
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        fakeSystemClock = FakeSystemClock()
+        backgroundExecutor = FakeExecutor(fakeSystemClock)
+        fakeSettings = FakeSettings()
+
+        `when`(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH))
+                .thenReturn(true)
+
+        controller = FlashlightControllerImpl(
+                dumpManager,
+                cameraManager,
+                backgroundExecutor,
+                fakeSettings,
+                broadcastSender,
+                packageManager
+        )
+    }
+
+    @Test
+    fun testNoCameraManagerInteractionDirectlyOnConstructor() {
+        verifyZeroInteractions(cameraManager)
+    }
+
+    @Test
+    fun testCameraManagerInitAfterConstructionOnExecutor() {
+        injectCamera()
+        backgroundExecutor.runAllReady()
+
+        verify(cameraManager).registerTorchCallback(eq(backgroundExecutor), any())
+    }
+
+    @Test
+    fun testNoCallbackIfNoFlashCamera() {
+        injectCamera(flash = false)
+        backgroundExecutor.runAllReady()
+
+        verify(cameraManager, never()).registerTorchCallback(any<Executor>(), any())
+    }
+
+    @Test
+    fun testNoCallbackIfNoBackCamera() {
+        injectCamera(facing = CameraCharacteristics.LENS_FACING_FRONT)
+        backgroundExecutor.runAllReady()
+
+        verify(cameraManager, never()).registerTorchCallback(any<Executor>(), any())
+    }
+
+    @Test
+    fun testSetFlashlightInBackgroundExecutor() {
+        val id = injectCamera()
+        backgroundExecutor.runAllReady()
+
+        clearInvocations(cameraManager)
+        val enable = !controller.isEnabled
+        controller.setFlashlight(enable)
+        verifyNoMoreInteractions(cameraManager)
+
+        backgroundExecutor.runAllReady()
+        verify(cameraManager).setTorchMode(id, enable)
+    }
+
+    private fun injectCamera(
+        flash: Boolean = true,
+        facing: Int = CameraCharacteristics.LENS_FACING_BACK
+    ): String {
+        val cameraID = "ID"
+        val camera = CameraCharacteristics(CameraMetadataNative().apply {
+            set(CameraCharacteristics.FLASH_INFO_AVAILABLE, flash)
+            set(CameraCharacteristics.LENS_FACING, facing)
+        })
+        `when`(cameraManager.cameraIdList).thenReturn(arrayOf(cameraID))
+        `when`(cameraManager.getCameraCharacteristics(cameraID)).thenReturn(camera)
+        return cameraID
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
index 1e35b0f..125b362 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
@@ -159,6 +159,24 @@
         Mockito.clearInvocations(callback);
     }
 
+    // Ensure that updating a callback that is removed doesn't result in an exception due to the
+    // absence of the condition.
+    @Test
+    public void testUpdateRemovedCallback() {
+        final Monitor.Callback callback1 =
+                mock(Monitor.Callback.class);
+        final Monitor.Subscription.Token subscription1 =
+                mConditionMonitor.addSubscription(getDefaultBuilder(callback1).build());
+        ArgumentCaptor<Condition.Callback> monitorCallback =
+                ArgumentCaptor.forClass(Condition.Callback.class);
+        mExecutor.runAllReady();
+        verify(mCondition1).addCallback(monitorCallback.capture());
+        // This will execute first before the handler for onConditionChanged.
+        mConditionMonitor.removeSubscription(subscription1);
+        monitorCallback.getValue().onConditionChanged(mCondition1);
+        mExecutor.runAllReady();
+    }
+
     @Test
     public void addCallback_addFirstCallback_addCallbackToAllConditions() {
         final Monitor.Callback callback1 =
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 2aabac4..abc4937 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -104,6 +104,7 @@
 import com.android.server.SystemService;
 import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.ActivityTaskManagerInternal;
 
 import java.io.File;
 import java.io.FileDescriptor;
@@ -143,6 +144,7 @@
     private CompanionDevicePresenceMonitor mDevicePresenceMonitor;
     private CompanionApplicationController mCompanionAppController;
 
+    private final ActivityTaskManagerInternal mAtmInternal;
     private final ActivityManagerInternal mAmInternal;
     private final IAppOpsService mAppOpsManager;
     private final PowerWhitelistManager mPowerWhitelistManager;
@@ -195,6 +197,7 @@
         mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class);
         mAppOpsManager = IAppOpsService.Stub.asInterface(
                 ServiceManager.getService(Context.APP_OPS_SERVICE));
+        mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
         mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mUserManager = context.getSystemService(UserManager.class);
@@ -1199,6 +1202,9 @@
                 companionAppUids.add(uid);
             }
         }
+        if (mAtmInternal != null) {
+            mAtmInternal.setCompanionAppUids(userId, companionAppUids);
+        }
         if (mAmInternal != null) {
             // Make a copy of the set and send it to ActivityManager.
             mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 297d28d..56990ed 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -19,7 +19,7 @@
 import static com.android.internal.util.dump.DumpUtils.writeStringIfNotNull;
 
 import android.annotation.NonNull;
-import android.annotation.TestApi;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -102,11 +102,26 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keysi
+ * Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keys
  * that are authorized to connect to the ADB service itself.
+ *
+ * <p>The AdbDebuggingManager controls two files:
+ * <ol>
+ *     <li>adb_keys
+ *     <li>adb_temp_keys.xml
+ * </ol>
+ *
+ * <p>The ADB Daemon (adbd) reads <em>only</em> the adb_keys file for authorization. Public keys
+ * from registered hosts are stored in adb_keys, one entry per line.
+ *
+ * <p>AdbDebuggingManager also keeps adb_temp_keys.xml, which is used for two things
+ * <ol>
+ *     <li>Removing unused keys from the adb_keys file
+ *     <li>Managing authorized WiFi access points for ADB over WiFi
+ * </ol>
  */
 public class AdbDebuggingManager {
-    private static final String TAG = "AdbDebuggingManager";
+    private static final String TAG = AdbDebuggingManager.class.getSimpleName();
     private static final boolean DEBUG = false;
     private static final boolean MDNS_DEBUG = false;
 
@@ -118,18 +133,20 @@
     // as a subsequent connection occurs within the allowed duration.
     private static final String ADB_TEMP_KEYS_FILE = "adb_temp_keys.xml";
     private static final int BUFFER_SIZE = 65536;
+    private static final Ticker SYSTEM_TICKER = () -> System.currentTimeMillis();
 
     private final Context mContext;
     private final ContentResolver mContentResolver;
-    private final Handler mHandler;
-    private AdbDebuggingThread mThread;
+    @VisibleForTesting final AdbDebuggingHandler mHandler;
+    @Nullable private AdbDebuggingThread mThread;
     private boolean mAdbUsbEnabled = false;
     private boolean mAdbWifiEnabled = false;
     private String mFingerprints;
     // A key can be used more than once (e.g. USB, wifi), so need to keep a refcount
-    private final Map<String, Integer> mConnectedKeys;
-    private String mConfirmComponent;
-    private final File mTestUserKeyFile;
+    private final Map<String, Integer> mConnectedKeys = new HashMap<>();
+    private final String mConfirmComponent;
+    @Nullable private final File mUserKeyFile;
+    @Nullable private final File mTempKeysFile;
 
     private static final String WIFI_PERSISTENT_CONFIG_PROPERTY =
             "persist.adb.tls_server.enable";
@@ -138,37 +155,44 @@
     private static final int PAIRING_CODE_LENGTH = 6;
     private PairingThread mPairingThread = null;
     // A list of keys connected via wifi
-    private final Set<String> mWifiConnectedKeys;
+    private final Set<String> mWifiConnectedKeys = new HashSet<>();
     // The current info of the adbwifi connection.
-    private AdbConnectionInfo mAdbConnectionInfo;
+    private AdbConnectionInfo mAdbConnectionInfo = new AdbConnectionInfo();
     // Polls for a tls port property when adb wifi is enabled
     private AdbConnectionPortPoller mConnectionPortPoller;
     private final PortListenerImpl mPortListener = new PortListenerImpl();
+    private final Ticker mTicker;
 
     public AdbDebuggingManager(Context context) {
-        mHandler = new AdbDebuggingHandler(FgThread.get().getLooper());
-        mContext = context;
-        mContentResolver = mContext.getContentResolver();
-        mTestUserKeyFile = null;
-        mConnectedKeys = new HashMap<String, Integer>();
-        mWifiConnectedKeys = new HashSet<String>();
-        mAdbConnectionInfo = new AdbConnectionInfo();
+        this(
+                context,
+                /* confirmComponent= */ null,
+                getAdbFile(ADB_KEYS_FILE),
+                getAdbFile(ADB_TEMP_KEYS_FILE),
+                /* adbDebuggingThread= */ null,
+                SYSTEM_TICKER);
     }
 
     /**
      * Constructor that accepts the component to be invoked to confirm if the user wants to allow
      * an adb connection from the key.
      */
-    @TestApi
-    protected AdbDebuggingManager(Context context, String confirmComponent, File testUserKeyFile) {
-        mHandler = new AdbDebuggingHandler(FgThread.get().getLooper());
+    @VisibleForTesting
+    AdbDebuggingManager(
+            Context context,
+            String confirmComponent,
+            File testUserKeyFile,
+            File tempKeysFile,
+            AdbDebuggingThread adbDebuggingThread,
+            Ticker ticker) {
         mContext = context;
         mContentResolver = mContext.getContentResolver();
         mConfirmComponent = confirmComponent;
-        mTestUserKeyFile = testUserKeyFile;
-        mConnectedKeys = new HashMap<String, Integer>();
-        mWifiConnectedKeys = new HashSet<String>();
-        mAdbConnectionInfo = new AdbConnectionInfo();
+        mUserKeyFile = testUserKeyFile;
+        mTempKeysFile = tempKeysFile;
+        mThread = adbDebuggingThread;
+        mTicker = ticker;
+        mHandler = new AdbDebuggingHandler(FgThread.get().getLooper(), mThread);
     }
 
     static void sendBroadcastWithDebugPermission(@NonNull Context context, @NonNull Intent intent,
@@ -189,8 +213,7 @@
         // consisting of only letters, digits, and hyphens, must begin and end
         // with a letter or digit, must not contain consecutive hyphens, and
         // must contain at least one letter.
-        @VisibleForTesting
-        static final String SERVICE_PROTOCOL = "adb-tls-pairing";
+        @VisibleForTesting static final String SERVICE_PROTOCOL = "adb-tls-pairing";
         private final String mServiceType = String.format("_%s._tcp.", SERVICE_PROTOCOL);
         private int mPort;
 
@@ -352,16 +375,24 @@
         }
     }
 
-    class AdbDebuggingThread extends Thread {
+    @VisibleForTesting
+    static class AdbDebuggingThread extends Thread {
         private boolean mStopped;
         private LocalSocket mSocket;
         private OutputStream mOutputStream;
         private InputStream mInputStream;
+        private Handler mHandler;
 
+        @VisibleForTesting
         AdbDebuggingThread() {
             super(TAG);
         }
 
+        @VisibleForTesting
+        void setHandler(Handler handler) {
+            mHandler = handler;
+        }
+
         @Override
         public void run() {
             if (DEBUG) Slog.d(TAG, "Entering thread");
@@ -536,7 +567,7 @@
         }
     }
 
-    class AdbConnectionInfo {
+    private static class AdbConnectionInfo {
         private String mBssid;
         private String mSsid;
         private int mPort;
@@ -743,11 +774,14 @@
         // Notification when adbd socket is disconnected.
         static final int MSG_ADBD_SOCKET_DISCONNECTED = 27;
 
+        // === Messages from other parts of the system
+        private static final int MESSAGE_KEY_FILES_UPDATED = 28;
+
         // === Messages we can send to adbd ===========
         static final String MSG_DISCONNECT_DEVICE = "DD";
         static final String MSG_DISABLE_ADBDWIFI = "DA";
 
-        private AdbKeyStore mAdbKeyStore;
+        @Nullable @VisibleForTesting AdbKeyStore mAdbKeyStore;
 
         // Usb, Wi-Fi transports can be enabled together or separately, so don't break the framework
         // connection unless all transport types are disconnected.
@@ -762,19 +796,19 @@
             }
         };
 
-        AdbDebuggingHandler(Looper looper) {
-            super(looper);
-        }
-
-        /**
-         * Constructor that accepts the AdbDebuggingThread to which responses should be sent
-         * and the AdbKeyStore to be used to store the temporary grants.
-         */
-        @TestApi
-        AdbDebuggingHandler(Looper looper, AdbDebuggingThread thread, AdbKeyStore adbKeyStore) {
+        /** Constructor that accepts the AdbDebuggingThread to which responses should be sent. */
+        @VisibleForTesting
+        AdbDebuggingHandler(Looper looper, AdbDebuggingThread thread) {
             super(looper);
             mThread = thread;
-            mAdbKeyStore = adbKeyStore;
+        }
+
+        /** Initialize the AdbKeyStore so tests can grab mAdbKeyStore immediately. */
+        @VisibleForTesting
+        void initKeyStore() {
+            if (mAdbKeyStore == null) {
+                mAdbKeyStore = new AdbKeyStore();
+            }
         }
 
         // Show when at least one device is connected.
@@ -805,6 +839,7 @@
 
             registerForAuthTimeChanges();
             mThread = new AdbDebuggingThread();
+            mThread.setHandler(mHandler);
             mThread.start();
 
             mAdbKeyStore.updateKeyStore();
@@ -825,8 +860,7 @@
 
             if (!mConnectedKeys.isEmpty()) {
                 for (Map.Entry<String, Integer> entry : mConnectedKeys.entrySet()) {
-                    mAdbKeyStore.setLastConnectionTime(entry.getKey(),
-                            System.currentTimeMillis());
+                    mAdbKeyStore.setLastConnectionTime(entry.getKey(), mTicker.currentTimeMillis());
                 }
                 sendPersistKeyStoreMessage();
                 mConnectedKeys.clear();
@@ -836,9 +870,7 @@
         }
 
         public void handleMessage(Message msg) {
-            if (mAdbKeyStore == null) {
-                mAdbKeyStore = new AdbKeyStore();
-            }
+            initKeyStore();
 
             switch (msg.what) {
                 case MESSAGE_ADB_ENABLED:
@@ -873,7 +905,7 @@
                             if (!mConnectedKeys.containsKey(key)) {
                                 mConnectedKeys.put(key, 1);
                             }
-                            mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                            mAdbKeyStore.setLastConnectionTime(key, mTicker.currentTimeMillis());
                             sendPersistKeyStoreMessage();
                             scheduleJobToUpdateAdbKeyStore();
                         }
@@ -920,9 +952,7 @@
                     mConnectedKeys.clear();
                     // If the key store has not yet been instantiated then do so now; this avoids
                     // the unnecessary creation of the key store when adb is not enabled.
-                    if (mAdbKeyStore == null) {
-                        mAdbKeyStore = new AdbKeyStore();
-                    }
+                    initKeyStore();
                     mWifiConnectedKeys.clear();
                     mAdbKeyStore.deleteKeyStore();
                     cancelJobToUpdateAdbKeyStore();
@@ -937,7 +967,8 @@
                             alwaysAllow = true;
                             int refcount = mConnectedKeys.get(key) - 1;
                             if (refcount == 0) {
-                                mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                                mAdbKeyStore.setLastConnectionTime(
+                                        key, mTicker.currentTimeMillis());
                                 sendPersistKeyStoreMessage();
                                 scheduleJobToUpdateAdbKeyStore();
                                 mConnectedKeys.remove(key);
@@ -963,7 +994,7 @@
                     if (!mConnectedKeys.isEmpty()) {
                         for (Map.Entry<String, Integer> entry : mConnectedKeys.entrySet()) {
                             mAdbKeyStore.setLastConnectionTime(entry.getKey(),
-                                    System.currentTimeMillis());
+                                    mTicker.currentTimeMillis());
                         }
                         sendPersistKeyStoreMessage();
                         scheduleJobToUpdateAdbKeyStore();
@@ -984,7 +1015,7 @@
                         } else {
                             mConnectedKeys.put(key, mConnectedKeys.get(key) + 1);
                         }
-                        mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                        mAdbKeyStore.setLastConnectionTime(key, mTicker.currentTimeMillis());
                         sendPersistKeyStoreMessage();
                         scheduleJobToUpdateAdbKeyStore();
                         logAdbConnectionChanged(key, AdbProtoEnums.AUTOMATICALLY_ALLOWED, true);
@@ -1206,6 +1237,10 @@
                     }
                     break;
                 }
+                case MESSAGE_KEY_FILES_UPDATED: {
+                    mAdbKeyStore.reloadKeyMap();
+                    break;
+                }
             }
         }
 
@@ -1377,8 +1412,7 @@
                 AdbDebuggingManager.sendBroadcastWithDebugPermission(mContext, intent,
                         UserHandle.ALL);
                 // Add the key into the keystore
-                mAdbKeyStore.setLastConnectionTime(publicKey,
-                        System.currentTimeMillis());
+                mAdbKeyStore.setLastConnectionTime(publicKey, mTicker.currentTimeMillis());
                 sendPersistKeyStoreMessage();
                 scheduleJobToUpdateAdbKeyStore();
             }
@@ -1449,19 +1483,13 @@
         extras.add(new AbstractMap.SimpleEntry<String, String>("ssid", ssid));
         extras.add(new AbstractMap.SimpleEntry<String, String>("bssid", bssid));
         int currentUserId = ActivityManager.getCurrentUser();
-        UserInfo userInfo = UserManager.get(mContext).getUserInfo(currentUserId);
-        String componentString;
-        if (userInfo.isAdmin()) {
-            componentString = Resources.getSystem().getString(
-                    com.android.internal.R.string.config_customAdbWifiNetworkConfirmationComponent);
-        } else {
-            componentString = Resources.getSystem().getString(
-                    com.android.internal.R.string.config_customAdbWifiNetworkConfirmationComponent);
-        }
+        String componentString =
+                Resources.getSystem().getString(
+                        R.string.config_customAdbWifiNetworkConfirmationComponent);
         ComponentName componentName = ComponentName.unflattenFromString(componentString);
+        UserInfo userInfo = UserManager.get(mContext).getUserInfo(currentUserId);
         if (startConfirmationActivity(componentName, userInfo.getUserHandle(), extras)
-                || startConfirmationService(componentName, userInfo.getUserHandle(),
-                        extras)) {
+                || startConfirmationService(componentName, userInfo.getUserHandle(), extras)) {
             return;
         }
         Slog.e(TAG, "Unable to start customAdbWifiNetworkConfirmation[SecondaryUser]Component "
@@ -1543,7 +1571,7 @@
     /**
      * Returns a new File with the specified name in the adb directory.
      */
-    private File getAdbFile(String fileName) {
+    private static File getAdbFile(String fileName) {
         File dataDir = Environment.getDataDirectory();
         File adbDir = new File(dataDir, ADB_DIRECTORY);
 
@@ -1556,66 +1584,38 @@
     }
 
     File getAdbTempKeysFile() {
-        return getAdbFile(ADB_TEMP_KEYS_FILE);
+        return mTempKeysFile;
     }
 
     File getUserKeyFile() {
-        return mTestUserKeyFile == null ? getAdbFile(ADB_KEYS_FILE) : mTestUserKeyFile;
-    }
-
-    private void writeKey(String key) {
-        try {
-            File keyFile = getUserKeyFile();
-
-            if (keyFile == null) {
-                return;
-            }
-
-            FileOutputStream fo = new FileOutputStream(keyFile, true);
-            fo.write(key.getBytes());
-            fo.write('\n');
-            fo.close();
-
-            FileUtils.setPermissions(keyFile.toString(),
-                    FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
-        } catch (IOException ex) {
-            Slog.e(TAG, "Error writing key:" + ex);
-        }
+        return mUserKeyFile;
     }
 
     private void writeKeys(Iterable<String> keys) {
-        AtomicFile atomicKeyFile = null;
+        if (mUserKeyFile == null) {
+            return;
+        }
+
+        AtomicFile atomicKeyFile = new AtomicFile(mUserKeyFile);
+        // Note: Do not use a try-with-resources with the FileOutputStream, because AtomicFile
+        // requires that it's cleaned up with AtomicFile.failWrite();
         FileOutputStream fo = null;
         try {
-            File keyFile = getUserKeyFile();
-
-            if (keyFile == null) {
-                return;
-            }
-
-            atomicKeyFile = new AtomicFile(keyFile);
             fo = atomicKeyFile.startWrite();
             for (String key : keys) {
                 fo.write(key.getBytes());
                 fo.write('\n');
             }
             atomicKeyFile.finishWrite(fo);
-
-            FileUtils.setPermissions(keyFile.toString(),
-                    FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
         } catch (IOException ex) {
             Slog.e(TAG, "Error writing keys: " + ex);
-            if (atomicKeyFile != null) {
-                atomicKeyFile.failWrite(fo);
-            }
+            atomicKeyFile.failWrite(fo);
+            return;
         }
-    }
 
-    private void deleteKeyFile() {
-        File keyFile = getUserKeyFile();
-        if (keyFile != null) {
-            keyFile.delete();
-        }
+        FileUtils.setPermissions(
+                mUserKeyFile.toString(),
+                FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
     }
 
     /**
@@ -1745,6 +1745,13 @@
     }
 
     /**
+     * Notify that they key files were updated so the AdbKeyManager reloads the keys.
+     */
+    public void notifyKeyFilesUpdated() {
+        mHandler.sendEmptyMessage(AdbDebuggingHandler.MESSAGE_KEY_FILES_UPDATED);
+    }
+
+    /**
      * Sends a message to the handler to persist the keystore.
      */
     private void sendPersistKeyStoreMessage() {
@@ -1778,7 +1785,7 @@
 
         try {
             dump.write("keystore", AdbDebuggingManagerProto.KEYSTORE,
-                    FileUtils.readTextFile(getAdbTempKeysFile(), 0, null));
+                    FileUtils.readTextFile(mTempKeysFile, 0, null));
         } catch (IOException e) {
             Slog.i(TAG, "Cannot read keystore: ", e);
         }
@@ -1792,12 +1799,12 @@
      * ADB_ALLOWED_CONNECTION_TIME setting.
      */
     class AdbKeyStore {
-        private Map<String, Long> mKeyMap;
-        private Set<String> mSystemKeys;
-        private File mKeyFile;
         private AtomicFile mAtomicKeyFile;
 
-        private List<String> mTrustedNetworks;
+        private final Set<String> mSystemKeys;
+        private final Map<String, Long> mKeyMap = new HashMap<>();
+        private final List<String> mTrustedNetworks = new ArrayList<>();
+
         private static final int KEYSTORE_VERSION = 1;
         private static final int MAX_SUPPORTED_KEYSTORE_VERSION = 1;
         private static final String XML_KEYSTORE_START_TAG = "keyStore";
@@ -1819,26 +1826,22 @@
         public static final long NO_PREVIOUS_CONNECTION = 0;
 
         /**
-         * Constructor that uses the default location for the persistent adb keystore.
+         * Create an AdbKeyStore instance.
+         *
+         * <p>Upon creation, we parse {@link #mTempKeysFile} to determine authorized WiFi APs and
+         * retrieve the map of stored ADB keys and their last connected times. After that, we read
+         * the {@link #mUserKeyFile}, and any keys that exist in that file that do not exist in the
+         * map are added to the map (for backwards compatibility).
          */
         AdbKeyStore() {
-            init();
-        }
-
-        /**
-         * Constructor that uses the specified file as the location for the persistent adb keystore.
-         */
-        AdbKeyStore(File keyFile) {
-            mKeyFile = keyFile;
-            init();
-        }
-
-        private void init() {
             initKeyFile();
-            mKeyMap = getKeyMap();
-            mTrustedNetworks = getTrustedNetworks();
+            readTempKeysFile();
             mSystemKeys = getSystemKeysFromFile(SYSTEM_KEY_FILE);
-            addUserKeysToKeyStore();
+            addExistingUserKeysToKeyStore();
+        }
+
+        public void reloadKeyMap() {
+            readTempKeysFile();
         }
 
         public void addTrustedNetwork(String bssid) {
@@ -1877,7 +1880,6 @@
         public void removeKey(String key) {
             if (mKeyMap.containsKey(key)) {
                 mKeyMap.remove(key);
-                writeKeys(mKeyMap.keySet());
                 sendPersistKeyStoreMessage();
             }
         }
@@ -1886,12 +1888,9 @@
          * Initializes the key file that will be used to persist the adb grants.
          */
         private void initKeyFile() {
-            if (mKeyFile == null) {
-                mKeyFile = getAdbTempKeysFile();
-            }
-            // getAdbTempKeysFile can return null if the adb file cannot be obtained
-            if (mKeyFile != null) {
-                mAtomicKeyFile = new AtomicFile(mKeyFile);
+            // mTempKeysFile can be null if the adb file cannot be obtained
+            if (mTempKeysFile != null) {
+                mAtomicKeyFile = new AtomicFile(mTempKeysFile);
             }
         }
 
@@ -1932,201 +1931,108 @@
         }
 
         /**
-         * Returns the key map with the keys and last connection times from the key file.
+         * Update the key map and the trusted networks list with values parsed from the temp keys
+         * file.
          */
-        private Map<String, Long> getKeyMap() {
-            Map<String, Long> keyMap = new HashMap<String, Long>();
-            // if the AtomicFile could not be instantiated before attempt again; if it still fails
-            // return an empty key map.
+        private void readTempKeysFile() {
+            mKeyMap.clear();
+            mTrustedNetworks.clear();
             if (mAtomicKeyFile == null) {
                 initKeyFile();
                 if (mAtomicKeyFile == null) {
-                    Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading");
-                    return keyMap;
+                    Slog.e(
+                            TAG,
+                            "Unable to obtain the key file, " + mTempKeysFile + ", for reading");
+                    return;
                 }
             }
             if (!mAtomicKeyFile.exists()) {
-                return keyMap;
+                return;
             }
             try (FileInputStream keyStream = mAtomicKeyFile.openRead()) {
-                TypedXmlPullParser parser = Xml.resolvePullParser(keyStream);
-                // Check for supported keystore version.
-                XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG);
-                if (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null || !XML_KEYSTORE_START_TAG.equals(tagName)) {
-                        Slog.e(TAG, "Expected " + XML_KEYSTORE_START_TAG + ", but got tag="
-                                + tagName);
-                        return keyMap;
-                    }
+                TypedXmlPullParser parser;
+                try {
+                    parser = Xml.resolvePullParser(keyStream);
+                    XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG);
+
                     int keystoreVersion = parser.getAttributeInt(null, XML_ATTRIBUTE_VERSION);
                     if (keystoreVersion > MAX_SUPPORTED_KEYSTORE_VERSION) {
                         Slog.e(TAG, "Keystore version=" + keystoreVersion
                                 + " not supported (max_supported="
                                 + MAX_SUPPORTED_KEYSTORE_VERSION + ")");
-                        return keyMap;
+                        return;
                     }
+                } catch (XmlPullParserException e) {
+                    // This could be because the XML document doesn't start with
+                    // XML_KEYSTORE_START_TAG. Try again, instead just starting the document with
+                    // the adbKey tag (the old format).
+                    parser = Xml.resolvePullParser(keyStream);
                 }
-                while (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null) {
-                        break;
-                    } else if (!tagName.equals(XML_TAG_ADB_KEY)) {
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY);
-                    long connectionTime;
-                    try {
-                        connectionTime = parser.getAttributeLong(null,
-                                XML_ATTRIBUTE_LAST_CONNECTION);
-                    } catch (XmlPullParserException e) {
-                        Slog.e(TAG,
-                                "Caught a NumberFormatException parsing the last connection time: "
-                                        + e);
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    keyMap.put(key, connectionTime);
-                }
+                readKeyStoreContents(parser);
             } catch (IOException e) {
                 Slog.e(TAG, "Caught an IOException parsing the XML key file: ", e);
             } catch (XmlPullParserException e) {
-                Slog.w(TAG, "Caught XmlPullParserException parsing the XML key file: ", e);
-                // The file could be written in a format prior to introducing keystore tag.
-                return getKeyMapBeforeKeystoreVersion();
+                Slog.e(TAG, "Caught XmlPullParserException parsing the XML key file: ", e);
             }
-            return keyMap;
         }
 
-
-        /**
-         * Returns the key map with the keys and last connection times from the key file.
-         * This implementation was prior to adding the XML_KEYSTORE_START_TAG.
-         */
-        private Map<String, Long> getKeyMapBeforeKeystoreVersion() {
-            Map<String, Long> keyMap = new HashMap<String, Long>();
-            // if the AtomicFile could not be instantiated before attempt again; if it still fails
-            // return an empty key map.
-            if (mAtomicKeyFile == null) {
-                initKeyFile();
-                if (mAtomicKeyFile == null) {
-                    Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading");
-                    return keyMap;
+        private void readKeyStoreContents(TypedXmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            // This parser is very forgiving. For backwards-compatibility, we simply iterate through
+            // all the tags in the file, skipping over anything that's not an <adbKey> tag or a
+            // <wifiAP> tag. Invalid tags (such as ones that don't have a valid "lastConnection"
+            // attribute) are simply ignored.
+            while ((parser.next()) != XmlPullParser.END_DOCUMENT) {
+                String tagName = parser.getName();
+                if (XML_TAG_ADB_KEY.equals(tagName)) {
+                    addAdbKeyToKeyMap(parser);
+                } else if (XML_TAG_WIFI_ACCESS_POINT.equals(tagName)) {
+                    addTrustedNetworkToTrustedNetworks(parser);
+                } else {
+                    Slog.w(TAG, "Ignoring tag '" + tagName + "'. Not recognized.");
                 }
+                XmlUtils.skipCurrentTag(parser);
             }
-            if (!mAtomicKeyFile.exists()) {
-                return keyMap;
-            }
-            try (FileInputStream keyStream = mAtomicKeyFile.openRead()) {
-                TypedXmlPullParser parser = Xml.resolvePullParser(keyStream);
-                XmlUtils.beginDocument(parser, XML_TAG_ADB_KEY);
-                while (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null) {
-                        break;
-                    } else if (!tagName.equals(XML_TAG_ADB_KEY)) {
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY);
-                    long connectionTime;
-                    try {
-                        connectionTime = parser.getAttributeLong(null,
-                                XML_ATTRIBUTE_LAST_CONNECTION);
-                    } catch (XmlPullParserException e) {
-                        Slog.e(TAG,
-                                "Caught a NumberFormatException parsing the last connection time: "
-                                        + e);
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    keyMap.put(key, connectionTime);
-                }
-            } catch (IOException | XmlPullParserException e) {
-                Slog.e(TAG, "Caught an exception parsing the XML key file: ", e);
-            }
-            return keyMap;
         }
 
-        /**
-         * Returns the map of trusted networks from the keystore file.
-         *
-         * This was implemented in keystore version 1.
-         */
-        private List<String> getTrustedNetworks() {
-            List<String> trustedNetworks = new ArrayList<String>();
-            // if the AtomicFile could not be instantiated before attempt again; if it still fails
-            // return an empty key map.
-            if (mAtomicKeyFile == null) {
-                initKeyFile();
-                if (mAtomicKeyFile == null) {
-                    Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading");
-                    return trustedNetworks;
-                }
+        private void addAdbKeyToKeyMap(TypedXmlPullParser parser) {
+            String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY);
+            try {
+                long connectionTime =
+                        parser.getAttributeLong(null, XML_ATTRIBUTE_LAST_CONNECTION);
+                mKeyMap.put(key, connectionTime);
+            } catch (XmlPullParserException e) {
+                Slog.e(TAG, "Error reading adbKey attributes", e);
             }
-            if (!mAtomicKeyFile.exists()) {
-                return trustedNetworks;
-            }
-            try (FileInputStream keyStream = mAtomicKeyFile.openRead()) {
-                TypedXmlPullParser parser = Xml.resolvePullParser(keyStream);
-                // Check for supported keystore version.
-                XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG);
-                if (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null || !XML_KEYSTORE_START_TAG.equals(tagName)) {
-                        Slog.e(TAG, "Expected " + XML_KEYSTORE_START_TAG + ", but got tag="
-                                + tagName);
-                        return trustedNetworks;
-                    }
-                    int keystoreVersion = parser.getAttributeInt(null, XML_ATTRIBUTE_VERSION);
-                    if (keystoreVersion > MAX_SUPPORTED_KEYSTORE_VERSION) {
-                        Slog.e(TAG, "Keystore version=" + keystoreVersion
-                                + " not supported (max_supported="
-                                + MAX_SUPPORTED_KEYSTORE_VERSION);
-                        return trustedNetworks;
-                    }
-                }
-                while (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null) {
-                        break;
-                    } else if (!tagName.equals(XML_TAG_WIFI_ACCESS_POINT)) {
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    String bssid = parser.getAttributeValue(null, XML_ATTRIBUTE_WIFI_BSSID);
-                    trustedNetworks.add(bssid);
-                }
-            } catch (IOException | XmlPullParserException | NumberFormatException e) {
-                Slog.e(TAG, "Caught an exception parsing the XML key file: ", e);
-            }
-            return trustedNetworks;
+        }
+
+        private void addTrustedNetworkToTrustedNetworks(TypedXmlPullParser parser) {
+            String bssid = parser.getAttributeValue(null, XML_ATTRIBUTE_WIFI_BSSID);
+            mTrustedNetworks.add(bssid);
         }
 
         /**
          * Updates the keystore with keys that were previously set to be always allowed before the
          * connection time of keys was tracked.
          */
-        private void addUserKeysToKeyStore() {
-            File userKeyFile = getUserKeyFile();
+        private void addExistingUserKeysToKeyStore() {
+            if (mUserKeyFile == null || !mUserKeyFile.exists()) {
+                return;
+            }
             boolean mapUpdated = false;
-            if (userKeyFile != null && userKeyFile.exists()) {
-                try (BufferedReader in = new BufferedReader(new FileReader(userKeyFile))) {
-                    long time = System.currentTimeMillis();
-                    String key;
-                    while ((key = in.readLine()) != null) {
-                        // if the keystore does not contain the key from the user key file then add
-                        // it to the Map with the current system time to prevent it from expiring
-                        // immediately if the user is actively using this key.
-                        if (!mKeyMap.containsKey(key)) {
-                            mKeyMap.put(key, time);
-                            mapUpdated = true;
-                        }
+            try (BufferedReader in = new BufferedReader(new FileReader(mUserKeyFile))) {
+                String key;
+                while ((key = in.readLine()) != null) {
+                    // if the keystore does not contain the key from the user key file then add
+                    // it to the Map with the current system time to prevent it from expiring
+                    // immediately if the user is actively using this key.
+                    if (!mKeyMap.containsKey(key)) {
+                        mKeyMap.put(key, mTicker.currentTimeMillis());
+                        mapUpdated = true;
                     }
-                } catch (IOException e) {
-                    Slog.e(TAG, "Caught an exception reading " + userKeyFile + ": " + e);
                 }
+            } catch (IOException e) {
+                Slog.e(TAG, "Caught an exception reading " + mUserKeyFile + ": " + e);
             }
             if (mapUpdated) {
                 sendPersistKeyStoreMessage();
@@ -2147,7 +2053,9 @@
             if (mAtomicKeyFile == null) {
                 initKeyFile();
                 if (mAtomicKeyFile == null) {
-                    Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for writing");
+                    Slog.e(
+                            TAG,
+                            "Unable to obtain the key file, " + mTempKeysFile + ", for writing");
                     return;
                 }
             }
@@ -2178,17 +2086,21 @@
                 Slog.e(TAG, "Caught an exception writing the key map: ", e);
                 mAtomicKeyFile.failWrite(keyStream);
             }
+            writeKeys(mKeyMap.keySet());
         }
 
         private boolean filterOutOldKeys() {
-            boolean keysDeleted = false;
             long allowedTime = getAllowedConnectionTime();
-            long systemTime = System.currentTimeMillis();
+            if (allowedTime == 0) {
+                return false;
+            }
+            boolean keysDeleted = false;
+            long systemTime = mTicker.currentTimeMillis();
             Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
             while (keyMapIterator.hasNext()) {
                 Map.Entry<String, Long> keyEntry = keyMapIterator.next();
                 long connectionTime = keyEntry.getValue();
-                if (allowedTime != 0 && systemTime > (connectionTime + allowedTime)) {
+                if (systemTime > (connectionTime + allowedTime)) {
                     keyMapIterator.remove();
                     keysDeleted = true;
                 }
@@ -2212,7 +2124,7 @@
             if (allowedTime == 0) {
                 return minExpiration;
             }
-            long systemTime = System.currentTimeMillis();
+            long systemTime = mTicker.currentTimeMillis();
             Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
             while (keyMapIterator.hasNext()) {
                 Map.Entry<String, Long> keyEntry = keyMapIterator.next();
@@ -2233,7 +2145,9 @@
         public void deleteKeyStore() {
             mKeyMap.clear();
             mTrustedNetworks.clear();
-            deleteKeyFile();
+            if (mUserKeyFile != null) {
+                mUserKeyFile.delete();
+            }
             if (mAtomicKeyFile == null) {
                 return;
             }
@@ -2260,7 +2174,8 @@
          * is set to true the time will be set even if it is older than the previously written
          * connection time.
          */
-        public void setLastConnectionTime(String key, long connectionTime, boolean force) {
+        @VisibleForTesting
+        void setLastConnectionTime(String key, long connectionTime, boolean force) {
             // Do not set the connection time to a value that is earlier than what was previously
             // stored as the last connection time unless force is set.
             if (mKeyMap.containsKey(key) && mKeyMap.get(key) >= connectionTime && !force) {
@@ -2271,11 +2186,6 @@
             if (mSystemKeys.contains(key)) {
                 return;
             }
-            // if this is the first time the key is being added then write it to the key file as
-            // well.
-            if (!mKeyMap.containsKey(key)) {
-                writeKey(key);
-            }
             mKeyMap.put(key, connectionTime);
         }
 
@@ -2307,12 +2217,8 @@
             long allowedConnectionTime = getAllowedConnectionTime();
             // if the allowed connection time is 0 then revert to the previous behavior of always
             // allowing previously granted adb grants.
-            if (allowedConnectionTime == 0 || (System.currentTimeMillis() < (lastConnectionTime
-                    + allowedConnectionTime))) {
-                return true;
-            } else {
-                return false;
-            }
+            return allowedConnectionTime == 0
+                    || (mTicker.currentTimeMillis() < (lastConnectionTime + allowedConnectionTime));
         }
 
         /**
@@ -2324,4 +2230,15 @@
             return mTrustedNetworks.contains(bssid);
         }
     }
+
+    /**
+     * A Guava-like interface for getting the current system time.
+     *
+     * This allows us to swap a fake ticker in for testing to reduce "Thread.sleep()" calls and test
+     * for exact expected times instead of random ones.
+     */
+    @VisibleForTesting
+    interface Ticker {
+        long currentTimeMillis();
+    }
 }
diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java
index 5d0c732..55d8dba 100644
--- a/services/core/java/com/android/server/adb/AdbService.java
+++ b/services/core/java/com/android/server/adb/AdbService.java
@@ -152,6 +152,14 @@
         }
 
         @Override
+        public void notifyKeyFilesUpdated() {
+            if (mDebuggingManager == null) {
+                return;
+            }
+            mDebuggingManager.notifyKeyFilesUpdated();
+        }
+
+        @Override
         public void startAdbdForTransport(byte transportType) {
             FgThread.getHandler().sendMessage(obtainMessage(
                     AdbService::setAdbdEnabledForTransport, AdbService.this, true, transportType));
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 43d77ab..0040ea9 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -365,6 +365,8 @@
     private static final int MSG_REMOVE_ASSISTANT_SERVICE_UID = 45;
     private static final int MSG_UPDATE_ACTIVE_ASSISTANT_SERVICE_UID = 46;
     private static final int MSG_DISPATCH_DEVICE_VOLUME_BEHAVIOR = 47;
+    private static final int MSG_ROTATION_UPDATE = 48;
+    private static final int MSG_FOLD_UPDATE = 49;
 
     // start of messages handled under wakelock
     //   these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
@@ -1251,7 +1253,9 @@
 
         intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
         if (mMonitorRotation) {
-            RotationHelper.init(mContext, mAudioHandler);
+            RotationHelper.init(mContext, mAudioHandler,
+                    rotationParam -> onRotationUpdate(rotationParam),
+                    foldParam -> onFoldUpdate(foldParam));
         }
 
         intentFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
@@ -1398,6 +1402,20 @@
     }
 
     //-----------------------------------------------------------------
+    // rotation/fold updates coming from RotationHelper
+    void onRotationUpdate(String rotationParameter) {
+        // use REPLACE as only the last rotation matters
+        sendMsg(mAudioHandler, MSG_ROTATION_UPDATE, SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0,
+                /*obj*/ rotationParameter, /*delay*/ 0);
+    }
+
+    void onFoldUpdate(String foldParameter) {
+        // use REPLACE as only the last fold state matters
+        sendMsg(mAudioHandler, MSG_FOLD_UPDATE, SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0,
+                /*obj*/ foldParameter, /*delay*/ 0);
+    }
+
+    //-----------------------------------------------------------------
     // monitoring requests for volume range initialization
     @Override // AudioSystemAdapter.OnVolRangeInitRequestListener
     public void onVolumeRangeInitRequestFromNative() {
@@ -8327,6 +8345,16 @@
                 case MSG_DISPATCH_DEVICE_VOLUME_BEHAVIOR:
                     dispatchDeviceVolumeBehavior((AudioDeviceAttributes) msg.obj, msg.arg1);
                     break;
+
+                case MSG_ROTATION_UPDATE:
+                    // rotation parameter format: "rotation=x" where x is one of 0, 90, 180, 270
+                    mAudioSystem.setParameters((String) msg.obj);
+                    break;
+
+                case MSG_FOLD_UPDATE:
+                    // fold parameter format: "device_folded=x" where x is one of on, off
+                    mAudioSystem.setParameters((String) msg.obj);
+                    break;
             }
         }
     }
diff --git a/services/core/java/com/android/server/audio/RotationHelper.java b/services/core/java/com/android/server/audio/RotationHelper.java
index eb8387f..5cdf58b 100644
--- a/services/core/java/com/android/server/audio/RotationHelper.java
+++ b/services/core/java/com/android/server/audio/RotationHelper.java
@@ -21,13 +21,14 @@
 import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerGlobal;
-import android.media.AudioSystem;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.util.Log;
 import android.view.Display;
 import android.view.Surface;
 
+import java.util.function.Consumer;
+
 /**
  * Class to handle device rotation events for AudioService, and forward device rotation
  * and folded state to the audio HALs through AudioSystem.
@@ -53,6 +54,10 @@
 
     private static AudioDisplayListener sDisplayListener;
     private static FoldStateListener sFoldStateListener;
+    /** callback to send rotation updates to AudioSystem */
+    private static Consumer<String> sRotationUpdateCb;
+    /** callback to send folded state updates to AudioSystem */
+    private static Consumer<String> sFoldUpdateCb;
 
     private static final Object sRotationLock = new Object();
     private static final Object sFoldStateLock = new Object();
@@ -67,13 +72,16 @@
      * - sDisplayListener != null
      * - sContext != null
      */
-    static void init(Context context, Handler handler) {
+    static void init(Context context, Handler handler,
+            Consumer<String> rotationUpdateCb, Consumer<String> foldUpdateCb) {
         if (context == null) {
             throw new IllegalArgumentException("Invalid null context");
         }
         sContext = context;
         sHandler = handler;
         sDisplayListener = new AudioDisplayListener();
+        sRotationUpdateCb = rotationUpdateCb;
+        sFoldUpdateCb = foldUpdateCb;
         enable();
     }
 
@@ -115,21 +123,26 @@
         if (DEBUG_ROTATION) {
             Log.i(TAG, "publishing device rotation =" + rotation + " (x90deg)");
         }
+        String rotationParam;
         switch (rotation) {
             case Surface.ROTATION_0:
-                AudioSystem.setParameters("rotation=0");
+                rotationParam = "rotation=0";
                 break;
             case Surface.ROTATION_90:
-                AudioSystem.setParameters("rotation=90");
+                rotationParam = "rotation=90";
                 break;
             case Surface.ROTATION_180:
-                AudioSystem.setParameters("rotation=180");
+                rotationParam = "rotation=180";
                 break;
             case Surface.ROTATION_270:
-                AudioSystem.setParameters("rotation=270");
+                rotationParam = "rotation=270";
                 break;
             default:
                 Log.e(TAG, "Unknown device rotation");
+                rotationParam = null;
+        }
+        if (rotationParam != null) {
+            sRotationUpdateCb.accept(rotationParam);
         }
     }
 
@@ -140,11 +153,13 @@
         synchronized (sFoldStateLock) {
             if (sDeviceFold != newFolded) {
                 sDeviceFold = newFolded;
+                String foldParam;
                 if (newFolded) {
-                    AudioSystem.setParameters("device_folded=on");
+                    foldParam = "device_folded=on";
                 } else {
-                    AudioSystem.setParameters("device_folded=off");
+                    foldParam = "device_folded=off";
                 }
+                sFoldUpdateCb.accept(foldParam);
             }
         }
     }
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 5b26672..dd44af1 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -280,18 +280,13 @@
             }
             // for both transaural / binaural, we are not forcing enablement as the init() method
             // could have been called another time after boot in case of audioserver restart
-            if (mTransauralSupported) {
-                // not force-enabling as this device might already be in the device list
-                addCompatibleAudioDevice(
-                        new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
-                                false /*forceEnable*/);
-            }
-            if (mBinauralSupported) {
-                // not force-enabling as this device might already be in the device list
-                addCompatibleAudioDevice(
-                        new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""),
-                                false /*forceEnable*/);
-            }
+            addCompatibleAudioDevice(
+                    new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
+                            false /*forceEnable*/);
+            // not force-enabling as this device might already be in the device list
+            addCompatibleAudioDevice(
+                    new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""),
+                            false /*forceEnable*/);
         } catch (RemoteException e) {
             resetCapabilities();
         } finally {
@@ -497,10 +492,9 @@
     synchronized @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() {
         // build unionOf(mCompatibleAudioDevices, mEnabledDevice) - mDisabledAudioDevices
         ArrayList<AudioDeviceAttributes> compatList = new ArrayList<>();
-        for (SADeviceState dev : mSADevices) {
-            if (dev.mEnabled) {
-                compatList.add(new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
-                        dev.mDeviceType, dev.mDeviceAddress == null ? "" : dev.mDeviceAddress));
+        for (SADeviceState deviceState : mSADevices) {
+            if (deviceState.mEnabled) {
+                compatList.add(deviceState.getAudioDeviceAttributes());
             }
         }
         return compatList;
@@ -521,15 +515,15 @@
      */
     private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada,
             boolean forceEnable) {
+        if (!isDeviceCompatibleWithSpatializationModes(ada)) {
+            return;
+        }
         loglogi("addCompatibleAudioDevice: dev=" + ada);
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
         boolean isInList = false;
         SADeviceState deviceUpdated = null; // non-null on update.
 
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 isInList = true;
                 if (forceEnable) {
                     deviceState.mEnabled = true;
@@ -539,11 +533,10 @@
             }
         }
         if (!isInList) {
-            final SADeviceState dev = new SADeviceState(deviceType,
-                    wireless ? ada.getAddress() : "");
-            dev.mEnabled = true;
-            mSADevices.add(dev);
-            deviceUpdated = dev;
+            final SADeviceState deviceState = new SADeviceState(ada.getType(), ada.getAddress());
+            deviceState.mEnabled = true;
+            mSADevices.add(deviceState);
+            deviceUpdated = deviceState;
         }
         if (deviceUpdated != null) {
             onRoutingUpdated();
@@ -574,13 +567,10 @@
 
     synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
         loglogi("removeCompatibleAudioDevice: dev=" + ada);
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
         SADeviceState deviceUpdated = null; // non-null on update.
 
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 deviceState.mEnabled = false;
                 deviceUpdated = deviceState;
                 break;
@@ -602,10 +592,9 @@
         // if not a wireless device, this value will be overwritten to map the type
         // to TYPE_BUILTIN_SPEAKER or TYPE_WIRED_HEADPHONES
         @AudioDeviceInfo.AudioDeviceType int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
 
         // if not a wireless device: find if media device is in the speaker, wired headphones
-        if (!wireless) {
+        if (!isWireless(deviceType)) {
             // is the device type capable of doing SA?
             if (!mSACapableDeviceTypes.contains(deviceType)) {
                 Log.i(TAG, "Device incompatible with Spatial Audio dev:" + ada);
@@ -640,9 +629,7 @@
         boolean enabled = false;
         boolean available = false;
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 available = true;
                 enabled = deviceState.mEnabled;
                 break;
@@ -652,11 +639,12 @@
     }
 
     private synchronized void addWirelessDeviceIfNew(@NonNull AudioDeviceAttributes ada) {
+        if (!isDeviceCompatibleWithSpatializationModes(ada)) {
+            return;
+        }
         boolean knownDevice = false;
         for (SADeviceState deviceState : mSADevices) {
-            // wireless device so always check address
-            if (ada.getType() == deviceState.mDeviceType
-                    && ada.getAddress().equals(deviceState.mDeviceAddress)) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 knownDevice = true;
                 break;
             }
@@ -704,13 +692,8 @@
         if (ada.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) {
             return false;
         }
-
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 return true;
             }
         }
@@ -719,12 +702,19 @@
 
     private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes,
             @NonNull AudioFormat format, @NonNull AudioDeviceAttributes[] devices) {
-        final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(devices[0].getType(),
+        if (isDeviceCompatibleWithSpatializationModes(devices[0])) {
+            return AudioSystem.canBeSpatialized(attributes, format, devices);
+        }
+        return false;
+    }
+
+    private boolean isDeviceCompatibleWithSpatializationModes(@NonNull AudioDeviceAttributes ada) {
+        final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(),
                 /*default when type not found*/ SpatializationMode.SPATIALIZER_BINAURAL);
         if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported)
                 || (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL
                         && mTransauralSupported)) {
-            return AudioSystem.canBeSpatialized(attributes, format, devices);
+            return true;
         }
         return false;
     }
@@ -1089,13 +1079,8 @@
             Log.v(TAG, "no headtracking support, ignoring setHeadTrackerEnabled to " + enabled
                     + " for " + ada);
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 if (!deviceState.mHasHeadTracker) {
                     Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled
                             + " device:" + ada + " on a device without headtracker");
@@ -1109,7 +1094,7 @@
             }
         }
         // check current routing to see if it affects the headtracking mode
-        if (ROUTING_DEVICES[0].getType() == deviceType
+        if (ROUTING_DEVICES[0].getType() == ada.getType()
                 && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) {
             setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled
                     : Spatializer.HEAD_TRACKING_MODE_DISABLED);
@@ -1121,13 +1106,8 @@
             Log.v(TAG, "no headtracking support, hasHeadTracker always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 return deviceState.mHasHeadTracker;
             }
         }
@@ -1144,13 +1124,8 @@
             Log.v(TAG, "no headtracking support, setHasHeadTracker always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 if (!deviceState.mHasHeadTracker) {
                     deviceState.mHasHeadTracker = true;
                     mAudioService.persistSpatialAudioDeviceSettings();
@@ -1168,13 +1143,8 @@
             Log.v(TAG, "no headtracking support, isHeadTrackerEnabled always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 if (!deviceState.mHasHeadTracker) {
                     return false;
                 }
@@ -1531,7 +1501,7 @@
 
         SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @NonNull String address) {
             mDeviceType = deviceType;
-            mDeviceAddress = Objects.requireNonNull(address);
+            mDeviceAddress = isWireless(deviceType) ? Objects.requireNonNull(address) : "";
         }
 
         @Override
@@ -1599,6 +1569,18 @@
                 return null;
             }
         }
+
+        public AudioDeviceAttributes getAudioDeviceAttributes() {
+            return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+                    mDeviceType, mDeviceAddress == null ? "" : mDeviceAddress);
+        }
+
+        public boolean matchesAudioDeviceAttributes(AudioDeviceAttributes ada) {
+            final int deviceType = ada.getType();
+            final boolean wireless = isWireless(deviceType);
+            return (deviceType == mDeviceType)
+                        && (!wireless || ada.getAddress().equals(mDeviceAddress));
+        }
     }
 
     /*package*/ synchronized String getSADeviceSettings() {
@@ -1619,7 +1601,9 @@
         // small list, not worth overhead of Arrays.stream(devSettings)
         for (String setting : devSettings) {
             SADeviceState devState = SADeviceState.fromPersistedString(setting);
-            if (devState != null) {
+            if (devState != null
+                    && isDeviceCompatibleWithSpatializationModes(
+                            devState.getAudioDeviceAttributes())) {
                 mSADevices.add(devState);
                 logDeviceState(devState, "setSADeviceSettings");
             }
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index cc49f07..41ca13f 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -538,13 +538,12 @@
 
     void onDialogAnimatedIn() {
         if (mState != STATE_AUTH_STARTED) {
-            Slog.w(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
+            Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
+            return;
         }
 
         mState = STATE_AUTH_STARTED_UI_SHOWING;
-
         startAllPreparedFingerprintSensors();
-        mState = STATE_AUTH_STARTED_UI_SHOWING;
     }
 
     void onTryAgainPressed() {
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index 968146a..ef2931f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -20,14 +20,18 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.hardware.biometrics.BiometricConstants;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.function.BooleanSupplier;
 
 /**
  * Contains all the necessary information for a HAL operation.
@@ -84,6 +88,8 @@
     private final BaseClientMonitor mClientMonitor;
     @Nullable
     private final ClientMonitorCallback mClientCallback;
+    @NonNull
+    private final BooleanSupplier mIsDebuggable;
     @Nullable
     private ClientMonitorCallback mOnStartCallback;
     @OperationState
@@ -99,14 +105,33 @@
         this(clientMonitor, callback, STATE_WAITING_IN_QUEUE);
     }
 
+    @VisibleForTesting
+    BiometricSchedulerOperation(
+            @NonNull BaseClientMonitor clientMonitor,
+            @Nullable ClientMonitorCallback callback,
+            @NonNull BooleanSupplier isDebuggable
+    ) {
+        this(clientMonitor, callback, STATE_WAITING_IN_QUEUE, isDebuggable);
+    }
+
     protected BiometricSchedulerOperation(
             @NonNull BaseClientMonitor clientMonitor,
             @Nullable ClientMonitorCallback callback,
             @OperationState int state
     ) {
+        this(clientMonitor, callback, state, Build::isDebuggable);
+    }
+
+    private BiometricSchedulerOperation(
+            @NonNull BaseClientMonitor clientMonitor,
+            @Nullable ClientMonitorCallback callback,
+            @OperationState int state,
+            @NonNull BooleanSupplier isDebuggable
+    ) {
         mClientMonitor = clientMonitor;
         mClientCallback = callback;
         mState = state;
+        mIsDebuggable = isDebuggable;
         mCancelWatchdog = () -> {
             if (!isFinished()) {
                 Slog.e(TAG, "[Watchdog Triggered]: " + this);
@@ -144,13 +169,19 @@
      * @return if this operation started
      */
     public boolean start(@NonNull ClientMonitorCallback callback) {
-        checkInState("start",
+        if (errorWhenNoneOf("start",
                 STATE_WAITING_IN_QUEUE,
                 STATE_WAITING_FOR_COOKIE,
-                STATE_WAITING_IN_QUEUE_CANCELING);
+                STATE_WAITING_IN_QUEUE_CANCELING)) {
+            return false;
+        }
 
         if (mClientMonitor.getCookie() != 0) {
-            throw new IllegalStateException("operation requires cookie");
+            String err = "operation requires cookie";
+            if (mIsDebuggable.getAsBoolean()) {
+                throw new IllegalStateException(err);
+            }
+            Slog.e(TAG, err);
         }
 
         return doStart(callback);
@@ -164,16 +195,18 @@
      * @return if this operation started
      */
     public boolean startWithCookie(@NonNull ClientMonitorCallback callback, int cookie) {
-        checkInState("start",
-                STATE_WAITING_IN_QUEUE,
-                STATE_WAITING_FOR_COOKIE,
-                STATE_WAITING_IN_QUEUE_CANCELING);
-
         if (mClientMonitor.getCookie() != cookie) {
             Slog.e(TAG, "Mismatched cookie for operation: " + this + ", received: " + cookie);
             return false;
         }
 
+        if (errorWhenNoneOf("start",
+                STATE_WAITING_IN_QUEUE,
+                STATE_WAITING_FOR_COOKIE,
+                STATE_WAITING_IN_QUEUE_CANCELING)) {
+            return false;
+        }
+
         return doStart(callback);
     }
 
@@ -217,10 +250,12 @@
      * immediately abort the operation and notify the client that it has finished unsuccessfully.
      */
     public void abort() {
-        checkInState("cannot abort a non-pending operation",
+        if (errorWhenNoneOf("abort",
                 STATE_WAITING_IN_QUEUE,
                 STATE_WAITING_FOR_COOKIE,
-                STATE_WAITING_IN_QUEUE_CANCELING);
+                STATE_WAITING_IN_QUEUE_CANCELING)) {
+            return;
+        }
 
         if (isHalOperation()) {
             ((HalClientMonitor<?>) mClientMonitor).unableToStart();
@@ -247,7 +282,9 @@
      *                 the callback used from {@link #start(ClientMonitorCallback)} is used)
      */
     public void cancel(@NonNull Handler handler, @NonNull ClientMonitorCallback callback) {
-        checkNotInState("cancel", STATE_FINISHED);
+        if (errorWhenOneOf("cancel", STATE_FINISHED)) {
+            return;
+        }
 
         final int currentState = mState;
         if (!isInterruptable()) {
@@ -402,21 +439,28 @@
         return mClientMonitor;
     }
 
-    private void checkNotInState(String message, @OperationState int... states) {
-        for (int state : states) {
-            if (mState == state) {
-                throw new IllegalStateException(message + ": illegal state= " + state);
+    private boolean errorWhenOneOf(String op, @OperationState int... states) {
+        final boolean isError = ArrayUtils.contains(states, mState);
+        if (isError) {
+            String err = op + ": mState must not be " + mState;
+            if (mIsDebuggable.getAsBoolean()) {
+                throw new IllegalStateException(err);
             }
+            Slog.e(TAG, err);
         }
+        return isError;
     }
 
-    private void checkInState(String message, @OperationState int... states) {
-        for (int state : states) {
-            if (mState == state) {
-                return;
+    private boolean errorWhenNoneOf(String op, @OperationState int... states) {
+        final boolean isError = !ArrayUtils.contains(states, mState);
+        if (isError) {
+            String err = op + ": mState=" + mState + " must be one of " + Arrays.toString(states);
+            if (mIsDebuggable.getAsBoolean()) {
+                throw new IllegalStateException(err);
             }
+            Slog.e(TAG, err);
         }
-        throw new IllegalStateException(message + ": illegal state= " + mState);
+        return isError;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 098e8f7..7d12ede 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -46,7 +46,6 @@
 import android.util.ArrayMap;
 import android.util.Slog;
 import android.view.ContentRecordingSession;
-import android.window.WindowContainerToken;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
@@ -433,7 +432,7 @@
         private IBinder mToken;
         private IBinder.DeathRecipient mDeathEater;
         private boolean mRestoreSystemAlertWindow;
-        private WindowContainerToken mTaskRecordingWindowContainerToken = null;
+        private IBinder mLaunchCookie = null;
 
         MediaProjection(int type, int uid, String packageName, int targetSdkVersion,
                 boolean isPrivileged) {
@@ -609,14 +608,13 @@
         }
 
         @Override // Binder call
-        public void setTaskRecordingWindowContainerToken(WindowContainerToken token) {
-            // TODO(b/221417940) set the task id to record from sysui, for the package chosen.
-            mTaskRecordingWindowContainerToken = token;
+        public void setLaunchCookie(IBinder launchCookie) {
+            mLaunchCookie = launchCookie;
         }
 
         @Override // Binder call
-        public WindowContainerToken getTaskRecordingWindowContainerToken() {
-            return mTaskRecordingWindowContainerToken;
+        public IBinder getLaunchCookie() {
+            return mLaunchCookie;
         }
 
         public MediaProjectionInfo getProjectionInfo() {
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index de9102a..6135fe8 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -340,7 +340,8 @@
             int newRuleInstanceCount = getCurrentInstanceCount(automaticZenRule.getOwner())
                     + getCurrentInstanceCount(automaticZenRule.getConfigurationActivity())
                     + 1;
-            if (newRuleInstanceCount > RULE_LIMIT_PER_PACKAGE
+            int newPackageRuleCount = getPackageRuleCount(pkg) + 1;
+            if (newPackageRuleCount > RULE_LIMIT_PER_PACKAGE
                     || (ruleInstanceLimit > 0 && ruleInstanceLimit < newRuleInstanceCount)) {
                 throw new IllegalArgumentException("Rule instance limit exceeded");
             }
@@ -521,6 +522,23 @@
         return count;
     }
 
+    // Equivalent method to getCurrentInstanceCount, but for all rules associated with a specific
+    // package rather than a condition provider service or activity.
+    private int getPackageRuleCount(String pkg) {
+        if (pkg == null) {
+            return 0;
+        }
+        int count = 0;
+        synchronized (mConfig) {
+            for (ZenRule rule : mConfig.automaticRules.values()) {
+                if (pkg.equals(rule.getPkg())) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
     public boolean canManageAutomaticZenRule(ZenRule rule) {
         final int callingUid = Binder.getCallingUid();
         if (callingUid == 0 || callingUid == Process.SYSTEM_UID) {
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index 06a54a4..9bfb40f 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -1540,6 +1540,7 @@
             try {
                 int minVers = ParsingUtils.DEFAULT_MIN_SDK_VERSION;
                 String minCode = null;
+                boolean minAssigned = false;
                 int targetVers = ParsingUtils.DEFAULT_TARGET_SDK_VERSION;
                 String targetCode = null;
                 int maxVers = Integer.MAX_VALUE;
@@ -1548,9 +1549,11 @@
                 if (val != null) {
                     if (val.type == TypedValue.TYPE_STRING && val.string != null) {
                         minCode = val.string.toString();
+                        minAssigned = !TextUtils.isEmpty(minCode);
                     } else {
                         // If it's not a string, it's an integer.
                         minVers = val.data;
+                        minAssigned = true;
                     }
                 }
 
@@ -1558,7 +1561,7 @@
                 if (val != null) {
                     if (val.type == TypedValue.TYPE_STRING && val.string != null) {
                         targetCode = val.string.toString();
-                        if (minCode == null) {
+                        if (!minAssigned) {
                             minCode = targetCode;
                         }
                     } else {
diff --git a/services/core/java/com/android/server/testharness/TestHarnessModeService.java b/services/core/java/com/android/server/testharness/TestHarnessModeService.java
index b6a4135..452bdf4 100644
--- a/services/core/java/com/android/server/testharness/TestHarnessModeService.java
+++ b/services/core/java/com/android/server/testharness/TestHarnessModeService.java
@@ -189,6 +189,7 @@
         if (adbManager.getAdbTempKeysFile() != null) {
             writeBytesToFile(persistentData.mAdbTempKeys, adbManager.getAdbTempKeysFile().toPath());
         }
+        adbManager.notifyKeyFilesUpdated();
     }
 
     private void configureUser() {
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 622de57..270891f 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -56,6 +56,10 @@
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE;
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE;
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_WARM_LAUNCH;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
 import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__APPEARED_APPLY_TREATMENT;
@@ -1376,7 +1380,7 @@
             return;
         }
 
-        logAppCompatStateInternal(activity, state, packageUid, compatStateInfo);
+        logAppCompatStateInternal(activity, state, compatStateInfo);
     }
 
     /**
@@ -1416,18 +1420,61 @@
             }
         }
         if (activityToLog != null && stateToLog != APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE) {
-            logAppCompatStateInternal(activityToLog, stateToLog, packageUid, compatStateInfo);
+            logAppCompatStateInternal(activityToLog, stateToLog, compatStateInfo);
         }
     }
 
+    private static boolean isAppCompateStateChangedToLetterboxed(int state) {
+        return state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO
+                || state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION
+                || state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
+    }
+
     private void logAppCompatStateInternal(@NonNull ActivityRecord activity, int state,
-            int packageUid, PackageCompatStateInfo compatStateInfo) {
+             PackageCompatStateInfo compatStateInfo) {
         compatStateInfo.mLastLoggedState = state;
         compatStateInfo.mLastLoggedActivity = activity;
-        FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, packageUid, state);
+        int packageUid = activity.info.applicationInfo.uid;
+
+        int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION;
+        if (isAppCompateStateChangedToLetterboxed(state)) {
+            positionToLog = activity.mLetterboxUiController.getLetterboxPositionForLogging();
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED,
+                packageUid, state, positionToLog);
 
         if (DEBUG_METRICS) {
-            Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s)", packageUid, state));
+            Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s, %s)",
+                    packageUid, state, positionToLog));
+        }
+    }
+
+    /**
+     * Logs the changing of the letterbox position along with its package UID
+     */
+    void logLetterboxPositionChange(@NonNull ActivityRecord activity, int position) {
+        int packageUid = activity.info.applicationInfo.uid;
+        FrameworkStatsLog.write(FrameworkStatsLog.LETTERBOX_POSITION_CHANGED, packageUid, position);
+
+        if (!mPackageUidToCompatStateInfo.contains(packageUid)) {
+            // There is no last logged activity for this packageUid so we should not log the
+            // position change as we can only log the position change for the current activity
+            return;
+        }
+        final PackageCompatStateInfo compatStateInfo = mPackageUidToCompatStateInfo.get(packageUid);
+        final ActivityRecord lastLoggedActivity = compatStateInfo.mLastLoggedActivity;
+        if (activity != lastLoggedActivity) {
+            // Only log the position change for the current activity to be consistent with
+            // findAppCompatStateToLog and ensure that metrics for the state changes are computed
+            // correctly
+            return;
+        }
+        int state = activity.getAppCompatState();
+        logAppCompatStateInternal(activity, state, compatStateInfo);
+
+        if (DEBUG_METRICS) {
+            Slog.i(TAG, String.format("LETTERBOX_POSITION_CHANGED(%s, %s)",
+                    packageUid, position));
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b6870f5..62427e1 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3215,12 +3215,29 @@
             return false;
         }
 
-        if (mRootWindowContainer.getTopResumedActivity() == this
-                && getDisplayContent().mFocusedApp == this) {
-            ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: already on top, "
-                    + "activity=%s", this);
-            return !isState(RESUMED);
+        // If this activity already positions on the top focused task, moving the task to front
+        // is not needed. But we still need to ensure this activity is focused because the
+        // current focused activity could be another activity in the same Task if activities are
+        // displayed on adjacent TaskFragments.
+        final ActivityRecord currentFocusedApp = mDisplayContent.mFocusedApp;
+        if (currentFocusedApp != null && currentFocusedApp.task == task) {
+            final Task topFocusableTask = mDisplayContent.getTask(
+                    (t) -> t.isLeafTask() && t.isFocusable(), true /*  traverseTopToBottom */);
+            if (task == topFocusableTask) {
+                if (currentFocusedApp == this) {
+                    ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: already on top "
+                            + "and focused, activity=%s", this);
+                } else {
+                    ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: set focused, "
+                            + "activity=%s", this);
+                    mDisplayContent.setFocusedApp(this);
+                    mAtmService.mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
+                            true /* updateInputWindows */);
+                }
+                return !isState(RESUMED);
+            }
         }
+
         ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: activity=%s", this);
 
         rootTask.moveToFront(reason, task);
@@ -7796,11 +7813,15 @@
                 newParentConfiguration.windowConfiguration.getWindowingMode();
         final boolean isFixedOrientationLetterboxAllowed =
                 parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW
-                        || parentWindowingMode == WINDOWING_MODE_FULLSCREEN;
+                        || parentWindowingMode == WINDOWING_MODE_FULLSCREEN
+                        // Switching from PiP to fullscreen.
+                        || (parentWindowingMode == WINDOWING_MODE_PINNED
+                                && resolvedConfig.windowConfiguration.getWindowingMode()
+                                        == WINDOWING_MODE_FULLSCREEN);
         // TODO(b/181207944): Consider removing the if condition and always run
         // resolveFixedOrientationConfiguration() since this should be applied for all cases.
         if (isFixedOrientationLetterboxAllowed) {
-            resolveFixedOrientationConfiguration(newParentConfiguration, parentWindowingMode);
+            resolveFixedOrientationConfiguration(newParentConfiguration);
         }
 
         if (mCompatDisplayInsets != null) {
@@ -8092,8 +8113,7 @@
      * <p>If letterboxed due to fixed orientation then aspect ratio restrictions are also applied
      * in this method.
      */
-    private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig,
-            int windowingMode) {
+    private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig) {
         mLetterboxBoundsForFixedOrientationAndAspectRatio = null;
         mIsEligibleForFixedOrientationLetterbox = false;
         final Rect parentBounds = newParentConfig.windowConfiguration.getBounds();
@@ -8113,11 +8133,6 @@
         if (organizedTf != null && !organizedTf.fillsParent()) {
             return;
         }
-        if (windowingMode == WINDOWING_MODE_PINNED) {
-            // PiP bounds have higher priority than the requested orientation. Otherwise the
-            // activity may be squeezed into a small piece.
-            return;
-        }
 
         final Rect resolvedBounds =
                 getResolvedOverrideConfiguration().windowConfiguration.getBounds();
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index fc412cbd..a7c09a4 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -79,6 +79,10 @@
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
 import static com.android.server.wm.Task.REPARENT_MOVE_ROOT_TASK_TO_FRONT;
+import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
+import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION;
+import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK;
+import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import android.annotation.NonNull;
@@ -132,6 +136,7 @@
 import com.android.server.uri.NeededUriGrants;
 import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
 import com.android.server.wm.LaunchParamsController.LaunchParams;
+import com.android.server.wm.TaskFragment.EmbeddingCheckResult;
 
 import java.io.PrintWriter;
 import java.text.DateFormat;
@@ -2074,24 +2079,6 @@
             }
         }
 
-        if (mInTaskFragment != null && !canEmbedActivity(mInTaskFragment, r, newTask, targetTask)) {
-            final StringBuilder errorMsg = new StringBuilder("Permission denied: Cannot embed " + r
-                    + " to " + mInTaskFragment.getTask() + ". newTask=" + newTask + ", targetTask= "
-                    + targetTask);
-            if (newTask && isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE,
-                    LAUNCH_SINGLE_INSTANCE_PER_TASK, LAUNCH_SINGLE_TASK)) {
-                errorMsg.append("\nActivity tries to launch on a new task because the launch mode"
-                        + " is " + launchModeToString(mLaunchMode));
-            } else if (newTask && (mLaunchFlags & (FLAG_ACTIVITY_NEW_DOCUMENT
-                    | FLAG_ACTIVITY_NEW_TASK)) != 0) {
-                errorMsg.append("\nActivity tries to launch on a new task because the launch flags"
-                        + " contains FLAG_ACTIVITY_NEW_DOCUMENT or FLAG_ACTIVITY_NEW_TASK. "
-                        + "mLaunchFlag=" + mLaunchFlags);
-            }
-            Slog.e(TAG, errorMsg.toString());
-            return START_PERMISSION_DENIED;
-        }
-
         // Do not start the activity if target display's DWPC does not allow it.
         // We can't return fatal error code here because it will crash the caller of
         // startActivity() if they don't catch the exception. We don't expect 3P apps to make
@@ -2118,19 +2105,21 @@
     }
 
     /**
-     * Return {@code true} if an activity can be embedded to the TaskFragment.
+     * Returns whether embedding of {@code starting} is allowed.
+     *
      * @param taskFragment the TaskFragment for embedding.
      * @param starting the starting activity.
-     * @param newTask whether the starting activity is going to be launched on a new task.
      * @param targetTask the target task for launching activity, which could be different from
      *                   the one who hosting the embedding.
      */
-    private boolean canEmbedActivity(@NonNull TaskFragment taskFragment,
-            @NonNull ActivityRecord starting, boolean newTask, Task targetTask) {
+    @VisibleForTesting
+    @EmbeddingCheckResult
+    static int canEmbedActivity(@NonNull TaskFragment taskFragment,
+            @NonNull ActivityRecord starting, @NonNull Task targetTask) {
         final Task hostTask = taskFragment.getTask();
         // Not allowed embedding a separate task or without host task.
-        if (hostTask == null || newTask || targetTask != hostTask) {
-            return false;
+        if (hostTask == null || targetTask != hostTask) {
+            return EMBEDDING_DISALLOWED_NEW_TASK;
         }
 
         return taskFragment.isAllowedToEmbedActivity(starting);
@@ -2576,6 +2565,7 @@
             mInTask = null;
         }
         mInTaskFragment = inTaskFragment;
+        sendNewTaskFragmentResultRequestIfNeeded();
 
         mStartFlags = startFlags;
         // If the onlyIfNeeded flag is set, then we can do this if the activity being launched
@@ -2618,6 +2608,18 @@
         }
     }
 
+    private void sendNewTaskFragmentResultRequestIfNeeded() {
+        if (mStartActivity.resultTo != null && mInTaskFragment != null
+                && mInTaskFragment != mStartActivity.resultTo.getTaskFragment()) {
+            Slog.w(TAG,
+                    "Activity is launching as a new TaskFragment, so cancelling activity result.");
+            mStartActivity.resultTo.sendResult(INVALID_UID, mStartActivity.resultWho,
+                    mStartActivity.requestCode, RESULT_CANCELED,
+                    null /* data */, null /* dataGrants */);
+            mStartActivity.resultTo = null;
+        }
+    }
+
     private void computeLaunchingTaskFlags() {
         // If the caller is not coming from another activity, but has given us an explicit task into
         // which they would like us to launch the new activity, then let's see about doing that.
@@ -2957,19 +2959,16 @@
         mIntentDelivered = true;
     }
 
+    /** Places {@link #mStartActivity} in {@code task} or an embedded {@link TaskFragment}. */
     private void addOrReparentStartingActivity(@NonNull Task task, String reason) {
         TaskFragment newParent = task;
         if (mInTaskFragment != null) {
-            // TODO(b/234351413): remove remaining embedded Task logic.
-            // mInTaskFragment is created and added to the leaf task by task fragment organizer's
-            // request. If the task was resolved and different than mInTaskFragment, reparent the
-            // task to mInTaskFragment for embedding.
-            if (mInTaskFragment.getTask() != task) {
-                if (shouldReparentInTaskFragment(task)) {
-                    task.reparent(mInTaskFragment, POSITION_TOP);
-                }
-            } else {
+            int embeddingCheckResult = canEmbedActivity(mInTaskFragment, mStartActivity, task);
+            if (embeddingCheckResult == EMBEDDING_ALLOWED) {
                 newParent = mInTaskFragment;
+            } else {
+                // Start mStartActivity to task instead if it can't be embedded to mInTaskFragment.
+                sendCanNotEmbedActivityError(mInTaskFragment, embeddingCheckResult);
             }
         } else {
             TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;
@@ -2981,20 +2980,12 @@
                 }
             }
             if (candidateTf != null && candidateTf.isEmbedded()
-                    && canEmbedActivity(candidateTf, mStartActivity, false /* newTask */, task)) {
+                    && canEmbedActivity(candidateTf, mStartActivity, task) == EMBEDDING_ALLOWED) {
                 // Use the embedded TaskFragment of the top activity as the new parent if the
                 // activity can be embedded.
                 newParent = candidateTf;
             }
         }
-        // Start Activity to the Task if mStartActivity's min dimensions are not satisfied.
-        if (newParent.isEmbedded() && newParent.smallerThanMinDimension(mStartActivity)) {
-            reason += " - MinimumDimensionViolation";
-            mService.mWindowOrganizerController.sendMinimumDimensionViolation(
-                    newParent, mStartActivity.getMinDimensions(), mRequest.errorCallbackToken,
-                    reason);
-            newParent = task;
-        }
         if (mStartActivity.getTaskFragment() == null
                 || mStartActivity.getTaskFragment() == newParent) {
             newParent.addChild(mStartActivity, POSITION_TOP);
@@ -3003,16 +2994,41 @@
         }
     }
 
-    private boolean shouldReparentInTaskFragment(Task task) {
-        // The task has not been embedded. We should reparent the task to TaskFragment.
-        if (!task.isEmbedded()) {
-            return true;
+    /**
+     * Notifies the client side that {@link #mStartActivity} cannot be embedded to
+     * {@code taskFragment}.
+     */
+    private void sendCanNotEmbedActivityError(TaskFragment taskFragment,
+            @EmbeddingCheckResult int result) {
+        final String errMsg;
+        switch(result) {
+            case EMBEDDING_DISALLOWED_NEW_TASK: {
+                errMsg = "Cannot embed " + mStartActivity + " that launched on another task"
+                        + ",mLaunchMode=" + launchModeToString(mLaunchMode)
+                        + ",mLaunchFlag=" + Integer.toHexString(mLaunchFlags);
+                break;
+            }
+            case EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION: {
+                errMsg = "Cannot embed " + mStartActivity
+                        + ". TaskFragment's bounds:" + taskFragment.getBounds()
+                        + ", minimum dimensions:" + mStartActivity.getMinDimensions();
+                break;
+            }
+            case EMBEDDING_DISALLOWED_UNTRUSTED_HOST: {
+                errMsg = "The app:" + mCallingUid + "is not trusted to " + mStartActivity;
+                break;
+            }
+            default:
+                errMsg = "Unhandled embed result:" + result;
         }
-        WindowContainer<?> parent = task.getParent();
-        // If the Activity is going to launch on top of embedded Task in the same TaskFragment,
-        // we don't need to reparent the Task. Otherwise, the embedded Task should reparent to
-        // another TaskFragment.
-        return parent.asTaskFragment() != mInTaskFragment;
+        if (taskFragment.isOrganized()) {
+            mService.mWindowOrganizerController.sendTaskFragmentOperationFailure(
+                    taskFragment.getTaskFragmentOrganizer(), mRequest.errorCallbackToken,
+                    new SecurityException(errMsg));
+        } else {
+            // If the taskFragment is not organized, just dump error message as warning logs.
+            Slog.w(TAG, errMsg);
+        }
     }
 
     private int adjustLaunchFlagsToDocumentMode(ActivityRecord r, boolean launchSingleInstance,
diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java
index fca4942..fff7637 100644
--- a/services/core/java/com/android/server/wm/ContentRecordingController.java
+++ b/services/core/java/com/android/server/wm/ContentRecordingController.java
@@ -63,6 +63,7 @@
      */
     void setContentRecordingSessionLocked(@Nullable ContentRecordingSession incomingSession,
             @NonNull WindowManagerService wmService) {
+        // TODO(b/219761722) handle a null session arriving due to task setup failing
         if (incomingSession != null && (!ContentRecordingSession.isValid(incomingSession)
                 || ContentRecordingSession.isSameDisplay(mSession, incomingSession))) {
             // Ignore an invalid session, or a session for the same display as currently recording.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 96d9d66..288777b 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1610,7 +1610,8 @@
         if (mTransitionController.useShellTransitionsRotation()) {
             return ROTATION_UNDEFINED;
         }
-        if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM) {
+        if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM
+                || getIgnoreOrientationRequest()) {
             return ROTATION_UNDEFINED;
         }
         if (r.mOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
@@ -6194,6 +6195,14 @@
                 .getKeyguardController().isAodShowing(mDisplayId);
     }
 
+    /**
+     * @return whether the keyguard is occluded on this display
+     */
+    boolean isKeyguardOccluded() {
+        return mRootWindowContainer.mTaskSupervisor
+                .getKeyguardController().isDisplayOccluded(mDisplayId);
+    }
+
     @VisibleForTesting
     void removeAllTasks() {
         forAllTasks((t) -> { t.getRootTask().removeChild(t, "removeAllTasks"); });
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 08715b1..91b2fb6 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -687,6 +687,24 @@
         }
     }
 
+    /*
+     * Gets the horizontal position of the letterboxed app window when horizontal reachability is
+     * enabled.
+     */
+    @LetterboxHorizontalReachabilityPosition
+    int getLetterboxPositionForHorizontalReachability() {
+        return mLetterboxPositionForHorizontalReachability;
+    }
+
+    /*
+     * Gets the vertical position of the letterboxed app window when vertical reachability is
+     * enabled.
+     */
+    @LetterboxVerticalReachabilityPosition
+    int getLetterboxPositionForVerticalReachability() {
+        return mLetterboxPositionForVerticalReachability;
+    }
+
     /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */
     static String letterboxHorizontalReachabilityPositionToString(
             @LetterboxHorizontalReachabilityPosition int position) {
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index f849d28..d652767 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -21,6 +21,20 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
 import static com.android.server.wm.ActivityRecord.computeAspectRatio;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -28,6 +42,12 @@
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
 import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString;
 
@@ -259,12 +279,26 @@
             return;
         }
 
+        int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
+                .getLetterboxPositionForHorizontalReachability();
         if (mLetterbox.getInnerFrame().left > x) {
             // Moving to the next stop on the left side of the app window: right > center > left.
             mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop();
+            int changeToLog =
+                    letterboxPositionForHorizontalReachability
+                            == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
+            logLetterboxPositionChange(changeToLog);
         } else if (mLetterbox.getInnerFrame().right < x) {
             // Moving to the next stop on the right side of the app window: left > center > right.
             mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
+            int changeToLog =
+                    letterboxPositionForHorizontalReachability
+                            == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
+            logLetterboxPositionChange(changeToLog);
         }
 
         // TODO(197549949): Add animation for transition.
@@ -280,13 +314,26 @@
             // Only react to clicks at the top and bottom of the letterboxed app window.
             return;
         }
-
+        int letterboxPositionForVerticalReachability = mLetterboxConfiguration
+                .getLetterboxPositionForVerticalReachability();
         if (mLetterbox.getInnerFrame().top > y) {
             // Moving to the next stop on the top side of the app window: bottom > center > top.
             mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop();
+            int changeToLog =
+                    letterboxPositionForVerticalReachability
+                            == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
+            logLetterboxPositionChange(changeToLog);
         } else if (mLetterbox.getInnerFrame().bottom < y) {
             // Moving to the next stop on the bottom side of the app window: top > center > bottom.
             mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
+            int changeToLog =
+                    letterboxPositionForVerticalReachability
+                            == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
+            logLetterboxPositionChange(changeToLog);
         }
 
         // TODO(197549949): Add animation for transition.
@@ -577,4 +624,63 @@
         return "UNKNOWN_REASON";
     }
 
+    private int letterboxHorizontalReachabilityPositionToLetterboxPosition(
+            @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition int position) {
+        switch (position) {
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
+            default:
+                throw new AssertionError(
+                        "Unexpected letterbox horizontal reachability position type: "
+                                + position);
+        }
+    }
+
+    private int letterboxVerticalReachabilityPositionToLetterboxPosition(
+            @LetterboxConfiguration.LetterboxVerticalReachabilityPosition int position) {
+        switch (position) {
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
+            default:
+                throw new AssertionError(
+                        "Unexpected letterbox vertical reachability position type: "
+                                + position);
+        }
+    }
+
+    int getLetterboxPositionForLogging() {
+        int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
+        if (isHorizontalReachabilityEnabled()) {
+            int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
+                    .getLetterboxPositionForHorizontalReachability();
+            positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
+                            letterboxPositionForHorizontalReachability);
+        } else if (isVerticalReachabilityEnabled()) {
+            int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
+                    .getLetterboxPositionForVerticalReachability();
+            positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
+                            letterboxPositionForVerticalReachability);
+        }
+        return positionToLog;
+    }
+
+    private LetterboxConfiguration getLetterboxConfiguration() {
+        return mLetterboxConfiguration;
+    }
+
+    /**
+     * Logs letterbox position changes via {@link ActivityMetricsLogger#logLetterboxPositionChange}.
+     */
+    private void logLetterboxPositionChange(int letterboxPositionChange) {
+        mActivityRecord.mTaskSupervisor.getActivityMetricsLogger()
+                .logLetterboxPositionChange(mActivityRecord, letterboxPositionChange);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index ad158c7..ac1a2b1 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -331,8 +331,10 @@
 
     private void invokeAnimationCancelled(String reason) {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "cancelAnimation(): reason=%s", reason);
+        final boolean isKeyguardOccluded = mDisplayContent.isKeyguardOccluded();
+
         try {
-            mRemoteAnimationAdapter.getRunner().onAnimationCancelled();
+            mRemoteAnimationAdapter.getRunner().onAnimationCancelled(isKeyguardOccluded);
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to notify cancel", e);
         }
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 36464b8..8220cae 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -1114,7 +1114,10 @@
         // If a task is launching from a created-by-organizer task, it should be launched into the
         // same created-by-organizer task as well. Unless, the candidate task is already positioned
         // in the another adjacent task.
-        if (sourceTask != null) {
+        if (sourceTask != null && (candidateTask == null
+                // A pinned task relaunching should be handled by its task organizer. Skip fallback
+                // launch target of a pinned task from source task.
+                || candidateTask.getWindowingMode() != WINDOWING_MODE_PINNED)) {
             Task launchTarget = sourceTask.getCreatedByOrganizerTask();
             if (launchTarget != null && launchTarget.getAdjacentTaskFragment() != null) {
                 if (candidateTask != null) {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 61e484a..f8a9d46 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -140,6 +140,45 @@
     static final boolean SHOW_APP_STARTING_PREVIEW = true;
 
     /**
+     * An embedding check result of {@link #isAllowedToEmbedActivity(ActivityRecord)} or
+     * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}:
+     * indicate that an Activity can be embedded successfully.
+     */
+    static final int EMBEDDING_ALLOWED = 0;
+    /**
+     * An embedding check result of {@link #isAllowedToEmbedActivity(ActivityRecord)} or
+     * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}:
+     * indicate that an Activity can't be embedded because either the Activity does not allow
+     * untrusted embedding, and the embedding host app is not trusted.
+     */
+    static final int EMBEDDING_DISALLOWED_UNTRUSTED_HOST = 1;
+    /**
+     * An embedding check result of {@link #isAllowedToEmbedActivity(ActivityRecord)} or
+     * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}:
+     * indicate that an Activity can't be embedded because this taskFragment's bounds are
+     * {@link #smallerThanMinDimension(ActivityRecord)}.
+     */
+    static final int EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION = 2;
+    /**
+     * An embedding check result of
+     * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}:
+     * indicate that an Activity can't be embedded because the Activity is started on a new task.
+     */
+    static final int EMBEDDING_DISALLOWED_NEW_TASK = 3;
+
+    /**
+     * Embedding check results of {@link #isAllowedToEmbedActivity(ActivityRecord)} or
+     * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}.
+     */
+    @IntDef(prefix = {"EMBEDDING_"}, value = {
+            EMBEDDING_ALLOWED,
+            EMBEDDING_DISALLOWED_UNTRUSTED_HOST,
+            EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION,
+            EMBEDDING_DISALLOWED_NEW_TASK,
+    })
+    @interface EmbeddingCheckResult {}
+
+    /**
      * Indicate that the minimal width/height should use the default value.
      *
      * @see #mMinWidth
@@ -509,20 +548,29 @@
         return false;
     }
 
-    boolean isAllowedToEmbedActivity(@NonNull ActivityRecord a) {
+    @EmbeddingCheckResult
+    int isAllowedToEmbedActivity(@NonNull ActivityRecord a) {
         return isAllowedToEmbedActivity(a, mTaskFragmentOrganizerUid);
     }
 
     /**
      * Checks if the organized task fragment is allowed to have the specified activity, which is
-     * allowed if an activity allows embedding in untrusted mode, or if the trusted mode can be
-     * enabled.
-     * @see #isAllowedToEmbedActivityInTrustedMode(ActivityRecord)
+     * allowed if an activity allows embedding in untrusted mode, if the trusted mode can be
+     * enabled, or if the organized task fragment bounds are not
+     * {@link #smallerThanMinDimension(ActivityRecord)}.
+     *
      * @param uid   uid of the TaskFragment organizer.
+     * @see #isAllowedToEmbedActivityInTrustedMode(ActivityRecord)
      */
-    boolean isAllowedToEmbedActivity(@NonNull ActivityRecord a, int uid) {
-        return isAllowedToEmbedActivityInUntrustedMode(a)
-                || isAllowedToEmbedActivityInTrustedMode(a, uid);
+    @EmbeddingCheckResult
+    int isAllowedToEmbedActivity(@NonNull ActivityRecord a, int uid) {
+        if (!isAllowedToEmbedActivityInUntrustedMode(a)
+                && !isAllowedToEmbedActivityInTrustedMode(a, uid)) {
+            return EMBEDDING_DISALLOWED_UNTRUSTED_HOST;
+        } else if (smallerThanMinDimension(a)) {
+            return EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION;
+        }
+        return EMBEDDING_ALLOWED;
     }
 
     boolean smallerThanMinDimension(@NonNull ActivityRecord activity) {
@@ -539,9 +587,8 @@
         }
         final int minWidth = minDimensions.x;
         final int minHeight = minDimensions.y;
-        final boolean smaller = taskFragBounds.width() < minWidth
+        return taskFragBounds.width() < minWidth
                 || taskFragBounds.height() < minHeight;
-        return smaller;
     }
 
     /**
@@ -598,7 +645,7 @@
         // The system is trusted to embed other apps securely and for all users.
         return UserHandle.getAppId(uid) == SYSTEM_UID
                 // Activities from the same UID can be embedded freely by the host.
-                || uid == a.getUid();
+                || a.isUid(uid);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 9aff23d..392d4c2 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -19,6 +19,7 @@
 import static android.window.TaskFragmentOrganizer.putExceptionInBundle;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
+import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
 import static com.android.server.wm.WindowOrganizerController.configurationsAreEqualForOrganizer;
 
 import android.annotation.IntDef;
@@ -235,7 +236,7 @@
                         + " is not in a task belong to the organizer app.");
                 return;
             }
-            if (!task.isAllowedToEmbedActivity(activity, mOrganizerUid)) {
+            if (task.isAllowedToEmbedActivity(activity, mOrganizerUid) != EMBEDDING_ALLOWED) {
                 Slog.d(TAG, "Reparent activity=" + activity.token
                         + " is not allowed to be embedded.");
                 return;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 3c0cac0..ae61f24 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -149,9 +149,6 @@
 
     final @TransitionType int mType;
     private int mSyncId = -1;
-    // Used for tracking a Transition throughout a lifecycle (i.e. from STATE_COLLECTING to
-    // STATE_FINISHED or STATE_ABORT), and should only be used for testing and debugging.
-    private int mDebugId = -1;
     private @TransitionFlags int mFlags;
     private final TransitionController mController;
     private final BLASTSyncEngine mSyncEngine;
@@ -295,11 +292,6 @@
         return mSyncId;
     }
 
-    @VisibleForTesting
-    int getDebugId() {
-        return mDebugId;
-    }
-
     @TransitionFlags
     int getFlags() {
         return mFlags;
@@ -315,6 +307,10 @@
         return mFinishTransaction;
     }
 
+    private boolean isCollecting() {
+        return mState == STATE_COLLECTING || mState == STATE_STARTED;
+    }
+
     /** Starts collecting phase. Once this starts, all relevant surface operations are sync. */
     void startCollecting(long timeoutMs) {
         if (mState != STATE_PENDING) {
@@ -322,7 +318,6 @@
         }
         mState = STATE_COLLECTING;
         mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG);
-        mDebugId = mSyncId;
 
         mController.mTransitionTracer.logState(this);
     }
@@ -353,7 +348,10 @@
         if (mState < STATE_COLLECTING) {
             throw new IllegalStateException("Transition hasn't started collecting.");
         }
-        if (mSyncId < 0) return;
+        if (!isCollecting()) {
+            // Too late, transition already started playing, so don't collect.
+            return;
+        }
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s",
                 mSyncId, wc);
         // "snapshot" all parents (as potential promotion targets). Do this before checking
@@ -403,7 +401,10 @@
      * or waiting until after the animation to close).
      */
     void collectExistenceChange(@NonNull WindowContainer wc) {
-        if (mSyncId < 0) return;
+        if (mState >= STATE_PLAYING) {
+            // Too late to collect. Don't check too-early here since `collect` will check that.
+            return;
+        }
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Existence Changed in transition %d:"
                 + " %s", mSyncId, wc);
         collect(wc);
@@ -437,7 +438,7 @@
      */
     void setOverrideAnimation(TransitionInfo.AnimationOptions options,
             @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) {
-        if (mSyncId < 0) return;
+        if (!isCollecting()) return;
         mOverrideOptions = options;
         sendRemoteCallback(mClientAnimationStartCallback);
         mClientAnimationStartCallback = startCallback;
@@ -455,7 +456,7 @@
      *           The transition will wait for all groups to be ready.
      */
     void setReady(WindowContainer wc, boolean ready) {
-        if (mSyncId < 0) return;
+        if (!isCollecting() || mSyncId < 0) return;
         mReadyTracker.setReadyFrom(wc, ready);
         applyReady();
     }
@@ -473,7 +474,7 @@
      * @see ReadyTracker#setAllReady.
      */
     void setAllReady() {
-        if (mSyncId < 0) return;
+        if (!isCollecting() || mSyncId < 0) return;
         mReadyTracker.setAllReady();
         applyReady();
     }
@@ -672,7 +673,7 @@
         SurfaceControl.Transaction inputSinkTransaction = null;
         for (int i = 0; i < mParticipants.size(); ++i) {
             final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
-            if (ar == null || !ar.isVisible()) continue;
+            if (ar == null || !ar.isVisible() || ar.getParent() == null) continue;
             if (inputSinkTransaction == null) {
                 inputSinkTransaction = new SurfaceControl.Transaction();
             }
@@ -889,7 +890,6 @@
             // No player registered, so just finish/apply immediately
             cleanUpOnFailure();
         }
-        mSyncId = -1;
         mOverrideOptions = null;
 
         reportStartReasonsToLogger();
@@ -1614,7 +1614,7 @@
     }
 
     boolean getLegacyIsReady() {
-        return (mState == STATE_STARTED || mState == STATE_COLLECTING) && mSyncId >= 0;
+        return isCollecting() && mSyncId >= 0;
     }
 
     static Transition fromBinder(IBinder binder) {
diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java
index b1951e0..c1927d8 100644
--- a/services/core/java/com/android/server/wm/TransitionTracer.java
+++ b/services/core/java/com/android/server/wm/TransitionTracer.java
@@ -79,7 +79,7 @@
             final ProtoOutputStream outputStream = new ProtoOutputStream();
             final long transitionEntryToken = outputStream.start(TRANSITION);
 
-            outputStream.write(ID, transition.getDebugId());
+            outputStream.write(ID, transition.getSyncId());
             outputStream.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos());
             outputStream.write(TRANSITION_TYPE, transition.mType);
             outputStream.write(STATE, transition.getState());
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 125d550..b5abd32 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -48,6 +48,7 @@
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES;
 import static android.provider.Settings.Global.DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR;
 import static android.provider.Settings.Global.DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH;
+import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
@@ -122,7 +123,6 @@
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
-import static com.android.server.wm.WindowContainer.SYNC_STATE_NONE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT_METHOD;
@@ -289,6 +289,7 @@
 import android.window.ClientWindowFrames;
 import android.window.ITaskFpsCallback;
 import android.window.TaskSnapshot;
+import android.window.WindowContainerToken;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -2252,7 +2253,7 @@
                 return 0;
             }
 
-            if (win.cancelAndRedraw()) {
+            if (win.cancelAndRedraw() && win.mPrepareSyncSeqId <= win.mLastSeqIdSentToRelayout) {
                 result |= RELAYOUT_RES_CANCEL_AND_REDRAW;
             }
 
@@ -2558,11 +2559,6 @@
 
                 win.mLastSeqIdSentToRelayout = win.mSyncSeqId;
                 outSyncIdBundle.putInt("seqid", win.mSyncSeqId);
-                // Only mark mAlreadyRequestedSync if there's an explicit sync request, and not if
-                // we're syncing due to mDrawHandlers
-                if (win.mSyncState != SYNC_STATE_NONE) {
-                    win.mAlreadyRequestedSync = true;
-                }
             } else {
                 outSyncIdBundle.putInt("seqid", -1);
             }
@@ -8287,6 +8283,26 @@
         @Override
         public void setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) {
             synchronized (mGlobalLock) {
+                // Allow the controller to handle teardown or a non-task session.
+                if (incomingSession == null
+                        || incomingSession.getContentToRecord() != RECORD_CONTENT_TASK) {
+                    mContentRecordingController.setContentRecordingSessionLocked(incomingSession,
+                            WindowManagerService.this);
+                    return;
+                }
+                // For a task session, find the activity identified by the launch cookie.
+                final WindowContainerToken wct = getTaskWindowContainerTokenForLaunchCookie(
+                        incomingSession.getTokenToRecord());
+                if (wct == null) {
+                    Slog.w(TAG, "Handling a new recording session; unable to find the "
+                            + "WindowContainerToken");
+                    mContentRecordingController.setContentRecordingSessionLocked(null,
+                            WindowManagerService.this);
+                    return;
+                }
+                // Replace the launch cookie in the session details with the task's
+                // WindowContainerToken.
+                incomingSession.setTokenToRecord(wct.asBinder());
                 mContentRecordingController.setContentRecordingSessionLocked(incomingSession,
                         WindowManagerService.this);
             }
@@ -8545,6 +8561,38 @@
     }
 
     /**
+     * Retrieve the {@link WindowContainerToken} of the task that contains the activity started
+     * with the given launch cookie.
+     *
+     * @param launchCookie the launch cookie set on the {@link ActivityOptions} when starting an
+     *                     activity
+     * @return a token representing the task containing the activity started with the given launch
+     * cookie, or {@code null} if the token couldn't be found.
+     */
+    @VisibleForTesting
+    @Nullable
+    WindowContainerToken getTaskWindowContainerTokenForLaunchCookie(@NonNull IBinder launchCookie) {
+        // Find the activity identified by the launch cookie.
+        final ActivityRecord targetActivity = mRoot.getActivity(
+                activity -> activity.mLaunchCookie == launchCookie);
+        if (targetActivity == null) {
+            Slog.w(TAG, "Unable to find the activity for this launch cookie");
+            return null;
+        }
+        if (targetActivity.getTask() == null) {
+            Slog.w(TAG, "Unable to find the task for this launch cookie");
+            return null;
+        }
+        WindowContainerToken taskWindowContainerToken =
+                targetActivity.getTask().mRemoteToken.toWindowContainerToken();
+        if (taskWindowContainerToken == null) {
+            Slog.w(TAG, "Unable to find the WindowContainerToken for " + targetActivity.getName());
+            return null;
+        }
+        return taskWindowContainerToken;
+    }
+
+    /**
      * You need ALLOW_SLIPPERY_TOUCHES permission to be able to set FLAG_SLIPPERY.
      */
     private int sanitizeFlagSlippery(int flags, String windowName, int callingUid, int callingPid) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 1d93c89..97dcb75 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -46,6 +46,7 @@
 import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
+import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
 import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
@@ -814,7 +815,7 @@
                     sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
                     break;
                 }
-                if (!parent.isAllowedToEmbedActivity(activity)) {
+                if (parent.isAllowedToEmbedActivity(activity) != EMBEDDING_ALLOWED) {
                     final Throwable exception = new SecurityException(
                             "The task fragment is not trusted to embed the given activity.");
                     sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
@@ -1057,7 +1058,7 @@
     }
 
     /** A helper method to send minimum dimension violation error to the client. */
-    void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions,
+    private void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions,
             IBinder errorCallbackToken, String reason) {
         if (taskFragment == null || taskFragment.getTaskFragmentOrganizer() == null) {
             return;
@@ -1672,7 +1673,7 @@
             // We are reparenting activities to a new embedded TaskFragment, this operation is only
             // allowed if the new parent is trusted by all reparent activities.
             final boolean isEmbeddingDisallowed = oldParent.forAllActivities(activity ->
-                    !newParentTF.isAllowedToEmbedActivity(activity));
+                    newParentTF.isAllowedToEmbedActivity(activity) == EMBEDDING_ALLOWED);
             if (isEmbeddingDisallowed) {
                 final Throwable exception = new SecurityException(
                         "The new parent is not trusted to embed the activities.");
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 6728e63..46091d8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -115,6 +115,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RESIZE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.policy.WindowManagerPolicy.TRANSIT_ENTER;
@@ -391,7 +392,9 @@
      */
     int mSyncSeqId = 0;
     int mLastSeqIdSentToRelayout = 0;
-    boolean mAlreadyRequestedSync;
+
+    /** The last syncId associated with a prepareSync or 0 when no sync is active. */
+    int mPrepareSyncSeqId = 0;
 
     /**
      * {@code true} when the client was still drawing for sync when the sync-set was finished or
@@ -4421,7 +4424,7 @@
             }
         }
 
-        pw.println(prefix + "mAlreadyRequestedSync=" + mAlreadyRequestedSync);
+        pw.println(prefix + "mPrepareSyncSeqId=" + mPrepareSyncSeqId);
     }
 
     @Override
@@ -5913,6 +5916,13 @@
         return mWinAnimator.getSurfaceControl();
     }
 
+    /** Drops a buffer for this window's view-root from a transaction */
+    private void dropBufferFrom(Transaction t) {
+        SurfaceControl viewSurface = getClientViewRootSurface();
+        if (viewSurface == null) return;
+        t.setBuffer(viewSurface, (android.hardware.HardwareBuffer) null);
+    }
+
     @Override
     boolean prepareSync() {
         if (!mDrawHandlers.isEmpty()) {
@@ -5928,7 +5938,18 @@
         // to draw even if the children draw first or don't need to sync, so we start
         // in WAITING state rather than READY.
         mSyncState = SYNC_STATE_WAITING_FOR_DRAW;
+
+        if (mPrepareSyncSeqId > 0) {
+            // another prepareSync during existing sync (eg. reparented), so pre-emptively
+            // drop buffer (if exists). If the buffer hasn't been received yet, it will be
+            // dropped in finishDrawing.
+            ProtoLog.d(WM_DEBUG_SYNC_ENGINE, "Preparing to sync a window that was already in the"
+                            + " sync, so try dropping buffer. win=%s", this);
+            dropBufferFrom(mSyncTransaction);
+        }
+
         mSyncSeqId++;
+        mPrepareSyncSeqId = mSyncSeqId;
         requestRedrawForSync();
         return true;
     }
@@ -5949,7 +5970,13 @@
         if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mRedrawForSyncReported) {
             mClientWasDrawingForSync = true;
         }
-        mAlreadyRequestedSync = false;
+        mPrepareSyncSeqId = 0;
+        if (cancel) {
+            // This is leaving sync so any buffers left in the sync have a chance of
+            // being applied out-of-order and can also block the buffer queue for this
+            // window. To prevent this, drop the buffer.
+            dropBufferFrom(mSyncTransaction);
+        }
         super.finishSync(outMergedTransaction, cancel);
     }
 
@@ -5971,6 +5998,17 @@
                     .notifyStartingWindowDrawn(mActivityRecord);
         }
 
+        final boolean syncActive = mPrepareSyncSeqId > 0;
+        final boolean syncStillPending = syncActive && mPrepareSyncSeqId > syncSeqId;
+        if (syncStillPending && postDrawTransaction != null) {
+            ProtoLog.d(WM_DEBUG_SYNC_ENGINE, "Got a buffer for request id=%d but latest request is"
+                    + " id=%d. Since the buffer is out-of-date, drop it. win=%s", syncSeqId,
+                    mPrepareSyncSeqId, this);
+            // sync is waiting for a newer seqId, so this buffer is obsolete and can be dropped
+            // to free up the buffer queue.
+            dropBufferFrom(postDrawTransaction);
+        }
+
         final boolean hasSyncHandlers = executeDrawHandlers(postDrawTransaction, syncSeqId);
 
         boolean skipLayout = false;
@@ -5983,10 +6021,15 @@
             // Layout is not needed because the window will be hidden by the fade leash.
             postDrawTransaction = null;
             skipLayout = true;
-        } else if (onSyncFinishedDrawing() && postDrawTransaction != null) {
-            mSyncTransaction.merge(postDrawTransaction);
-            // Consume the transaction because the sync group will merge it.
-            postDrawTransaction = null;
+        } else if (syncActive) {
+            if (!syncStillPending) {
+                onSyncFinishedDrawing();
+            }
+            if (postDrawTransaction != null) {
+                mSyncTransaction.merge(postDrawTransaction);
+                // Consume the transaction because the sync group will merge it.
+                postDrawTransaction = null;
+            }
         }
 
         final boolean layoutNeeded =
@@ -6218,6 +6261,7 @@
     }
 
     public boolean cancelAndRedraw() {
-        return mSyncState != SYNC_STATE_NONE && mAlreadyRequestedSync;
+        // Cancel any draw requests during a sync.
+        return mPrepareSyncSeqId > 0;
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
index b36aa06..e87dd4b 100644
--- a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
@@ -36,8 +36,6 @@
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.server.FgThread;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -48,6 +46,11 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.FileReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
@@ -88,6 +91,7 @@
     private long mOriginalAllowedConnectionTime;
     private File mAdbKeyXmlFile;
     private File mAdbKeyFile;
+    private FakeTicker mFakeTicker;
 
     @Before
     public void setUp() throws Exception {
@@ -96,14 +100,25 @@
         if (mAdbKeyFile.exists()) {
             mAdbKeyFile.delete();
         }
-        mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile);
         mAdbKeyXmlFile = new File(mContext.getFilesDir(), "test_adb_keys.xml");
         if (mAdbKeyXmlFile.exists()) {
             mAdbKeyXmlFile.delete();
         }
+
+        mFakeTicker = new FakeTicker();
+        // Set the ticker time to October 22, 2008 (the day the T-Mobile G1 was released)
+        mFakeTicker.advance(1224658800L);
+
         mThread = new AdbDebuggingThreadTest();
-        mKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
-        mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore);
+        mManager = new AdbDebuggingManager(
+                mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile, mAdbKeyXmlFile, mThread, mFakeTicker);
+
+        mHandler = mManager.mHandler;
+        mThread.setHandler(mHandler);
+
+        mHandler.initKeyStore();
+        mKeyStore = mHandler.mAdbKeyStore;
+
         mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime();
         mBlockingQueue = new ArrayBlockingQueue<>(1);
     }
@@ -122,7 +137,7 @@
     private void setAllowedConnectionTime(long connectionTime) {
         Settings.Global.putLong(mContext.getContentResolver(),
                 Settings.Global.ADB_ALLOWED_CONNECTION_TIME, connectionTime);
-    };
+    }
 
     @Test
     public void testAllowNewKeyOnce() throws Exception {
@@ -158,20 +173,15 @@
         // Allow a connection from a new key with the 'Always allow' option selected.
         runAdbTest(TEST_KEY_1, true, true, false);
 
-        // Get the last connection time for the currently connected key to verify that it is updated
-        // after the disconnect.
-        long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
-
-        // Sleep for a small amount of time to ensure a difference can be observed in the last
-        // connection time after a disconnect.
-        Thread.sleep(10);
+        // Advance the clock by 10ms to ensure there's a difference
+        mFakeTicker.advance(10 * 1_000_000);
 
         // Send the disconnect message for the currently connected key to trigger an update of the
         // last connection time.
         disconnectKey(TEST_KEY_1);
-        assertNotEquals(
+        assertEquals(
                 "The last connection time was not updated after the disconnect",
-                lastConnectionTime,
+                mFakeTicker.currentTimeMillis(),
                 mKeyStore.getLastConnectionTime(TEST_KEY_1));
     }
 
@@ -244,8 +254,8 @@
         // Get the current last connection time for comparison after the scheduled job is run
         long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
 
-        // Sleep a small amount of time to ensure that the updated connection time changes
-        Thread.sleep(10);
+        // Advance a small amount of time to ensure that the updated connection time changes
+        mFakeTicker.advance(10);
 
         // Send a message to the handler to update the last connection time for the active key
         updateKeyStore();
@@ -269,13 +279,13 @@
         persistKeyStore();
         assertTrue(
                 "The key with the 'Always allow' option selected was not persisted in the keystore",
-                mManager.new AdbKeyStore(mAdbKeyXmlFile).isKeyAuthorized(TEST_KEY_1));
+                mManager.new AdbKeyStore().isKeyAuthorized(TEST_KEY_1));
 
         // Get the current last connection time to ensure it is updated in the persisted keystore.
         long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
 
-        // Sleep a small amount of time to ensure the last connection time is updated.
-        Thread.sleep(10);
+        // Advance a small amount of time to ensure the last connection time is updated.
+        mFakeTicker.advance(10);
 
         // Send a message to the handler to update the last connection time for the active key.
         updateKeyStore();
@@ -286,7 +296,7 @@
         assertNotEquals(
                 "The last connection time in the key file was not updated after the update "
                         + "connection time message", lastConnectionTime,
-                mManager.new AdbKeyStore(mAdbKeyXmlFile).getLastConnectionTime(TEST_KEY_1));
+                mManager.new AdbKeyStore().getLastConnectionTime(TEST_KEY_1));
         // Verify that the key is in the adb_keys file
         assertTrue("The key was not in the adb_keys file after persisting the keystore",
                 isKeyInFile(TEST_KEY_1, mAdbKeyFile));
@@ -327,8 +337,8 @@
         // Set the allowed window to a small value to ensure the time is beyond the allowed window.
         setAllowedConnectionTime(1);
 
-        // Sleep for a small amount of time to exceed the allowed window.
-        Thread.sleep(10);
+        // Advance a small amount of time to exceed the allowed window.
+        mFakeTicker.advance(10);
 
         // The AdbKeyStore has a method to get the time of the next key expiration to ensure the
         // scheduled job runs at the time of the next expiration or after 24 hours, whichever occurs
@@ -478,9 +488,12 @@
         // Set the current expiration time to a minute from expiration and verify this new value is
         // returned.
         final long newExpirationTime = 60000;
-        mKeyStore.setLastConnectionTime(TEST_KEY_1,
-                System.currentTimeMillis() - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME
-                        + newExpirationTime, true);
+        mKeyStore.setLastConnectionTime(
+                TEST_KEY_1,
+                mFakeTicker.currentTimeMillis()
+                        - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME
+                        + newExpirationTime,
+                true);
         expirationTime = mKeyStore.getNextExpirationTime();
         if (Math.abs(expirationTime - newExpirationTime) > epsilon) {
             fail("The expiration time for a key about to expire, " + expirationTime
@@ -525,7 +538,7 @@
         // Get the last connection time for the key to verify that it is updated when the connected
         // key message is sent.
         long connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY,
                 TEST_KEY_1).sendToTarget();
         flushHandlerQueue();
@@ -536,7 +549,7 @@
 
         // Verify that the scheduled job updates the connection time of the key.
         connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         updateKeyStore();
         assertNotEquals(
                 "The connection time for the key must be updated when the update keystore message"
@@ -545,7 +558,7 @@
 
         // Verify that the connection time is updated when the key is disconnected.
         connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         disconnectKey(TEST_KEY_1);
         assertNotEquals(
                 "The connection time for the key must be updated when the disconnected message is"
@@ -628,11 +641,11 @@
         setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
 
         // The untracked keys should be added to the keystore as part of the constructor.
-        AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
+        AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore();
 
         // Verify that the connection time for each test key is within a small value of the current
         // time.
-        long time = System.currentTimeMillis();
+        long time = mFakeTicker.currentTimeMillis();
         for (String key : testKeys) {
             long connectionTime = adbKeyStore.getLastConnectionTime(key);
             if (Math.abs(time - connectionTime) > epsilon) {
@@ -651,11 +664,11 @@
         runAdbTest(TEST_KEY_1, true, true, false);
         runAdbTest(TEST_KEY_2, true, true, false);
 
-        // Sleep a small amount of time to ensure the connection time is updated by the scheduled
+        // Advance a small amount of time to ensure the connection time is updated by the scheduled
         // job.
         long connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
         long connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         updateKeyStore();
         assertNotEquals(
                 "The connection time for test key 1 must be updated after the scheduled job runs",
@@ -669,7 +682,7 @@
         disconnectKey(TEST_KEY_2);
         connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
         connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         updateKeyStore();
         assertNotEquals(
                 "The connection time for test key 1 must be updated after another key is "
@@ -686,8 +699,6 @@
         // to clear the adb authorizations when adb is disabled after a boot a NullPointerException
         // was thrown as deleteKeyStore is invoked against the key store. This test ensures the
         // key store can be successfully cleared when adb is disabled.
-        mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper());
-
         clearKeyStore();
     }
 
@@ -723,6 +734,9 @@
 
         // Now remove one of the keys and make sure the other key is still there
         mKeyStore.removeKey(TEST_KEY_1);
+        // Wait for the handler queue to receive the MESSAGE_ADB_PERSIST_KEYSTORE
+        flushHandlerQueue();
+
         assertFalse("The key was still in the adb_keys file after removing the key",
                 isKeyInFile(TEST_KEY_1, mAdbKeyFile));
         assertTrue("The key was not in the adb_keys file after removing a different key",
@@ -730,6 +744,95 @@
     }
 
     @Test
+    public void testAdbKeyStore_addDuplicateKey_doesNotAddDuplicateToAdbKeyFile() throws Exception {
+        setAllowedConnectionTime(0);
+
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+
+        assertEquals("adb_keys contains duplicate keys", 1, adbKeyFileKeys(mAdbKeyFile).size());
+    }
+
+    @Test
+    public void testAdbKeyStore_adbTempKeysFile_readsLastConnectionTimeFromXml() throws Exception {
+        long insertTime = mFakeTicker.currentTimeMillis();
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+
+        mFakeTicker.advance(10);
+        AdbDebuggingManager.AdbKeyStore newKeyStore = mManager.new AdbKeyStore();
+
+        assertEquals(
+                "KeyStore not populated from the XML file.",
+                insertTime,
+                newKeyStore.getLastConnectionTime(TEST_KEY_1));
+    }
+
+    @Test
+    public void test_notifyKeyFilesUpdated_filesDeletedRemovesPreviouslyAddedKey()
+            throws Exception {
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+
+        Files.delete(mAdbKeyXmlFile.toPath());
+        Files.delete(mAdbKeyFile.toPath());
+
+        mManager.notifyKeyFilesUpdated();
+        flushHandlerQueue();
+
+        assertFalse(
+                "Key is authorized after reloading deleted key files. Was state preserved?",
+                mKeyStore.isKeyAuthorized(TEST_KEY_1));
+    }
+
+    @Test
+    public void test_notifyKeyFilesUpdated_newKeyIsAuthorized() throws Exception {
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+
+        // Back up the existing key files
+        Path tempXmlFile = Files.createTempFile("adbKeyXmlFile", ".tmp");
+        Path tempAdbKeysFile = Files.createTempFile("adb_keys", ".tmp");
+        Files.copy(mAdbKeyXmlFile.toPath(), tempXmlFile, StandardCopyOption.REPLACE_EXISTING);
+        Files.copy(mAdbKeyFile.toPath(), tempAdbKeysFile, StandardCopyOption.REPLACE_EXISTING);
+
+        // Delete the existing key files
+        Files.delete(mAdbKeyXmlFile.toPath());
+        Files.delete(mAdbKeyFile.toPath());
+
+        // Notify the manager that adb key files have changed.
+        mManager.notifyKeyFilesUpdated();
+        flushHandlerQueue();
+
+        // Copy the files back
+        Files.copy(tempXmlFile, mAdbKeyXmlFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+        Files.copy(tempAdbKeysFile, mAdbKeyFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+
+        // Tell the manager that the key files have changed.
+        mManager.notifyKeyFilesUpdated();
+        flushHandlerQueue();
+
+        assertTrue(
+                "Key is not authorized after reloading key files.",
+                mKeyStore.isKeyAuthorized(TEST_KEY_1));
+    }
+
+    @Test
+    public void testAdbKeyStore_adbWifiConnect_storesBssidWhenAlwaysAllow() throws Exception {
+        String trustedNetwork = "My Network";
+        mKeyStore.addTrustedNetwork(trustedNetwork);
+        persistKeyStore();
+
+        AdbDebuggingManager.AdbKeyStore newKeyStore = mManager.new AdbKeyStore();
+
+        assertTrue(
+                "Persisted trusted network not found in new keystore instance.",
+                newKeyStore.isTrustedNetwork(trustedNetwork));
+    }
+
+    @Test
     public void testIsValidMdnsServiceName() {
         // Longer than 15 characters
         assertFalse(isValidMdnsServiceName("abcd1234abcd1234"));
@@ -1030,28 +1133,27 @@
         if (key == null) {
             return false;
         }
+        return adbKeyFileKeys(keyFile).contains(key);
+    }
+
+    private static List<String> adbKeyFileKeys(File keyFile) throws Exception {
+        List<String> keys = new ArrayList<>();
         if (keyFile.exists()) {
             try (BufferedReader in = new BufferedReader(new FileReader(keyFile))) {
                 String currKey;
                 while ((currKey = in.readLine()) != null) {
-                    if (key.equals(currKey)) {
-                        return true;
-                    }
+                    keys.add(currKey);
                 }
             }
         }
-        return false;
+        return keys;
     }
 
     /**
      * Helper class that extends AdbDebuggingThread to receive the response from AdbDebuggingManager
      * indicating whether the key should be allowed to  connect.
      */
-    class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread {
-        AdbDebuggingThreadTest() {
-            mManager.super();
-        }
-
+    private class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread {
         @Override
         public void sendResponse(String msg) {
             TestResult result = new TestResult(TestResult.RESULT_RESPONSE_RECEIVED, msg);
@@ -1091,4 +1193,17 @@
             return "{mReturnCode = " + mReturnCode + ", mMessage = " + mMessage + "}";
         }
     }
+
+    private static class FakeTicker implements AdbDebuggingManager.Ticker {
+        private long mCurrentTime;
+
+        private void advance(long milliseconds) {
+            mCurrentTime += milliseconds;
+        }
+
+        @Override
+        public long currentTimeMillis() {
+            return mCurrentTime;
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index 25cf8a8..e95924a 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -20,7 +20,9 @@
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGATIVE;
 
-import static com.android.server.biometrics.BiometricServiceStateProto.*;
+import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
+import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED;
+import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -32,6 +34,8 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -280,6 +284,43 @@
     }
 
     @Test
+    public void testOnDialogAnimatedInDoesNothingDuringInvalidState() throws Exception {
+        setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+        final long operationId = 123;
+        final int userId = 10;
+
+        final AuthSession session = createAuthSession(mSensors,
+                false /* checkDevicePolicyManager */,
+                Authenticators.BIOMETRIC_STRONG,
+                TEST_REQUEST_ID,
+                operationId,
+                userId);
+        final IBiometricAuthenticator impl = session.mPreAuthInfo.eligibleSensors.get(0).impl;
+
+        session.goToInitialState();
+        for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) {
+            assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState());
+            session.onCookieReceived(
+                    session.mPreAuthInfo.eligibleSensors.get(sensor.id).getCookie());
+        }
+        assertTrue(session.allCookiesReceived());
+        assertEquals(STATE_AUTH_STARTED, session.getState());
+        verify(impl, never()).startPreparedClient(anyInt());
+
+        // First invocation should start the client monitor.
+        session.onDialogAnimatedIn();
+        assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
+        verify(impl).startPreparedClient(anyInt());
+
+        // Subsequent invocations should not start the client monitor again.
+        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn();
+        assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
+        verify(impl, times(1)).startPreparedClient(anyInt());
+    }
+
+    @Test
     public void testCancelAuthentication_whenStateAuthCalled_invokesCancel()
             throws RemoteException {
         testInvokesCancel(session -> session.onCancelAuthSession(false /* force */));
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
index c173473..9e9d703 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
@@ -80,11 +80,14 @@
 
     private Handler mHandler;
     private BiometricSchedulerOperation mOperation;
+    private boolean mIsDebuggable;
 
     @Before
     public void setUp() {
         mHandler = new Handler(TestableLooper.get(this).getLooper());
-        mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback);
+        mIsDebuggable = false;
+        mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback,
+                () -> mIsDebuggable);
     }
 
     @Test
@@ -126,6 +129,34 @@
     }
 
     @Test
+    public void testSecondStartWithCookieCrashesWhenDebuggable() {
+        final int cookie = 5;
+        mIsDebuggable = true;
+        when(mClientMonitor.getCookie()).thenReturn(cookie);
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
+        assertThat(started).isTrue();
+
+        assertThrows(IllegalStateException.class,
+                () -> mOperation.startWithCookie(mOnStartCallback, cookie));
+    }
+
+    @Test
+    public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() {
+        final int cookie = 5;
+        mIsDebuggable = false;
+        when(mClientMonitor.getCookie()).thenReturn(cookie);
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
+        assertThat(started).isTrue();
+
+        final boolean startedAgain = mOperation.startWithCookie(mOnStartCallback, cookie);
+        assertThat(startedAgain).isFalse();
+    }
+
+    @Test
     public void startsWhenReadyAndHalAvailable() {
         when(mClientMonitor.getCookie()).thenReturn(0);
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
@@ -170,7 +201,34 @@
     }
 
     @Test
+    public void secondStartCrashesWhenDebuggable() {
+        mIsDebuggable = true;
+        when(mClientMonitor.getCookie()).thenReturn(0);
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        final boolean started = mOperation.start(mOnStartCallback);
+        assertThat(started).isTrue();
+
+        assertThrows(IllegalStateException.class, () -> mOperation.start(mOnStartCallback));
+    }
+
+    @Test
+    public void secondStartFailsNicelyWhenNotDebuggable() {
+        mIsDebuggable = false;
+        when(mClientMonitor.getCookie()).thenReturn(0);
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        final boolean started = mOperation.start(mOnStartCallback);
+        assertThat(started).isTrue();
+
+        final boolean startedAgain = mOperation.start(mOnStartCallback);
+        assertThat(startedAgain).isFalse();
+    }
+
+    @Test
     public void doesNotStartWithCookie() {
+        // This class only throws exceptions when debuggable.
+        mIsDebuggable = true;
         when(mClientMonitor.getCookie()).thenReturn(9);
         assertThrows(IllegalStateException.class,
                 () -> mOperation.start(mock(ClientMonitorCallback.class)));
@@ -178,6 +236,8 @@
 
     @Test
     public void cannotRestart() {
+        // This class only throws exceptions when debuggable.
+        mIsDebuggable = true;
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mOperation.start(mOnStartCallback);
@@ -188,6 +248,8 @@
 
     @Test
     public void abortsNotRunning() {
+        // This class only throws exceptions when debuggable.
+        mIsDebuggable = true;
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mOperation.abort();
@@ -200,7 +262,8 @@
     }
 
     @Test
-    public void cannotAbortRunning() {
+    public void abortCrashesWhenDebuggableIfOperationIsRunning() {
+        mIsDebuggable = true;
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mOperation.start(mOnStartCallback);
@@ -209,6 +272,16 @@
     }
 
     @Test
+    public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() {
+        mIsDebuggable = false;
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        mOperation.start(mOnStartCallback);
+
+        mOperation.abort();
+    }
+
+    @Test
     public void cancel() {
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
@@ -254,6 +327,30 @@
     }
 
     @Test
+    public void cancelCrashesWhenDebuggableIfOperationIsFinished() {
+        mIsDebuggable = true;
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        mOperation.abort();
+        assertThat(mOperation.isFinished()).isTrue();
+
+        final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
+        assertThrows(IllegalStateException.class, () -> mOperation.cancel(mHandler, cancelCb));
+    }
+
+    @Test
+    public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() {
+        mIsDebuggable = false;
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        mOperation.abort();
+        assertThat(mOperation.isFinished()).isTrue();
+
+        final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
+        mOperation.cancel(mHandler, cancelCb);
+    }
+
+    @Test
     public void markCanceling() {
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
diff --git a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
index 75bd2cc..bc2c57e 100644
--- a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
+++ b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
@@ -22,6 +22,7 @@
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
 
 import android.app.usage.TimeSparseArray;
 import android.app.usage.UsageEvents.Event;
@@ -440,6 +441,7 @@
         prevDB.readMappingsLocked();
         prevDB.init(1);
         prevDB.putUsageStats(UsageStatsManager.INTERVAL_DAILY, mIntervalStats);
+        Set<String> prevDBApps = mIntervalStats.packageStats.keySet();
         // Create a backup with a specific version
         byte[] blob = prevDB.getBackupPayload(KEY_USAGE_STATS, version);
         if (version >= 1 && version <= 3) {
@@ -447,6 +449,11 @@
                     "UsageStatsDatabase shouldn't be able to write backups as XML");
             return;
         }
+        if (version < 1 || version > UsageStatsDatabase.BACKUP_VERSION) {
+            assertFalse(blob != null && blob.length != 0,
+                    "UsageStatsDatabase shouldn't be able to write backups for unknown versions");
+            return;
+        }
 
         clearUsageStatsFiles();
 
@@ -454,9 +461,11 @@
         newDB.readMappingsLocked();
         newDB.init(1);
         // Attempt to restore the usage stats from the backup
-        newDB.applyRestoredPayload(KEY_USAGE_STATS, blob);
-        List<IntervalStats> stats = newDB.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, 0, mEndTime,
-                mIntervalStatsVerifier);
+        Set<String> restoredApps = newDB.applyRestoredPayload(KEY_USAGE_STATS, blob);
+        assertTrue(restoredApps.containsAll(prevDBApps),
+                "List of restored apps does not match list backed-up apps list.");
+        List<IntervalStats> stats = newDB.queryUsageStats(
+                UsageStatsManager.INTERVAL_DAILY, 0, mEndTime, mIntervalStatsVerifier);
 
         if (version > UsageStatsDatabase.BACKUP_VERSION || version < 1) {
             assertFalse(stats != null && !stats.isEmpty(),
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index fd1536c..4550b56 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -1622,7 +1622,9 @@
                     ZenModeConfig.toScheduleConditionId(si),
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
-            String id = mZenModeHelperSpy.addAutomaticZenRule("android", zenRule, "test");
+            // We need the package name to be something that's not "android" so there aren't any
+            // existing rules under that package.
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
             assertNotNull(id);
         }
         try {
@@ -1632,12 +1634,41 @@
                     ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
-            String id = mZenModeHelperSpy.addAutomaticZenRule("android", zenRule, "test");
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
             fail("allowed too many rules to be created");
         } catch (IllegalArgumentException e) {
             // yay
         }
+    }
 
+    @Test
+    public void testAddAutomaticZenRule_beyondSystemLimit_differentComponents() {
+        // Make sure the system limit is enforced per-package even with different component provider
+        // names.
+        for (int i = 0; i < RULE_LIMIT_PER_PACKAGE; i++) {
+            ScheduleInfo si = new ScheduleInfo();
+            si.startHour = i;
+            AutomaticZenRule zenRule = new AutomaticZenRule("name" + i,
+                    null,
+                    new ComponentName("android", "ScheduleConditionProvider" + i),
+                    ZenModeConfig.toScheduleConditionId(si),
+                    new ZenPolicy.Builder().build(),
+                    NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
+            assertNotNull(id);
+        }
+        try {
+            AutomaticZenRule zenRule = new AutomaticZenRule("name",
+                    null,
+                    new ComponentName("android", "ScheduleConditionProviderFinal"),
+                    ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                    new ZenPolicy.Builder().build(),
+                    NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
+            fail("allowed too many rules to be created");
+        } catch (IllegalArgumentException e) {
+            // yay
+        }
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index cfeaf85..0c3b270 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -29,6 +29,8 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
+import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
+import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
 import static android.content.pm.ActivityInfo.FLAG_SUPPORTS_PICTURE_IN_PICTURE;
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_ALWAYS;
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT;
@@ -182,6 +184,10 @@
 
     private final String mPackageName = getInstrumentation().getTargetContext().getPackageName();
 
+    private static final int ORIENTATION_CONFIG_CHANGES =
+            CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT | CONFIG_SCREEN_SIZE
+                    | CONFIG_SMALLEST_SCREEN_SIZE;
+
     @Before
     public void setUp() throws Exception {
         setBooted(mAtm);
@@ -487,7 +493,7 @@
     public void testSetRequestedOrientationUpdatesConfiguration() throws Exception {
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
-                .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT)
+                .setConfigChanges(ORIENTATION_CONFIG_CHANGES)
                 .build();
         activity.setState(RESUMED, "Testing");
 
@@ -710,7 +716,7 @@
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
                 .setLaunchTaskBehind(true)
-                .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT)
+                .setConfigChanges(ORIENTATION_CONFIG_CHANGES)
                 .build();
         final Task task = activity.getTask();
         activity.setState(STOPPED, "Testing");
@@ -779,7 +785,7 @@
                     }
 
                     @Override
-                    public void onAnimationCancelled() {
+                    public void onAnimationCancelled(boolean isKeyguardOccluded) {
                     }
                 }, 0, 0));
         activity.updateOptionsLocked(opts);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 1176786..c78bc59 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -37,6 +37,7 @@
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
 import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
+import static android.content.pm.ActivityInfo.FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING;
 import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
 import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
 import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK;
@@ -52,6 +53,11 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.ActivityStarter.canEmbedActivity;
+import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
+import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION;
+import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK;
+import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST;
 import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
@@ -59,6 +65,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -87,6 +94,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.service.voice.IVoiceInteractionSession;
 import android.util.Pair;
+import android.util.Size;
 import android.view.Gravity;
 import android.window.TaskFragmentOrganizerToken;
 
@@ -1171,6 +1179,7 @@
                 null /* inTask */, taskFragment);
 
         assertFalse(taskFragment.hasChild());
+        assertNotNull("Target record must be started on Task.", targetRecord.getParent().asTask());
     }
 
     @Test
@@ -1341,6 +1350,58 @@
                 any());
     }
 
+    @Test
+    public void testCanEmbedActivity() {
+        final Size minDimensions = new Size(1000, 1000);
+        final WindowLayout windowLayout = new WindowLayout(0, 0, 0, 0, 0,
+                minDimensions.getWidth(), minDimensions.getHeight());
+        final ActivityRecord starting = new ActivityBuilder(mAtm)
+                .setUid(UNIMPORTANT_UID)
+                .setWindowLayout(windowLayout)
+                .build();
+
+        // Task fragment hasn't attached to a task yet. Start activity to a new task.
+        TaskFragment taskFragment = new TaskFragmentBuilder(mAtm).build();
+        final Task task = new TaskBuilder(mSupervisor).build();
+
+        assertEquals(EMBEDDING_DISALLOWED_NEW_TASK,
+                canEmbedActivity(taskFragment, starting, task));
+
+        // Starting activity is going to be started on a task different from task fragment's parent
+        // task. Start activity to a new task.
+        task.addChild(taskFragment, POSITION_TOP);
+        final Task newTask = new TaskBuilder(mSupervisor).build();
+
+        assertEquals(EMBEDDING_DISALLOWED_NEW_TASK,
+                canEmbedActivity(taskFragment, starting, newTask));
+
+        // Make task fragment bounds exceed task bounds.
+        final Rect taskBounds = task.getBounds();
+        taskFragment.setBounds(taskBounds.left, taskBounds.top, taskBounds.right + 1,
+                taskBounds.bottom + 1);
+
+        assertEquals(EMBEDDING_DISALLOWED_UNTRUSTED_HOST,
+                canEmbedActivity(taskFragment, starting, task));
+
+        taskFragment.setBounds(taskBounds);
+        starting.info.flags |= FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING;
+
+        assertEquals(EMBEDDING_ALLOWED, canEmbedActivity(taskFragment, starting, task));
+
+        starting.info.flags &= ~FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING;
+        // Set task fragment's uid as the same as starting activity's uid.
+        taskFragment.setTaskFragmentOrganizer(mock(TaskFragmentOrganizerToken.class),
+                UNIMPORTANT_UID, "test");
+
+        assertEquals(EMBEDDING_ALLOWED, canEmbedActivity(taskFragment, starting, task));
+
+        // Make task fragment bounds smaller than starting activity's minimum dimensions
+        taskFragment.setBounds(0, 0, minDimensions.getWidth() - 1, minDimensions.getHeight() - 1);
+
+        assertEquals(EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION,
+                canEmbedActivity(taskFragment, starting, task));
+    }
+
     private static void startActivityInner(ActivityStarter starter, ActivityRecord target,
             ActivityRecord source, ActivityOptions options, Task inTask,
             TaskFragment inTaskFragment) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
index 71f1914..b5764f5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
@@ -87,7 +87,7 @@
         }
 
         @Override
-        public void onAnimationCancelled() {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) {
         }
 
         @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 8656a4f..f2d6273 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -806,7 +806,7 @@
         }
 
         @Override
-        public void onAnimationCancelled() throws RemoteException {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
             mFinishedCallback = null;
         }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 436cf36..7415460 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -522,7 +522,7 @@
         }
 
         @Override
-        public void onAnimationCancelled() {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) {
             mCancelled = true;
         }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index d737963..40e266c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1699,6 +1699,13 @@
         assertFalse(displayContent.mPinnedTaskController.isFreezingTaskConfig(pinnedTask));
         assertEquals(pinnedActivity.getConfiguration().orientation,
                 displayContent.getConfiguration().orientation);
+
+        // No need to apply rotation if the display ignores orientation request.
+        doCallRealMethod().when(displayContent).rotationForActivityInDifferentOrientation(any());
+        pinnedActivity.mOrientation = SCREEN_ORIENTATION_LANDSCAPE;
+        displayContent.setIgnoreOrientationRequest(true);
+        assertEquals(WindowConfiguration.ROTATION_UNDEFINED,
+                displayContent.rotationForActivityInDifferentOrientation(pinnedActivity));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
index 204c7e6..027f521 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -43,6 +43,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -210,7 +211,7 @@
         mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
 
         adapter.onAnimationCancelled(mMockLeash);
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
     }
 
     @Test
@@ -226,7 +227,7 @@
         mClock.fastForward(10500);
         mHandler.timeAdvance();
 
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
         verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION),
                 eq(adapter));
     }
@@ -247,12 +248,12 @@
             mClock.fastForward(10500);
             mHandler.timeAdvance();
 
-            verify(mMockRunner, never()).onAnimationCancelled();
+            verify(mMockRunner, never()).onAnimationCancelled(anyBoolean());
 
             mClock.fastForward(52500);
             mHandler.timeAdvance();
 
-            verify(mMockRunner).onAnimationCancelled();
+            verify(mMockRunner).onAnimationCancelled(anyBoolean());
             verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION),
                     eq(adapter));
         } finally {
@@ -264,7 +265,7 @@
     public void testZeroAnimations() throws Exception {
         mController.goodToGo(TRANSIT_OLD_NONE);
         verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any());
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
     }
 
     @Test
@@ -274,7 +275,7 @@
                 new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false);
         mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
         verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any());
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
     }
 
     @Test
@@ -316,7 +317,7 @@
         win.mActivityRecord.removeImmediately();
         mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
         verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any());
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
         verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION),
                 eq(adapter));
     }
@@ -574,7 +575,7 @@
 
             // Cancel the wallpaper window animator and ensure the runner is not canceled
             wallpaperWindowToken.cancelAnimation();
-            verify(mMockRunner, never()).onAnimationCancelled();
+            verify(mMockRunner, never()).onAnimationCancelled(anyBoolean());
         } finally {
             mDisplayContent.mOpeningApps.clear();
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index 2a9fcb9..7f09606 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -762,12 +762,20 @@
         Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
                 ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */,
                 0 /* launchFlags */, candidateTask);
-        assertSame(rootTask, actualRootTask.getRootTask());
+        assertSame(rootTask, actualRootTask);
 
         // Verify the launch root task without candidate task
         actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
                 ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */,
                 0 /* launchFlags */);
-        assertSame(adjacentRootTask, actualRootTask.getRootTask());
+        assertSame(adjacentRootTask, actualRootTask);
+
+        final Task pinnedTask = createTask(
+                mDisplayContent, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
+        // Verify not adjusting launch target for pinned candidate task
+        actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
+                ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */,
+                0 /* launchFlags */, pinnedTask /* candidateTask */);
+        assertNull(actualRootTask);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 1c3b869..e47bcc9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -21,6 +21,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.testing.Assert.assertThrows;
 
@@ -530,7 +531,7 @@
         mWindowOrganizerController.mLaunchTaskFragments
                 .put(mFragmentToken, mTaskFragment);
         mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token);
-        doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity);
+        doReturn(EMBEDDING_ALLOWED).when(mTaskFragment).isAllowedToEmbedActivity(activity);
         clearInvocations(mAtm.mRootWindowContainer);
 
         mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
@@ -920,7 +921,6 @@
                 .setOrganizer(mOrganizer)
                 .setBounds(mTaskFragBounds)
                 .build();
-        doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity);
         mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
 
         // Reparent activity to mTaskFragment, which is smaller than activity's
@@ -956,7 +956,6 @@
                 .setOrganizer(mOrganizer)
                 .setBounds(mTaskFragBounds)
                 .build();
-        doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity);
         mWindowOrganizerController.mLaunchTaskFragments.put(oldFragToken, oldTaskFrag);
         mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 228cb65..5f30963 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -475,5 +475,13 @@
         assertFalse(activity0.isLetterboxedForFixedOrientationAndAspectRatio());
         assertFalse(activity1.isLetterboxedForFixedOrientationAndAspectRatio());
         assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation());
+
+        tf0.setResumedActivity(activity0, "test");
+        tf1.setResumedActivity(activity1, "test");
+        mDisplayContent.mFocusedApp = activity1;
+
+        // Making the activity0 be the focused activity and ensure the focused app is updated.
+        activity0.moveFocusableActivityToTop("test");
+        assertEquals(activity0, mDisplayContent.mFocusedApp);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 5743922..1715a29 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -979,7 +979,7 @@
                     }
 
                     @Override
-                    public void onAnimationCancelled() {
+                    public void onAnimationCancelled(boolean isKeyguardOccluded) {
                     }
                 }, 0, 0, false);
         adapter.setCallingPidUid(123, 456);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index e09a94f..1a64f5e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -61,6 +61,7 @@
 import android.view.InsetsVisibilities;
 import android.view.View;
 import android.view.WindowManager;
+import android.window.WindowContainerToken;
 
 import androidx.test.filters.SmallTest;
 
@@ -317,4 +318,76 @@
         verify(mWm.mInputManager).setInTouchMode(
                 !currentTouchMode, callingPid, callingUid, /* hasPermission= */ false);
     }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_nullCookie() {
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(null);
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_invalidCookie() {
+        Binder cookie = new Binder("test cookie");
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie);
+        assertThat(wct).isNull();
+
+        final ActivityRecord testActivity = new ActivityBuilder(mAtm)
+                .setCreateTask(true)
+                .build();
+
+        wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie);
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_validCookie() {
+        final Binder cookie = new Binder("ginger cookie");
+        final WindowContainerToken launchRootTask = mock(WindowContainerToken.class);
+        setupActivityWithLaunchCookie(cookie, launchRootTask);
+
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie);
+        assertThat(wct).isEqualTo(launchRootTask);
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies() {
+        final Binder cookie1 = new Binder("ginger cookie");
+        final WindowContainerToken launchRootTask1 = mock(WindowContainerToken.class);
+        setupActivityWithLaunchCookie(cookie1, launchRootTask1);
+
+        setupActivityWithLaunchCookie(new Binder("choc chip cookie"),
+                mock(WindowContainerToken.class));
+
+        setupActivityWithLaunchCookie(new Binder("peanut butter cookie"),
+                mock(WindowContainerToken.class));
+
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie1);
+        assertThat(wct).isEqualTo(launchRootTask1);
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies_noneValid() {
+        setupActivityWithLaunchCookie(new Binder("ginger cookie"),
+                mock(WindowContainerToken.class));
+
+        setupActivityWithLaunchCookie(new Binder("choc chip cookie"),
+                mock(WindowContainerToken.class));
+
+        setupActivityWithLaunchCookie(new Binder("peanut butter cookie"),
+                mock(WindowContainerToken.class));
+
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(
+                new Binder("some other cookie"));
+        assertThat(wct).isNull();
+    }
+
+    private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) {
+        final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class);
+        when(remoteToken.toWindowContainerToken()).thenReturn(wct);
+        final ActivityRecord testActivity = new ActivityBuilder(mAtm)
+                .setCreateTask(true)
+                .build();
+        testActivity.mLaunchCookie = launchCookie;
+        testActivity.getTask().mRemoteToken = remoteToken;
+    }
 }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index cc33f88..26a1e9d 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -16,6 +16,7 @@
 
 package com.android.server.usage;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.usage.TimeSparseArray;
 import android.app.usage.UsageEvents;
@@ -24,6 +25,7 @@
 import android.os.Build;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -55,8 +57,11 @@
 import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Provides an interface to query for UsageStat data from a Protocol Buffer database.
@@ -1252,6 +1257,10 @@
             Slog.wtf(TAG, "Attempting to backup UsageStats as XML with version " + version);
             return null;
         }
+        if (version < 1 || version > BACKUP_VERSION) {
+            Slog.wtf(TAG, "Attempting to backup UsageStats with an unknown version: " + version);
+            return null;
+        }
         synchronized (mLock) {
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             if (KEY_USAGE_STATS.equals(key)) {
@@ -1300,14 +1309,26 @@
             }
             return baos.toByteArray();
         }
+    }
 
+    /**
+     * Updates the set of packages given to only include those that have been used within the
+     * given timeframe (as defined by {@link UsageStats#getLastTimePackageUsed()}).
+     */
+    private void calculatePackagesUsedWithinTimeframe(
+            IntervalStats stats, Set<String> packagesList, long timeframeMs) {
+        for (UsageStats stat : stats.packageStats.values()) {
+            if (stat.getLastTimePackageUsed() > timeframeMs) {
+                packagesList.add(stat.mPackageName);
+            }
+        }
     }
 
     /**
      * @hide
      */
     @VisibleForTesting
-    public void applyRestoredPayload(String key, byte[] payload) {
+    public @NonNull Set<String> applyRestoredPayload(String key, byte[] payload) {
         synchronized (mLock) {
             if (KEY_USAGE_STATS.equals(key)) {
                 // Read stats files for the current device configs
@@ -1320,12 +1341,15 @@
                 IntervalStats yearlyConfigSource =
                         getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);
 
+                final Set<String> packagesRestored = new ArraySet<>();
                 try {
                     DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
                     int backupDataVersion = in.readInt();
 
                     // Can't handle this backup set
-                    if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return;
+                    if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) {
+                        return packagesRestored;
+                    }
 
                     // Delete all stats files
                     // Do this after reading version and before actually restoring
@@ -1333,10 +1357,14 @@
                         deleteDirectoryContents(mIntervalDirs[i]);
                     }
 
+                    // 90 days before today in epoch
+                    final long timeframe = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(90);
                     int fileCount = in.readInt();
                     for (int i = 0; i < fileCount; i++) {
                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                 backupDataVersion);
+                        calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
+                        packagesRestored.addAll(stats.packageStats.keySet());
                         stats = mergeStats(stats, dailyConfigSource);
                         putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats);
                     }
@@ -1345,6 +1373,7 @@
                     for (int i = 0; i < fileCount; i++) {
                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                 backupDataVersion);
+                        calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
                         stats = mergeStats(stats, weeklyConfigSource);
                         putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats);
                     }
@@ -1353,6 +1382,7 @@
                     for (int i = 0; i < fileCount; i++) {
                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                 backupDataVersion);
+                        calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
                         stats = mergeStats(stats, monthlyConfigSource);
                         putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats);
                     }
@@ -1361,6 +1391,7 @@
                     for (int i = 0; i < fileCount; i++) {
                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                 backupDataVersion);
+                        calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
                         stats = mergeStats(stats, yearlyConfigSource);
                         putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats);
                     }
@@ -1370,7 +1401,9 @@
                 } finally {
                     indexFilesLocked();
                 }
+                return packagesRestored;
             }
+            return Collections.EMPTY_SET;
         }
     }
 
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index ef13cd9..f595c3d 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -3034,7 +3034,8 @@
                     if (userStats == null) {
                         return; // user was stopped or removed
                     }
-                    userStats.applyRestoredPayload(key, payload);
+                    final Set<String> restoredApps = userStats.applyRestoredPayload(key, payload);
+                    mAppStandby.restoreAppsToRare(restoredApps, user);
                 }
             }
         }
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index c609add..34c6c16 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -63,6 +63,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A per-user UsageStatsService. All methods are meant to be called with the main lock held
@@ -1374,8 +1375,8 @@
         return mDatabase.getBackupPayload(key);
     }
 
-    void applyRestoredPayload(String key, byte[] payload){
+    Set<String> applyRestoredPayload(String key, byte[] payload) {
         checkAndGetTimeLocked();
-        mDatabase.applyRestoredPayload(key, payload);
+        return mDatabase.applyRestoredPayload(key, payload);
     }
 }