Merge "No onMultiWindowModeChanged callback from split to PiP" into rvc-dev
diff --git a/ApiDocs.bp b/ApiDocs.bp
index 90df19a..a81342a 100644
--- a/ApiDocs.bp
+++ b/ApiDocs.bp
@@ -66,7 +66,7 @@
         ":opt-telephony-srcs",
         ":opt-net-voip-srcs",
         ":art-module-public-api-stubs-source",
-        ":conscrypt.module.public.api.stubs.source",
+        ":conscrypt.module.public.api{.public.stubs.source}",
         ":android_icu4j_public_api_files",
         "test-mock/src/**/*.java",
         "test-runner/src/**/*.java",
diff --git a/StubLibraries.bp b/StubLibraries.bp
index 6e6efe5..c0197c4 100644
--- a/StubLibraries.bp
+++ b/StubLibraries.bp
@@ -69,7 +69,7 @@
     name: "metalava-full-api-stubs-default",
     defaults: ["metalava-base-api-stubs-default"],
     srcs: [
-        ":conscrypt.module.public.api.stubs.source",
+        ":conscrypt.module.public.api{.public.stubs.source}",
         ":framework-updatable-sources",
     ],
     sdk_version: "core_platform",
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
index 49b3ec1..cea7fcc 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
@@ -212,7 +212,10 @@
     }
 
     boolean isAccessAllowedForCaller(@NonNull String callingPackage, int callingUid) {
-        // TODO: verify blob is still valid (expiryTime is not elapsed)
+        // Don't allow the blob to be accessed after it's expiry time has passed.
+        if (getBlobHandle().isExpired()) {
+            return false;
+        }
         synchronized (mMetadataLock) {
             // Check if packageName already holds a lease on the blob.
             for (int i = 0, size = mLeasees.size(); i < size; ++i) {
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
index 7a27b2c..a2bce31 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -1059,6 +1059,18 @@
         }
     }
 
+    boolean isBlobAvailable(long blobId, int userId) {
+        synchronized (mBlobsLock) {
+            final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(userId);
+            for (BlobMetadata blobMetadata : userBlobs.values()) {
+                if (blobMetadata.getBlobId() == blobId) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
     @GuardedBy("mBlobsLock")
     private void dumpSessionsLocked(IndentingPrintWriter fout, DumpArgs dumpArgs) {
         for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) {
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
index 72af323..a4a2e80 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
@@ -46,6 +46,8 @@
                 return runDeleteBlob(pw);
             case "idle-maintenance":
                 return runIdleMaintenance(pw);
+            case "query-blob-existence":
+                return runQueryBlobExistence(pw);
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -91,6 +93,16 @@
         return 0;
     }
 
+    private int runQueryBlobExistence(PrintWriter pw) {
+        final ParsedArgs args = new ParsedArgs();
+        if (parseOptions(pw, args) < 0) {
+            return -1;
+        }
+
+        pw.println(mService.isBlobAvailable(args.blobId, args.userId) ? 1 : 0);
+        return 0;
+    }
+
     @Override
     public void onHelp() {
         final PrintWriter pw = getOutPrintWriter();
@@ -121,6 +133,8 @@
         pw.println("      --tag: Tag of the blob to delete.");
         pw.println("idle-maintenance");
         pw.println("    Run idle maintenance which takes care of removing stale data.");
+        pw.println("query-blob-existence [-b BLOB_ID]");
+        pw.println("    Prints 1 if blob exists, otherwise 0.");
         pw.println();
     }
 
@@ -147,6 +161,9 @@
                 case "--tag":
                     args.tag = getNextArgRequired();
                     break;
+                case "-b":
+                    args.blobId = Long.parseLong(getNextArgRequired());
+                    break;
                 default:
                     pw.println("Error: unknown option '" + opt + "'");
                     return -1;
@@ -166,6 +183,7 @@
         public long expiryTimeMillis;
         public CharSequence label;
         public String tag;
+        public long blobId;
 
         public BlobHandle getBlobHandle() {
             return BlobHandle.create(algorithm, digest, label, expiryTimeMillis, tag);
diff --git a/api/test-current.txt b/api/test-current.txt
index 5dc7bdb..a163dea 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1728,6 +1728,15 @@
 
 }
 
+package android.media.tv {
+
+  public final class TvInputManager {
+    method public void addHardwareDevice(int);
+    method public void removeHardwareDevice(int);
+  }
+
+}
+
 package android.metrics {
 
   public class LogMaker {
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index ecb95bd..3bcabe5 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -48,6 +48,7 @@
 #include <ui/Region.h>
 
 #include <gui/ISurfaceComposer.h>
+#include <gui/DisplayEventReceiver.h>
 #include <gui/Surface.h>
 #include <gui/SurfaceComposerClient.h>
 
@@ -113,8 +114,8 @@
 // ---------------------------------------------------------------------------
 
 BootAnimation::BootAnimation(sp<Callbacks> callbacks)
-        : Thread(false), mClockEnabled(true), mTimeIsAccurate(false),
-        mTimeFormat12Hour(false), mTimeCheckThread(nullptr), mCallbacks(callbacks) {
+        : Thread(false), mClockEnabled(true), mTimeIsAccurate(false), mTimeFormat12Hour(false),
+        mTimeCheckThread(nullptr), mCallbacks(callbacks), mLooper(new Looper(false)) {
     mSession = new SurfaceComposerClient();
 
     std::string powerCtl = android::base::GetProperty("sys.powerctl", "");
@@ -154,8 +155,7 @@
     return mSession;
 }
 
-void BootAnimation::binderDied(const wp<IBinder>&)
-{
+void BootAnimation::binderDied(const wp<IBinder>&) {
     // woah, surfaceflinger died!
     SLOGD("SurfaceFlinger died, exiting...");
 
@@ -219,8 +219,7 @@
     return NO_ERROR;
 }
 
-status_t BootAnimation::initTexture(FileMap* map, int* width, int* height)
-{
+status_t BootAnimation::initTexture(FileMap* map, int* width, int* height) {
     SkBitmap bitmap;
     sk_sp<SkData> data = SkData::MakeWithoutCopy(map->getDataPtr(),
             map->getDataLength());
@@ -278,6 +277,78 @@
     return NO_ERROR;
 }
 
+class BootAnimation::DisplayEventCallback : public LooperCallback {
+    BootAnimation* mBootAnimation;
+
+public:
+    DisplayEventCallback(BootAnimation* bootAnimation) {
+        mBootAnimation = bootAnimation;
+    }
+
+    int handleEvent(int /* fd */, int events, void* /* data */) {
+        if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) {
+            ALOGE("Display event receiver pipe was closed or an error occurred. events=0x%x",
+                    events);
+            return 0; // remove the callback
+        }
+
+        if (!(events & Looper::EVENT_INPUT)) {
+            ALOGW("Received spurious callback for unhandled poll event.  events=0x%x", events);
+            return 1; // keep the callback
+        }
+
+        constexpr int kBufferSize = 100;
+        DisplayEventReceiver::Event buffer[kBufferSize];
+        ssize_t numEvents;
+        do {
+            numEvents = mBootAnimation->mDisplayEventReceiver->getEvents(buffer, kBufferSize);
+            for (size_t i = 0; i < static_cast<size_t>(numEvents); i++) {
+                const auto& event = buffer[i];
+                if (event.header.type == DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG) {
+                    SLOGV("Hotplug received");
+
+                    if (!event.hotplug.connected) {
+                        // ignore hotplug disconnect
+                        continue;
+                    }
+                    auto token = SurfaceComposerClient::getPhysicalDisplayToken(
+                        event.header.displayId);
+
+                    if (token != mBootAnimation->mDisplayToken) {
+                        // ignore hotplug of a secondary display
+                        continue;
+                    }
+
+                    DisplayConfig displayConfig;
+                    const status_t error = SurfaceComposerClient::getActiveDisplayConfig(
+                        mBootAnimation->mDisplayToken, &displayConfig);
+                    if (error != NO_ERROR) {
+                        SLOGE("Can't get active display configuration.");
+                    }
+                    mBootAnimation->resizeSurface(displayConfig.resolution.getWidth(),
+                        displayConfig.resolution.getHeight());
+                }
+            }
+        } while (numEvents > 0);
+
+        return 1;  // keep the callback
+    }
+};
+
+EGLConfig BootAnimation::getEglConfig(const EGLDisplay& display) {
+    const EGLint attribs[] = {
+        EGL_RED_SIZE,   8,
+        EGL_GREEN_SIZE, 8,
+        EGL_BLUE_SIZE,  8,
+        EGL_DEPTH_SIZE, 0,
+        EGL_NONE
+    };
+    EGLint numConfigs;
+    EGLConfig config;
+    eglChooseConfig(display, attribs, &config, 1, &numConfigs);
+    return config;
+}
+
 status_t BootAnimation::readyToRun() {
     mAssets.addDefaultAssets();
 
@@ -346,25 +417,12 @@
     sp<Surface> s = control->getSurface();
 
     // initialize opengl and egl
-    const EGLint attribs[] = {
-            EGL_RED_SIZE,   8,
-            EGL_GREEN_SIZE, 8,
-            EGL_BLUE_SIZE,  8,
-            EGL_DEPTH_SIZE, 0,
-            EGL_NONE
-    };
-    EGLint w, h;
-    EGLint numConfigs;
-    EGLConfig config;
-    EGLSurface surface;
-    EGLContext context;
-
     EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
-
     eglInitialize(display, nullptr, nullptr);
-    eglChooseConfig(display, attribs, &config, 1, &numConfigs);
-    surface = eglCreateWindowSurface(display, config, s.get(), nullptr);
-    context = eglCreateContext(display, config, nullptr, nullptr);
+    EGLConfig config = getEglConfig(display);
+    EGLSurface surface = eglCreateWindowSurface(display, config, s.get(), nullptr);
+    EGLContext context = eglCreateContext(display, config, nullptr, nullptr);
+    EGLint w, h;
     eglQuerySurface(display, surface, EGL_WIDTH, &w);
     eglQuerySurface(display, surface, EGL_HEIGHT, &h);
 
@@ -380,9 +438,46 @@
     mFlingerSurface = s;
     mTargetInset = -1;
 
+    // Register a display event receiver
+    mDisplayEventReceiver = std::make_unique<DisplayEventReceiver>();
+    status_t status = mDisplayEventReceiver->initCheck();
+    SLOGE_IF(status != NO_ERROR, "Initialization of DisplayEventReceiver failed with status: %d",
+            status);
+    mLooper->addFd(mDisplayEventReceiver->getFd(), 0, Looper::EVENT_INPUT,
+            new DisplayEventCallback(this), nullptr);
+
     return NO_ERROR;
 }
 
+void BootAnimation::resizeSurface(int newWidth, int newHeight) {
+    // We assume this function is called on the animation thread.
+    if (newWidth == mWidth && newHeight == mHeight) {
+        return;
+    }
+    SLOGV("Resizing the boot animation surface to %d %d", newWidth, newHeight);
+
+    eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+    eglDestroySurface(mDisplay, mSurface);
+
+    mWidth = newWidth;
+    mHeight = newHeight;
+
+    SurfaceComposerClient::Transaction t;
+    t.setSize(mFlingerSurfaceControl, mWidth, mHeight);
+    t.apply();
+
+    EGLConfig config = getEglConfig(mDisplay);
+    EGLSurface surface = eglCreateWindowSurface(mDisplay, config, mFlingerSurface.get(), nullptr);
+    if (eglMakeCurrent(mDisplay, surface, surface, mContext) == EGL_FALSE) {
+        SLOGE("Can't make the new surface current. Error %d", eglGetError());
+        return;
+    }
+    glViewport(0, 0, mWidth, mHeight);
+    glScissor(0, 0, mWidth, mHeight);
+
+    mSurface = surface;
+}
+
 bool BootAnimation::preloadAnimation() {
     findBootAnimationFile();
     if (!mZipFileName.isEmpty()) {
@@ -443,15 +538,14 @@
     }
 }
 
-bool BootAnimation::threadLoop()
-{
-    bool r;
+bool BootAnimation::threadLoop() {
+    bool result;
     // We have no bootanimation file, so we use the stock android logo
     // animation.
     if (mZipFileName.isEmpty()) {
-        r = android();
+        result = android();
     } else {
-        r = movie();
+        result = movie();
     }
 
     eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
@@ -462,11 +556,10 @@
     eglTerminate(mDisplay);
     eglReleaseThread();
     IPCThreadState::self()->stopProcess();
-    return r;
+    return result;
 }
 
-bool BootAnimation::android()
-{
+bool BootAnimation::android() {
     SLOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
             elapsedRealtime());
     initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png");
@@ -485,19 +578,19 @@
     glEnable(GL_TEXTURE_2D);
     glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
 
-    const GLint xc = (mWidth  - mAndroid[0].w) / 2;
-    const GLint yc = (mHeight - mAndroid[0].h) / 2;
-    const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
-
-    glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
-            updateRect.height());
-
     // Blend state
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
     glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
 
     const nsecs_t startTime = systemTime();
     do {
+        processDisplayEvents();
+        const GLint xc = (mWidth  - mAndroid[0].w) / 2;
+        const GLint yc = (mHeight - mAndroid[0].h) / 2;
+        const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
+        glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
+                updateRect.height());
+
         nsecs_t now = systemTime();
         double time = now - startTime;
         float t = 4.0f * float(time / us2ns(16667)) / mAndroid[1].w;
@@ -612,8 +705,7 @@
 }
 
 
-static bool readFile(ZipFileRO* zip, const char* name, String8& outString)
-{
+static bool readFile(ZipFileRO* zip, const char* name, String8& outString) {
     ZipEntryRO entry = zip->findEntryByName(name);
     SLOGE_IF(!entry, "couldn't find %s", name);
     if (!entry) {
@@ -734,8 +826,7 @@
     drawText(out, font, false, &x, &y);
 }
 
-bool BootAnimation::parseAnimationDesc(Animation& animation)
-{
+bool BootAnimation::parseAnimationDesc(Animation& animation)  {
     String8 desString;
 
     if (!readFile(animation.zip, "desc.txt", desString)) {
@@ -802,8 +893,7 @@
     return true;
 }
 
-bool BootAnimation::preloadZip(Animation& animation)
-{
+bool BootAnimation::preloadZip(Animation& animation) {
     // read all the data structures
     const size_t pcount = animation.parts.size();
     void *cookie = nullptr;
@@ -900,8 +990,7 @@
     return true;
 }
 
-bool BootAnimation::movie()
-{
+bool BootAnimation::movie() {
     if (mAnimation == nullptr) {
         mAnimation = loadAnimation(mZipFileName);
     }
@@ -987,12 +1076,9 @@
     return false;
 }
 
-bool BootAnimation::playAnimation(const Animation& animation)
-{
+bool BootAnimation::playAnimation(const Animation& animation) {
     const size_t pcount = animation.parts.size();
     nsecs_t frameDuration = s2ns(1) / animation.fps;
-    const int animationX = (mWidth - animation.width) / 2;
-    const int animationY = (mHeight - animation.height) / 2;
 
     SLOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
             elapsedRealtime());
@@ -1023,6 +1109,11 @@
                     1.0f);
 
             for (size_t j=0 ; j<fcount && (!exitPending() || part.playUntilComplete) ; j++) {
+                processDisplayEvents();
+
+                const int animationX = (mWidth - animation.width) / 2;
+                const int animationY = (mHeight - animation.height) / 2;
+
                 const Animation::Frame& frame(part.frames[j]);
                 nsecs_t lastFrame = systemTime();
 
@@ -1106,6 +1197,12 @@
     return true;
 }
 
+void BootAnimation::processDisplayEvents() {
+    // This will poll mDisplayEventReceiver and if there are new events it'll call
+    // displayEventCallback synchronously.
+    mLooper->pollOnce(0);
+}
+
 void BootAnimation::handleViewport(nsecs_t timestep) {
     if (mShuttingDown || !mFlingerSurfaceControl || mTargetInset == 0) {
         return;
@@ -1148,8 +1245,7 @@
     mCurrentInset += delta;
 }
 
-void BootAnimation::releaseAnimation(Animation* animation) const
-{
+void BootAnimation::releaseAnimation(Animation* animation) const {
     for (Vector<Animation::Part>::iterator it = animation->parts.begin(),
          e = animation->parts.end(); it != e; ++it) {
         if (it->animation)
@@ -1160,8 +1256,7 @@
     delete animation;
 }
 
-BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn)
-{
+BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn) {
     if (mLoadedFiles.indexOf(fn) >= 0) {
         SLOGE("File \"%s\" is already loaded. Cyclic ref is not allowed",
             fn.string());
@@ -1323,5 +1418,4 @@
 
 // ---------------------------------------------------------------------------
 
-}
-; // namespace android
+} // namespace android
diff --git a/cmds/bootanimation/BootAnimation.h b/cmds/bootanimation/BootAnimation.h
index 574d65e..36cd91b 100644
--- a/cmds/bootanimation/BootAnimation.h
+++ b/cmds/bootanimation/BootAnimation.h
@@ -18,11 +18,14 @@
 #define ANDROID_BOOTANIMATION_H
 
 #include <vector>
+#include <queue>
 
 #include <stdint.h>
 #include <sys/types.h>
 
 #include <androidfw/AssetManager.h>
+#include <gui/DisplayEventReceiver.h>
+#include <utils/Looper.h>
 #include <utils/Thread.h>
 #include <binder/IBinder.h>
 
@@ -145,6 +148,11 @@
         BootAnimation* mBootAnimation;
     };
 
+    // Display event handling
+    class DisplayEventCallback;
+    int displayEventCallback(int fd, int events, void* data);
+    void processDisplayEvents();
+
     status_t initTexture(Texture* texture, AssetManager& asset, const char* name);
     status_t initTexture(FileMap* map, int* width, int* height);
     status_t initFont(Font* font, const char* fallback);
@@ -161,6 +169,8 @@
     void findBootAnimationFile();
     bool findBootAnimationFileInternal(const std::vector<std::string>& files);
     bool preloadAnimation();
+    EGLConfig getEglConfig(const EGLDisplay&);
+    void resizeSurface(int newWidth, int newHeight);
 
     void checkExit();
 
@@ -189,6 +199,8 @@
     sp<TimeCheckThread> mTimeCheckThread = nullptr;
     sp<Callbacks> mCallbacks;
     Animation* mAnimation = nullptr;
+    std::unique_ptr<DisplayEventReceiver> mDisplayEventReceiver;
+    sp<Looper> mLooper;
 };
 
 // ---------------------------------------------------------------------------
diff --git a/cmds/statsd/Android.bp b/cmds/statsd/Android.bp
index f30ed17c..3dbe413 100644
--- a/cmds/statsd/Android.bp
+++ b/cmds/statsd/Android.bp
@@ -292,6 +292,7 @@
     name: "statsd_test",
     defaults: ["statsd_defaults"],
     test_suites: ["device-tests", "mts"],
+    test_config: "statsd_test.xml",
 
     //TODO(b/153588990): Remove when the build system properly separates
     //32bit and 64bit architectures.
@@ -299,7 +300,10 @@
     multilib: {
         lib64: {
             suffix: "64",
-        }
+        },
+        lib32: {
+            suffix: "32",
+        },
     },
 
     cflags: [
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index 4ffa040..47bab29 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -1298,7 +1298,6 @@
     return Status::ok();
 }
 
-
 void StatsService::statsCompanionServiceDied(void* cookie) {
     auto thiz = static_cast<StatsService*>(cookie);
     thiz->statsCompanionServiceDiedImpl();
diff --git a/cmds/statsd/src/shell/ShellSubscriber.cpp b/cmds/statsd/src/shell/ShellSubscriber.cpp
index 361b161..fd883c2 100644
--- a/cmds/statsd/src/shell/ShellSubscriber.cpp
+++ b/cmds/statsd/src/shell/ShellSubscriber.cpp
@@ -41,13 +41,8 @@
 
     {
         std::unique_lock<std::mutex> lock(mMutex);
-        if (myToken != mToken) {
-            // Some other subscription has already come in. Stop.
-            return;
-        }
         mSubscriptionInfo = mySubscriptionInfo;
-
-        spawnHelperThreadsLocked(mySubscriptionInfo, myToken);
+        spawnHelperThread(myToken);
         waitForSubscriptionToEndLocked(mySubscriptionInfo, myToken, lock, timeoutSec);
 
         if (mSubscriptionInfo == mySubscriptionInfo) {
@@ -57,14 +52,9 @@
     }
 }
 
-void ShellSubscriber::spawnHelperThreadsLocked(shared_ptr<SubscriptionInfo> myInfo, int myToken) {
-    if (!myInfo->mPulledInfo.empty() && myInfo->mPullIntervalMin > 0) {
-        std::thread puller([this, myToken] { startPull(myToken); });
-        puller.detach();
-    }
-
-    std::thread heartbeatSender([this, myToken] { sendHeartbeats(myToken); });
-    heartbeatSender.detach();
+void ShellSubscriber::spawnHelperThread(int myToken) {
+    std::thread t([this, myToken] { pullAndSendHeartbeats(myToken); });
+    t.detach();
 }
 
 void ShellSubscriber::waitForSubscriptionToEndLocked(shared_ptr<SubscriptionInfo> myInfo,
@@ -114,13 +104,7 @@
         subscriptionInfo->mPushedMatchers.push_back(pushed);
     }
 
-    int minInterval = -1;
     for (const auto& pulled : config.pulled()) {
-        // All intervals need to be multiples of the min interval.
-        if (minInterval < 0 || pulled.freq_millis() < minInterval) {
-            minInterval = pulled.freq_millis();
-        }
-
         vector<string> packages;
         vector<int32_t> uids;
         for (const string& pkg : pulled.packages()) {
@@ -136,18 +120,18 @@
                                                    uids);
         VLOG("adding matcher for pulled atom %d", pulled.matcher().atom_id());
     }
-    subscriptionInfo->mPullIntervalMin = minInterval;
 
     return true;
 }
 
-void ShellSubscriber::startPull(int myToken) {
-    VLOG("ShellSubscriber: pull thread %d starting", myToken);
+void ShellSubscriber::pullAndSendHeartbeats(int myToken) {
+    VLOG("ShellSubscriber: helper thread %d starting", myToken);
     while (true) {
+        int64_t sleepTimeMs = INT_MAX;
         {
             std::lock_guard<std::mutex> lock(mMutex);
             if (!mSubscriptionInfo || mToken != myToken) {
-                VLOG("ShellSubscriber: pulling thread %d done!", myToken);
+                VLOG("ShellSubscriber: helper thread %d done!", myToken);
                 return;
             }
 
@@ -168,11 +152,27 @@
 
                 pullInfo.mPrevPullElapsedRealtimeMs = nowMillis;
             }
+
+            // Send a heartbeat, consisting of a data size of 0, if perfd hasn't recently received
+            // data from statsd. When it receives the data size of 0, perfd will not expect any
+            // atoms and recheck whether the subscription should end.
+            if (nowMillis - mLastWriteMs > kMsBetweenHeartbeats) {
+                attemptWriteToPipeLocked(/*dataSize=*/0);
+            }
+
+            // Determine how long to sleep before doing more work.
+            for (PullInfo& pullInfo : mSubscriptionInfo->mPulledInfo) {
+                int64_t nextPullTime = pullInfo.mPrevPullElapsedRealtimeMs + pullInfo.mInterval;
+                int64_t timeBeforePull = nextPullTime - nowMillis; // guaranteed to be non-negative
+                if (timeBeforePull < sleepTimeMs) sleepTimeMs = timeBeforePull;
+            }
+            int64_t timeBeforeHeartbeat = (mLastWriteMs + kMsBetweenHeartbeats) - nowMillis;
+            if (timeBeforeHeartbeat < sleepTimeMs) sleepTimeMs = timeBeforeHeartbeat;
         }
 
-        VLOG("ShellSubscriber: pulling thread %d sleeping for %d ms", myToken,
-             mSubscriptionInfo->mPullIntervalMin);
-        std::this_thread::sleep_for(std::chrono::milliseconds(mSubscriptionInfo->mPullIntervalMin));
+        VLOG("ShellSubscriber: helper thread %d sleeping for %lld ms", myToken,
+             (long long)sleepTimeMs);
+        std::this_thread::sleep_for(std::chrono::milliseconds(sleepTimeMs));
     }
 }
 
@@ -200,7 +200,7 @@
         }
     }
 
-    if (count > 0) attemptWriteToSocketLocked(mProto.size());
+    if (count > 0) attemptWriteToPipeLocked(mProto.size());
 }
 
 void ShellSubscriber::onLogEvent(const LogEvent& event) {
@@ -214,26 +214,24 @@
                                               util::FIELD_COUNT_REPEATED | FIELD_ID_ATOM);
             event.ToProto(mProto);
             mProto.end(atomToken);
-            attemptWriteToSocketLocked(mProto.size());
+            attemptWriteToPipeLocked(mProto.size());
         }
     }
 }
 
-// Tries to write the atom encoded in mProto to the socket. If the write fails
+// Tries to write the atom encoded in mProto to the pipe. If the write fails
 // because the read end of the pipe has closed, signals to other threads that
 // the subscription should end.
-void ShellSubscriber::attemptWriteToSocketLocked(size_t dataSize) {
-    // First write the payload size.
+void ShellSubscriber::attemptWriteToPipeLocked(size_t dataSize) {
+    // First, write the payload size.
     if (!android::base::WriteFully(mSubscriptionInfo->mOutputFd, &dataSize, sizeof(dataSize))) {
         mSubscriptionInfo->mClientAlive = false;
         mSubscriptionShouldEnd.notify_one();
         return;
     }
 
-    if (dataSize == 0) return;
-
-    // Then, write the payload.
-    if (!mProto.flush(mSubscriptionInfo->mOutputFd)) {
+    // Then, write the payload if this is not just a heartbeat.
+    if (dataSize > 0 && !mProto.flush(mSubscriptionInfo->mOutputFd)) {
         mSubscriptionInfo->mClientAlive = false;
         mSubscriptionShouldEnd.notify_one();
         return;
@@ -242,28 +240,6 @@
     mLastWriteMs = getElapsedRealtimeMillis();
 }
 
-// Send a heartbeat, consisting solely of a data size of 0, if perfd has not
-// recently received any writes from statsd. When it receives the data size of
-// 0, perfd will not expect any data and recheck whether the shell command is
-// still running.
-void ShellSubscriber::sendHeartbeats(int myToken) {
-    while (true) {
-        {
-            std::lock_guard<std::mutex> lock(mMutex);
-            if (!mSubscriptionInfo || myToken != mToken) {
-                VLOG("ShellSubscriber: heartbeat thread %d done!", myToken);
-                return;
-            }
-
-            if (getElapsedRealtimeMillis() - mLastWriteMs > kMsBetweenHeartbeats) {
-                VLOG("ShellSubscriber: sending a heartbeat to perfd");
-                attemptWriteToSocketLocked(/*dataSize=*/0);
-            }
-        }
-        std::this_thread::sleep_for(std::chrono::milliseconds(kMsBetweenHeartbeats));
-    }
-}
-
 }  // namespace statsd
 }  // namespace os
 }  // namespace android
diff --git a/cmds/statsd/src/shell/ShellSubscriber.h b/cmds/statsd/src/shell/ShellSubscriber.h
index 26c8a2a..4c05fa7 100644
--- a/cmds/statsd/src/shell/ShellSubscriber.h
+++ b/cmds/statsd/src/shell/ShellSubscriber.h
@@ -92,7 +92,6 @@
         int mOutputFd;
         std::vector<SimpleAtomMatcher> mPushedMatchers;
         std::vector<PullInfo> mPulledInfo;
-        int mPullIntervalMin;
         bool mClientAlive;
     };
 
@@ -100,27 +99,25 @@
 
     bool readConfig(std::shared_ptr<SubscriptionInfo> subscriptionInfo);
 
-    void spawnHelperThreadsLocked(std::shared_ptr<SubscriptionInfo> myInfo, int myToken);
+    void spawnHelperThread(int myToken);
 
     void waitForSubscriptionToEndLocked(std::shared_ptr<SubscriptionInfo> myInfo,
                                         int myToken,
                                         std::unique_lock<std::mutex>& lock,
                                         int timeoutSec);
 
-    void startPull(int myToken);
+    // Helper thread that pulls atoms at a regular frequency and sends
+    // heartbeats to perfd if statsd hasn't recently sent any data. Statsd must
+    // send heartbeats for perfd to escape a blocking read call and recheck if
+    // the user has terminated the subscription.
+    void pullAndSendHeartbeats(int myToken);
 
     void writePulledAtomsLocked(const vector<std::shared_ptr<LogEvent>>& data,
                                 const SimpleAtomMatcher& matcher);
 
     void getUidsForPullAtom(vector<int32_t>* uids, const PullInfo& pullInfo);
 
-    void attemptWriteToSocketLocked(size_t dataSize);
-
-    // Send ocassional heartbeats for two reasons: (a) for statsd to detect when
-    // the read end of the pipe has closed and (b) for perfd to escape a
-    // blocking read call and recheck if the user has terminated the
-    // subscription.
-    void sendHeartbeats(int myToken);
+    void attemptWriteToPipeLocked(size_t dataSize);
 
     sp<UidMap> mUidMap;
 
diff --git a/cmds/statsd/statsd_test.xml b/cmds/statsd/statsd_test.xml
new file mode 100644
index 0000000..8f9bb1c
--- /dev/null
+++ b/cmds/statsd/statsd_test.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs statsd_test.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-native" />
+    <option name="test-suite-tag" value="mts" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+       <option name="cleanup" value="true" />
+       <option name="push" value="statsd_test->/data/local/tmp/statsd_test" />
+       <option name="append-bitness" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="statsd_test" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.os.statsd" />
+    </object>
+</configuration>
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 90206b6..e8ce92d 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3614,6 +3614,7 @@
          * <li>Directional conversations where there is an active speaker and many passive
          * individuals</li>
          * <li>Stream / posting updates from other individuals</li>
+         * <li>Email, document comments, or other conversation types that are not real-time</li>
          * </ul>
          * </p>
          *
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 01cf2b94a..5806876 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -188,6 +188,17 @@
     private static final boolean DEBUG = false;
     private static final boolean VERIFY = false;
 
+    // Per-Cache performance counters. As some cache instances are declared static,
+    @GuardedBy("mLock")
+    private long mHits = 0;
+
+    @GuardedBy("mLock")
+    private long mMisses = 0;
+
+    // Most invalidation is done in a static context, so the counters need to be accessible.
+    @GuardedBy("sCorkLock")
+    private static final HashMap<String, Long> sInvalidates = new HashMap<>();
+
     /**
      * If sEnabled is false then all cache operations are stubbed out.  Set
      * it to false inside test processes.
@@ -265,6 +276,7 @@
             };
         synchronized (sCorkLock) {
             sCaches.put(this, null);
+            sInvalidates.put(propertyName, (long) 0);
         }
     }
 
@@ -365,6 +377,8 @@
             synchronized (mLock) {
                 if (currentNonce == mLastSeenNonce) {
                     cachedResult = mCache.get(query);
+
+                    if (cachedResult != null) mHits++;
                 } else {
                     if (DEBUG) {
                         Log.d(TAG,
@@ -428,6 +442,7 @@
                 if (mLastSeenNonce == currentNonce && result != null) {
                     mCache.put(query, result);
                 }
+                mMisses++;
             }
             return maybeCheckConsistency(query, result);
         }
@@ -531,6 +546,8 @@
                             newValueString));
         }
         SystemProperties.set(name, newValueString);
+        long invalidateCount = sInvalidates.getOrDefault(name, (long) 0);
+        sInvalidates.put(name, ++invalidateCount);
     }
 
     /**
@@ -758,8 +775,16 @@
     }
 
     private void dumpContents(PrintWriter pw, String[] args) {
+        long invalidateCount;
+
+        synchronized (sCorkLock) {
+            invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0);
+        }
+
         synchronized (mLock) {
             pw.println(String.format("  Cache Property Name: %s", cacheName()));
+            pw.println(String.format("    Hits: %d, Misses: %d, Invalidates: %d",
+                    mHits, mMisses, invalidateCount));
             pw.println(String.format("    Last Observed Nonce: %d", mLastSeenNonce));
             pw.println(String.format("    Current Size: %d, Max Size: %d",
                     mCache.entrySet().size(), mMaxEntries));
diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl
index aa29040..389458b 100644
--- a/core/java/android/content/pm/ILauncherApps.aidl
+++ b/core/java/android/content/pm/ILauncherApps.aidl
@@ -99,9 +99,9 @@
             in IShortcutChangeCallback callback);
 
     void cacheShortcuts(String callingPackage, String packageName, in List<String> shortcutIds,
-            in UserHandle user);
+            in UserHandle user, int cacheFlags);
     void uncacheShortcuts(String callingPackage, String packageName, in List<String> shortcutIds,
-            in UserHandle user);
+            in UserHandle user, int cacheFlags);
 
     String getShortcutIconUri(String callingPackage, String packageName, String shortcutId,
             int userId);
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 4299e80..bd1ee27 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -155,6 +155,26 @@
     public static final String EXTRA_PIN_ITEM_REQUEST =
             "android.content.pm.extra.PIN_ITEM_REQUEST";
 
+    /**
+     * Cache shortcuts which are used in notifications.
+     * @hide
+     */
+    public static final int FLAG_CACHE_NOTIFICATION_SHORTCUTS = 0;
+
+    /**
+     * Cache shortcuts which are used in bubbles.
+     * @hide
+     */
+    public static final int FLAG_CACHE_BUBBLE_SHORTCUTS = 1;
+
+    /** @hide */
+    @IntDef(flag = false, prefix = { "FLAG_CACHE_" }, value = {
+            FLAG_CACHE_NOTIFICATION_SHORTCUTS,
+            FLAG_CACHE_BUBBLE_SHORTCUTS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ShortcutCacheFlags {}
+
     private final Context mContext;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private final ILauncherApps mService;
@@ -1109,6 +1129,11 @@
      * @param packageName The target package name.
      * @param shortcutIds The IDs of the shortcut to be cached.
      * @param user The UserHandle of the profile.
+     * @param cacheFlags One of the values in:
+     * <ul>
+     *     <li>{@link #FLAG_CACHE_NOTIFICATION_SHORTCUTS}
+     *     <li>{@link #FLAG_CACHE_BUBBLE_SHORTCUTS}
+     * </ul>
      * @throws IllegalStateException when the user is locked, or when the {@code user} user
      * is locked or not running.
      *
@@ -1118,10 +1143,11 @@
      */
     @RequiresPermission(android.Manifest.permission.ACCESS_SHORTCUTS)
     public void cacheShortcuts(@NonNull String packageName, @NonNull List<String> shortcutIds,
-            @NonNull UserHandle user) {
+            @NonNull UserHandle user, @ShortcutCacheFlags int cacheFlags) {
         logErrorForInvalidProfileAccess(user);
         try {
-            mService.cacheShortcuts(mContext.getPackageName(), packageName, shortcutIds, user);
+            mService.cacheShortcuts(
+                    mContext.getPackageName(), packageName, shortcutIds, user, cacheFlags);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1133,6 +1159,11 @@
      * @param packageName The target package name.
      * @param shortcutIds The IDs of the shortcut to be uncached.
      * @param user The UserHandle of the profile.
+     * @param cacheFlags One of the values in:
+     * <ul>
+     *     <li>{@link #FLAG_CACHE_NOTIFICATION_SHORTCUTS}
+     *     <li>{@link #FLAG_CACHE_BUBBLE_SHORTCUTS}
+     * </ul>
      * @throws IllegalStateException when the user is locked, or when the {@code user} user
      * is locked or not running.
      *
@@ -1142,10 +1173,11 @@
      */
     @RequiresPermission(android.Manifest.permission.ACCESS_SHORTCUTS)
     public void uncacheShortcuts(@NonNull String packageName, @NonNull List<String> shortcutIds,
-            @NonNull UserHandle user) {
+            @NonNull UserHandle user, @ShortcutCacheFlags int cacheFlags) {
         logErrorForInvalidProfileAccess(user);
         try {
-            mService.uncacheShortcuts(mContext.getPackageName(), packageName, shortcutIds, user);
+            mService.uncacheShortcuts(
+                    mContext.getPackageName(), packageName, shortcutIds, user, cacheFlags);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index dcc6cb2..1b3c46f 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -119,12 +119,27 @@
     /** @hide */
     public static final int FLAG_LONG_LIVED = 1 << 13;
 
-    /** @hide */
-    public static final int FLAG_CACHED = 1 << 14;
+    /**
+     * TODO(b/155135057): This is a quick and temporary fix for b/155135890. ShortcutService doesn't
+     *  need to be aware of the outside world. Replace this with a more extensible solution.
+     * @hide
+     */
+    public static final int FLAG_CACHED_NOTIFICATIONS = 1 << 14;
 
     /** @hide */
     public static final int FLAG_HAS_ICON_URI = 1 << 15;
 
+
+    /**
+     * TODO(b/155135057): This is a quick and temporary fix for b/155135890. ShortcutService doesn't
+     *  need to be aware of the outside world. Replace this with a more extensible solution.
+     * @hide
+     */
+    public static final int FLAG_CACHED_BUBBLES = 1 << 30;
+
+    /** @hide */
+    public static final int FLAG_CACHED_ALL = FLAG_CACHED_NOTIFICATIONS | FLAG_CACHED_BUBBLES;
+
     /** @hide */
     @IntDef(flag = true, prefix = { "FLAG_" }, value = {
             FLAG_DYNAMIC,
@@ -141,8 +156,9 @@
             FLAG_ICON_FILE_PENDING_SAVE,
             FLAG_SHADOW,
             FLAG_LONG_LIVED,
-            FLAG_CACHED,
             FLAG_HAS_ICON_URI,
+            FLAG_CACHED_NOTIFICATIONS,
+            FLAG_CACHED_BUBBLES,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ShortcutFlags {}
@@ -1707,13 +1723,13 @@
     }
 
     /** @hide */
-    public void setCached() {
-        addFlags(FLAG_CACHED);
+    public void setCached(@ShortcutFlags int cacheFlag) {
+        addFlags(cacheFlag);
     }
 
     /** Return whether a shortcut is cached. */
     public boolean isCached() {
-        return hasFlags(FLAG_CACHED);
+        return (getFlags() & FLAG_CACHED_ALL) != 0;
     }
 
     /** Return whether a shortcut is dynamic. */
@@ -1807,7 +1823,7 @@
     /** @hide */
     public boolean isAlive() {
         return hasFlags(FLAG_PINNED) || hasFlags(FLAG_DYNAMIC) || hasFlags(FLAG_MANIFEST)
-                || hasFlags(FLAG_CACHED);
+                || isCached();
     }
 
     /** @hide */
diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java
index eee91ce1..c62767e 100644
--- a/core/java/android/content/pm/ShortcutServiceInternal.java
+++ b/core/java/android/content/pm/ShortcutServiceInternal.java
@@ -92,10 +92,10 @@
 
     public abstract void cacheShortcuts(int launcherUserId,
             @NonNull String callingPackage, @NonNull String packageName,
-            @NonNull List<String> shortcutIds, int userId);
+            @NonNull List<String> shortcutIds, int userId, int cacheFlags);
     public abstract void uncacheShortcuts(int launcherUserId,
             @NonNull String callingPackage, @NonNull String packageName,
-            @NonNull List<String> shortcutIds, int userId);
+            @NonNull List<String> shortcutIds, int userId, int cacheFlags);
 
     /**
      * Retrieves all of the direct share targets that match the given IntentFilter for the specified
diff --git a/core/java/android/net/ConnectivityDiagnosticsManager.java b/core/java/android/net/ConnectivityDiagnosticsManager.java
index 9086d49..275e38c 100644
--- a/core/java/android/net/ConnectivityDiagnosticsManager.java
+++ b/core/java/android/net/ConnectivityDiagnosticsManager.java
@@ -437,7 +437,7 @@
          */
         private long mReportTimestamp;
 
-        /** The detection method used to identify the suspected data stall */
+        /** A bitmask of the detection methods used to identify the suspected data stall */
         @DetectionMethod private final int mDetectionMethod;
 
         /** LinkProperties available on the Network at the reported timestamp */
@@ -499,9 +499,9 @@
         }
 
         /**
-         * Returns the detection method used to identify this suspected data stall.
+         * Returns the bitmask of detection methods used to identify this suspected data stall.
          *
-         * @return The detection method used to identify the suspected data stall
+         * @return The bitmask of detection methods used to identify the suspected data stall
          */
         public int getDetectionMethod() {
             return mDetectionMethod;
diff --git a/core/java/android/net/Ikev2VpnProfile.java b/core/java/android/net/Ikev2VpnProfile.java
index 836624b..407ff04 100644
--- a/core/java/android/net/Ikev2VpnProfile.java
+++ b/core/java/android/net/Ikev2VpnProfile.java
@@ -101,6 +101,7 @@
     private final boolean mIsBypassable; // Defaults in builder
     private final boolean mIsMetered; // Defaults in builder
     private final int mMaxMtu; // Defaults in builder
+    private final boolean mIsRestrictedToTestNetworks;
 
     private Ikev2VpnProfile(
             int type,
@@ -116,7 +117,8 @@
             @NonNull List<String> allowedAlgorithms,
             boolean isBypassable,
             boolean isMetered,
-            int maxMtu) {
+            int maxMtu,
+            boolean restrictToTestNetworks) {
         super(type);
 
         checkNotNull(serverAddr, MISSING_PARAM_MSG_TMPL, "Server address");
@@ -140,6 +142,7 @@
         mIsBypassable = isBypassable;
         mIsMetered = isMetered;
         mMaxMtu = maxMtu;
+        mIsRestrictedToTestNetworks = restrictToTestNetworks;
 
         validate();
     }
@@ -329,6 +332,15 @@
         return mMaxMtu;
     }
 
+    /**
+     * Returns whether or not this VPN profile is restricted to test networks.
+     *
+     * @hide
+     */
+    public boolean isRestrictedToTestNetworks() {
+        return mIsRestrictedToTestNetworks;
+    }
+
     @Override
     public int hashCode() {
         return Objects.hash(
@@ -345,7 +357,8 @@
                 mAllowedAlgorithms,
                 mIsBypassable,
                 mIsMetered,
-                mMaxMtu);
+                mMaxMtu,
+                mIsRestrictedToTestNetworks);
     }
 
     @Override
@@ -368,7 +381,8 @@
                 && Objects.equals(mAllowedAlgorithms, other.mAllowedAlgorithms)
                 && mIsBypassable == other.mIsBypassable
                 && mIsMetered == other.mIsMetered
-                && mMaxMtu == other.mMaxMtu;
+                && mMaxMtu == other.mMaxMtu
+                && mIsRestrictedToTestNetworks == other.mIsRestrictedToTestNetworks;
     }
 
     /**
@@ -381,7 +395,8 @@
      */
     @NonNull
     public VpnProfile toVpnProfile() throws IOException, GeneralSecurityException {
-        final VpnProfile profile = new VpnProfile("" /* Key; value unused by IKEv2VpnProfile(s) */);
+        final VpnProfile profile = new VpnProfile("" /* Key; value unused by IKEv2VpnProfile(s) */,
+                mIsRestrictedToTestNetworks);
         profile.type = mType;
         profile.server = mServerAddr;
         profile.ipsecIdentifier = mUserIdentity;
@@ -449,6 +464,9 @@
         builder.setBypassable(profile.isBypassable);
         builder.setMetered(profile.isMetered);
         builder.setMaxMtu(profile.maxMtu);
+        if (profile.isRestrictedToTestNetworks) {
+            builder.restrictToTestNetworks();
+        }
 
         switch (profile.type) {
             case TYPE_IKEV2_IPSEC_USER_PASS:
@@ -621,6 +639,7 @@
         private boolean mIsBypassable = false;
         private boolean mIsMetered = true;
         private int mMaxMtu = PlatformVpnProfile.MAX_MTU_DEFAULT;
+        private boolean mIsRestrictedToTestNetworks = false;
 
         /**
          * Creates a new builder with the basic parameters of an IKEv2/IPsec VPN.
@@ -842,6 +861,21 @@
         }
 
         /**
+         * Restricts this profile to use test networks (only).
+         *
+         * <p>This method is for testing only, and must not be used by apps. Calling
+         * provisionVpnProfile() with a profile where test-network usage is enabled will require the
+         * MANAGE_TEST_NETWORKS permission.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder restrictToTestNetworks() {
+            mIsRestrictedToTestNetworks = true;
+            return this;
+        }
+
+        /**
          * Validates, builds and provisions the VpnProfile.
          *
          * @throws IllegalArgumentException if any of the required keys or values were invalid
@@ -862,7 +896,8 @@
                     mAllowedAlgorithms,
                     mIsBypassable,
                     mIsMetered,
-                    mMaxMtu);
+                    mMaxMtu,
+                    mIsRestrictedToTestNetworks);
         }
     }
 }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index bf105ce..7845200 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -230,13 +230,14 @@
     public static final String DISALLOW_MODIFY_ACCOUNTS = "no_modify_accounts";
 
     /**
-     * Specifies if a user is disallowed from changing Wi-Fi
-     * access points. The default value is <code>false</code>.
-     * <p>
-     * Device owner and profile owner can set this restriction, although the restriction has no
-     * effect in a managed profile. When it is set by the profile owner of an organization-owned
-     * managed profile on the parent profile, it will disallow the personal user from changing
-     * Wi-Fi access points.
+     * Specifies if a user is disallowed from changing Wi-Fi access points via Settings.
+     *
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner, a profile owner on the
+     * primary user or by a profile owner of an organization-owned managed profile on the parent
+     * profile, it disallows the primary user from changing Wi-Fi access points.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -285,14 +286,16 @@
 
     /**
      * Specifies if a user is disallowed from turning on location sharing.
-     * The default value is <code>false</code>.
-     * <p>
-     * In a managed profile, location sharing always reflects the primary user's setting, but
+     *
+     * <p>In a managed profile, location sharing by default reflects the primary user's setting, but
      * can be overridden and forced off by setting this restriction to true in the managed profile.
-     * <p>
-     * Device owner and profile owner can set this restriction. When it is set by the profile
-     * owner of an organization-owned managed profile on the parent profile, it will prevent the
-     * user from turning on location sharing in the personal profile.
+     *
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it prevents the primary user from turning on
+     * location sharing.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -304,12 +307,13 @@
 
     /**
      * Specifies if airplane mode is disallowed on the device.
-     * <p>
-     * This restriction can only be set by the device owner, the profile owner on the primary user
-     * or the profile owner of an organization-owned managed profile on the parent profile, and it
-     * applies globally - i.e. it disables airplane mode on the entire device.
-     * <p>
-     * The default value is <code>false</code>.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by any of these owners, it applies globally - i.e., it disables airplane mode
+     * on the entire device.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -397,17 +401,18 @@
             "no_install_unknown_sources_globally";
 
     /**
-     * Specifies if a user is disallowed from configuring bluetooth.
-     * This does <em>not</em> restrict the user from turning bluetooth on or off.
-     * The default value is <code>false</code>.
-     * <p>
-     * This restriction doesn't prevent the user from using bluetooth. For disallowing usage of
+     * Specifies if a user is disallowed from configuring bluetooth via Settings. This does
+     * <em>not</em> restrict the user from turning bluetooth on or off.
+     *
+     * <p>This restriction doesn't prevent the user from using bluetooth. For disallowing usage of
      * bluetooth completely on the device, use {@link #DISALLOW_BLUETOOTH}.
-     * <p>
-     * Device owner and profile owner can set this restriction, although the restriction has no
-     * effect in a managed profile. When it is set by the profile owner of an organization-owned
-     * managed profile on the parent profile, it will disallow the personal user from configuring
-     * bluetooth.
+     *
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner, a profile owner on the
+     * primary user or by a profile owner of an organization-owned managed profile on the parent
+     * profile, it disallows the primary user from configuring bluetooth.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -418,13 +423,19 @@
     public static final String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth";
 
     /**
-     * Specifies if bluetooth is disallowed on the device.
+     * Specifies if bluetooth is disallowed on the device. If bluetooth is disallowed on the device,
+     * bluetooth cannot be turned on or configured via Settings.
      *
-     * <p> This restriction can only be set by the device owner, the profile owner on the
-     * primary user or the profile owner of an organization-owned managed profile on the
-     * parent profile and it applies globally - i.e. it disables bluetooth on the entire
-     * device.
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally - i.e., it disables bluetooth on
+     * the entire device and all users will be affected. When it is set by a profile owner on the
+     * primary user or by a profile owner of an organization-owned managed profile on the parent
+     * profile, it disables the primary user from using bluetooth and configuring bluetooth
+     * in Settings.
+     *
      * <p>The default value is <code>false</code>.
+     *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
@@ -434,14 +445,17 @@
     public static final String DISALLOW_BLUETOOTH = "no_bluetooth";
 
     /**
-     * Specifies if outgoing bluetooth sharing is disallowed on the device. Device owner and profile
-     * owner can set this restriction. When it is set by device owner or the profile owner of an
-     * organization-owned managed profile on the parent profile, all users on this device will be
-     * affected.
+     * Specifies if outgoing bluetooth sharing is disallowed.
      *
-     * <p>Default is <code>true</code> for managed profiles and false for otherwise. When a device
-     * upgrades to {@link android.os.Build.VERSION_CODES#O}, the system sets it for all existing
-     * managed profiles.
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, it applies globally. When it is set by a profile owner on the primary user or by a
+     * profile owner of an organization-owned managed profile on the parent profile, it disables
+     * the primary user from any outgoing bluetooth sharing.
+     *
+     * <p>Default is <code>true</code> for managed profiles and false otherwise.
+     *
+     * <p>When a device upgrades to {@link android.os.Build.VERSION_CODES#O}, the system sets it
+     * for all existing managed profiles.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -452,10 +466,17 @@
     public static final String DISALLOW_BLUETOOTH_SHARING = "no_bluetooth_sharing";
 
     /**
-     * Specifies if a user is disallowed from transferring files over
-     * USB. This can only be set by device owners, profile owners on the primary user or
-     * profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from transferring files over USB.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from transferring files over USB. No other
+     * user on the device is able to use file transfer over USB because the UI for file transfer
+     * is always associated with the primary user.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -512,13 +533,16 @@
     public static final String DISALLOW_REMOVE_MANAGED_PROFILE = "no_remove_managed_profile";
 
     /**
-     * Specifies if a user is disallowed from enabling or accessing debugging features. When set on
-     * the primary user or by the profile owner of an organization-owned managed profile on the
-     * parent profile, disables debugging features altogether, including USB debugging. When set on
-     * a managed profile or a secondary user, blocks debugging for that user only, including
-     * starting activities, making service calls, accessing content providers, sending broadcasts,
-     * installing/uninstalling packages, clearing user data, etc.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from enabling or accessing debugging features.
+     *
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it disables debugging features altogether, including
+     * USB debugging. When set on a managed profile or a secondary user, it blocks debugging for
+     * that user only, including starting activities, making service calls, accessing content
+     * providers, sending broadcasts, installing/uninstalling packages, clearing user data, etc.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -546,19 +570,18 @@
 
     /**
      * Specifies if a user is disallowed from enabling or disabling location providers. As a
-     * result, user is disallowed from turning on or off location.
+     * result, user is disallowed from turning on or off location via Settings.
      *
-     * <p>
-     * In a managed profile, location sharing is forced off when it is turned off on the primary
-     * user or by the profile owner of an organization-owned managed profile on the parent profile.
-     * The user can still turn off location sharing on a managed profile when the restriction is
-     * set by the profile owner on a managed profile.
-     * <p>
-     * This user restriction is different from {@link #DISALLOW_SHARE_LOCATION},
-     * as the device owner or profile owner can still enable or disable location mode via
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it disallows the primary user from turning location
+     * on or off.
+     *
+     * <p>The default value is <code>false</code>.
+     *
+     * <p>This user restriction is different from {@link #DISALLOW_SHARE_LOCATION},
+     * as a device owner or a profile owner can still enable or disable location mode via
      * {@link DevicePolicyManager#setLocationEnabled} when this restriction is on.
-     * <p>
-     * The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -570,15 +593,18 @@
     public static final String DISALLOW_CONFIG_LOCATION = "no_config_location";
 
     /**
-     * Specifies if date, time and timezone configuring is disallowed.
+     * Specifies configuring date, time and timezone is disallowed via Settings.
      *
-     * <p>When restriction is set by device owners or profile owners of organization-owned
-     * managed profiles on the parent profile, it applies globally - i.e., it disables date,
-     * time and timezone setting on the entire device and all users will be affected. When it's set
-     * by profile owners, it's only applied to the managed user.
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner or by a profile owner of an
+     * organization-owned managed profile on the parent profile, it applies globally - i.e.,
+     * it disables date, time and timezone setting on the entire device and all users are affected.
+     * When it is set by a profile owner on the primary user, it disables the primary user
+     * from configuring date, time and timezone and disables all configuring of date, time and
+     * timezone in Settings.
+     *
      * <p>The default value is <code>false</code>.
      *
-     * <p>This user restriction has no effect on managed profiles.
      * <p>Key for user restrictions.
      * <p>Type: Boolean
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
@@ -588,10 +614,18 @@
     public static final String DISALLOW_CONFIG_DATE_TIME = "no_config_date_time";
 
     /**
-     * Specifies if a user is disallowed from configuring Tethering
-     * & portable hotspots. This can only be set by device owners, profile owners on the
-     * primary user or profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from configuring Tethering and portable hotspots
+     * via Settings.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from using Tethering and hotspots and
+     * disables all configuring of Tethering and hotspots in Settings.
+     *
+     * <p>The default value is <code>false</code>.
+     *
      * <p>In Android 9.0 or higher, if tethering is enabled when this restriction is set,
      * tethering will be automatically turned off.
      *
@@ -685,10 +719,16 @@
     public static final String ENSURE_VERIFY_APPS = "ensure_verify_apps";
 
     /**
-     * Specifies if a user is disallowed from configuring cell
-     * broadcasts. This can only be set by device owners, profile owners on the primary user or
-     * profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from configuring cell broadcasts.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from configuring cell broadcasts.
+     *
+     * <p>The default value is <code>false</code>.
+     *
      * <p>This restriction has no effect on secondary users and managed profiles since only the
      * primary user can configure cell broadcasts.
      *
@@ -701,10 +741,16 @@
     public static final String DISALLOW_CONFIG_CELL_BROADCASTS = "no_config_cell_broadcasts";
 
     /**
-     * Specifies if a user is disallowed from configuring mobile
-     * networks. This can only be set by device owners, profile owners on the primary user or
-     * profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from configuring mobile networks.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from configuring mobile networks.
+     *
+     * <p>The default value is <code>false</code>.
+     *
      * <p>This restriction has no effect on secondary users and managed profiles since only the
      * primary user can configure mobile networks.
      *
@@ -747,11 +793,14 @@
 
     /**
      * Specifies if a user is disallowed from mounting physical external media.
-     * <p>
-     * This restriction can only be set by the device owner, the profile owner on the primary user
-     * or the profile owner of an organization-owned managed profile on the parent profile.
-     * <p>
-     * The default value is <code>false</code>.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from mounting physical external media.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -764,13 +813,14 @@
     /**
      * Specifies if a user is disallowed from adjusting microphone volume. If set, the microphone
      * will be muted.
-     * <p>
-     * The default value is <code>false</code>.
-     * <p>
-     * Device owner and profile owner can set this restriction, although the restriction has no
-     * effect in a managed profile. When it is set by the profile owner of an organization-owned
-     * managed profile on the parent profile, it will disallow the personal user from adjusting the
-     * microphone volume.
+     *
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner, it applies globally. When
+     * it is set by a profile owner on the primary user or by a profile owner of an
+     * organization-owned managed profile on the parent profile, it will disallow the primary user
+     * from adjusting the microphone volume.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -800,13 +850,13 @@
     /**
      * Specifies that the user is not allowed to make outgoing phone calls. Emergency calls are
      * still permitted.
-     * <p>
-     * The default value is <code>false</code>.
-     * <p>
-     * Device owner and profile owner can set this restriction, although the restriction has no
-     * effect in a managed profile. When it is set by the profile owner of an organization-owned
-     * managed profile on the parent profile, it will disallow the personal user from making
-     * outgoing phone calls.
+     *
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner, a profile owner on the
+     * primary user or by a profile owner of an organization-owned managed profile on the parent
+     * profile, it disallows the primary user from making outgoing phone calls.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -817,12 +867,15 @@
     public static final String DISALLOW_OUTGOING_CALLS = "no_outgoing_calls";
 
     /**
-     * Specifies that the user is not allowed to send or receive
-     * SMS messages. The default value is <code>false</code>.
-     * <p>
-     * Device owner and profile owner can set this restriction. When it is set by the
-     * profile owner of an organization-owned managed profile on the parent profile,
-     * it will disable SMS in the personal profile.
+     * Specifies that the user is not allowed to send or receive SMS messages.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from sending or receiving SMS messages.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -941,9 +994,15 @@
 
     /**
      * Specifies if the user is not allowed to reboot the device into safe boot mode.
-     * This can only be set by device owners, profile owners on the primary user or profile
-     * owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from rebooting the device into safe
+     * boot mode.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -981,12 +1040,14 @@
 
     /**
      * Specifies if a user is not allowed to use the camera.
-     * <p>
-     * Device owner and profile owner can set this restriction. When the restriction is set by
-     * the device owner or the profile owner of an organization-owned managed profile on the
-     * parent profile, it is applied globally.
-     * <p>
-     * The default value is <code>false</code>.
+     *
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a
+     * device owner, it applies globally - i.e., it disables the use of camera on the entire device
+     * and all users are affected. When it is set by a profile owner on the primary user or by a
+     * profile owner of an organization-owned managed profile on the parent profile, it disables
+     * the primary user from using camera.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
      * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
@@ -1006,9 +1067,15 @@
     public static final String DISALLOW_UNMUTE_DEVICE = "disallow_unmute_device";
 
     /**
-     * Specifies if a user is not allowed to use cellular data when roaming. This can only be set by
-     * device owners or profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is not allowed to use cellular data when roaming.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from using cellular data when roaming.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
      * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
@@ -1103,9 +1170,10 @@
      * Specifies if the contents of a user's screen is not allowed to be captured for artificial
      * intelligence purposes.
      *
-     * <p>Device owner and profile owner can set this restriction. When it is set by the
-     * device owner or the profile owner of an organization-owned managed profile on the parent
-     * profile, only the target user will be affected.
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it disables the primary user's screen from being
+     * captured for artificial intelligence purposes.
      *
      * <p>The default value is <code>false</code>.
      *
@@ -1119,9 +1187,10 @@
      * Specifies if the current user is able to receive content suggestions for selections based on
      * the contents of their screen.
      *
-     * <p>Device owner and profile owner can set this restriction. When it is set by the
-     * device owner or the profile owner of an organization-owned managed profile on the parent
-     * profile, only the target user will be affected.
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it disables the primary user from receiving content
+     * suggestions for selections based on the contents of their screen.
      *
      * <p>The default value is <code>false</code>.
      *
@@ -1185,10 +1254,11 @@
     /**
      * Specifies whether the user is allowed to modify private DNS settings.
      *
-     * <p>The default value is <code>false</code>.
+     * <p>This restriction can only be set by a device owner or a profile owner of an
+     * organization-owned managed profile on the parent profile. When it is set by either of these
+     * owners, it applies globally.
      *
-     * <p>This user restriction can only be applied by the device owner or the profile owner
-     * of an organization-owned managed profile on the parent profile.
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -4032,12 +4102,25 @@
     }
 
     /**
-     * Returns true if the user switcher should be shown, this will be if device supports multi-user
-     * and there are at least 2 users available that are not managed profiles.
-     * @hide
+     * Returns true if the user switcher should be shown.
+     * I.e., returns whether the user switcher is enabled and there is something actionable to show.
+     *
      * @return true if user switcher should be shown.
+     * @hide
      */
     public boolean isUserSwitcherEnabled() {
+        return isUserSwitcherEnabled(false);
+    }
+
+    /**
+     * Returns true if the user switcher should be shown.
+     *
+     * @param showEvenIfNotActionable value to return if the feature is enabled but there is nothing
+     *                                actionable for the user to do anyway
+     * @return true if user switcher should be shown.
+     * @hide
+     */
+    public boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable) {
         if (!supportsMultipleUsers()) {
             return false;
         }
@@ -4048,15 +4131,26 @@
         if (isDeviceInDemoMode(mContext)) {
             return false;
         }
-        // If user disabled this feature, don't show switcher
-        final boolean userSwitcherEnabled = Settings.Global.getInt(mContext.getContentResolver(),
-                Settings.Global.USER_SWITCHER_ENABLED, 1) != 0;
-        if (!userSwitcherEnabled) {
+        // Check the Settings.Global.USER_SWITCHER_ENABLED that the user can toggle on/off.
+        final boolean userSwitcherSettingOn = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.USER_SWITCHER_ENABLED,
+                Resources.getSystem().getBoolean(R.bool.config_showUserSwitcherByDefault) ? 1 : 0)
+                != 0;
+        if (!userSwitcherSettingOn) {
             return false;
         }
-        List<UserInfo> users = getUsers(true);
+
+        // The feature is enabled. But is it worth showing?
+        return showEvenIfNotActionable
+                || areThereUsersToWhichToSwitch() // There are switchable users.
+                || !hasUserRestriction(UserManager.DISALLOW_ADD_USER); // New users can be added.
+    }
+
+    /** Returns whether there are any users (other than the current user) to which to switch. */
+    private boolean areThereUsersToWhichToSwitch() {
+        final List<UserInfo> users = getUsers(true);
         if (users == null) {
-           return false;
+            return false;
         }
         int switchableUserCount = 0;
         for (UserInfo user : users) {
@@ -4064,9 +4158,7 @@
                 ++switchableUserCount;
             }
         }
-        final boolean guestEnabled = !mContext.getSystemService(DevicePolicyManager.class)
-                .getGuestUserDisabled(null);
-        return switchableUserCount > 1 || guestEnabled;
+        return switchableUserCount > 1;
     }
 
     /**
diff --git a/core/java/android/os/incremental/IIncrementalService.aidl b/core/java/android/os/incremental/IIncrementalService.aidl
index 4c57570..220ce22 100644
--- a/core/java/android/os/incremental/IIncrementalService.aidl
+++ b/core/java/android/os/incremental/IIncrementalService.aidl
@@ -111,9 +111,9 @@
     void deleteStorage(int storageId);
 
     /**
-     * Setting up native library directories and extract native libs onto a storage.
+     * Setting up native library directories and extract native libs onto a storage if needed.
      */
-    boolean configureNativeBinaries(int storageId, in @utf8InCpp String apkFullPath, in @utf8InCpp String libDirRelativePath, in @utf8InCpp String abi);
+    boolean configureNativeBinaries(int storageId, in @utf8InCpp String apkFullPath, in @utf8InCpp String libDirRelativePath, in @utf8InCpp String abi, boolean extractNativeLibs);
 
     /**
      * Waits until all native library extraction is done for the storage
diff --git a/core/java/android/os/incremental/IncrementalStorage.java b/core/java/android/os/incremental/IncrementalStorage.java
index 70ebbaa..6200a38 100644
--- a/core/java/android/os/incremental/IncrementalStorage.java
+++ b/core/java/android/os/incremental/IncrementalStorage.java
@@ -469,12 +469,15 @@
      * @param apkFullPath Source APK to extract native libs from.
      * @param libDirRelativePath Target dir to put lib files, e.g., "lib" or "lib/arm".
      * @param abi Target ABI of the native lib files. Only extract native libs of this ABI.
+     * @param extractNativeLibs If true, extract native libraries; otherwise just setup directories
+     *                          without extracting.
      * @return Success of not.
      */
     public boolean configureNativeBinaries(String apkFullPath, String libDirRelativePath,
-            String abi) {
+            String abi, boolean extractNativeLibs) {
         try {
-            return mService.configureNativeBinaries(mId, apkFullPath, libDirRelativePath, abi);
+            return mService.configureNativeBinaries(mId, apkFullPath, libDirRelativePath, abi,
+                    extractNativeLibs);
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
             return false;
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 05abc60..cd56ca9 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static android.view.InsetsController.AnimationType;
+import static android.view.InsetsController.DEBUG;
 import static android.view.InsetsState.ISIDE_BOTTOM;
 import static android.view.InsetsState.ISIDE_FLOATING;
 import static android.view.InsetsState.ISIDE_LEFT;
@@ -30,6 +31,7 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.util.ArraySet;
+import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.SparseSetArray;
@@ -52,6 +54,8 @@
 public class InsetsAnimationControlImpl implements WindowInsetsAnimationController,
         InsetsAnimationControlRunner {
 
+    private static final String TAG = "InsetsAnimationCtrlImpl";
+
     private final Rect mTmpFrame = new Rect();
 
     private final WindowInsetsAnimationControlListener mListener;
@@ -165,6 +169,7 @@
      */
     public boolean applyChangeInsets(InsetsState state) {
         if (mCancelled) {
+            if (DEBUG) Log.d(TAG, "applyChangeInsets canceled");
             return false;
         }
         final Insets offset = Insets.subtract(mShownInsets, mPendingInsets);
@@ -186,9 +191,13 @@
         mCurrentAlpha = mPendingAlpha;
         mAnimation.setAlpha(mPendingAlpha);
         if (mFinished) {
+            if (DEBUG) Log.d(TAG, String.format(
+                    "notifyFinished shown: %s, currentAlpha: %f, currentInsets: %s",
+                    mShownOnFinish, mCurrentAlpha, mCurrentInsets));
             mController.notifyFinished(this, mShownOnFinish);
             releaseLeashes();
         }
+        if (DEBUG) Log.d(TAG, "Animation finished abruptly.");
         return mFinished;
     }
 
@@ -203,12 +212,15 @@
     @Override
     public void finish(boolean shown) {
         if (mCancelled || mFinished) {
+            if (DEBUG) Log.d(TAG, "Animation already canceled or finished, not notifying.");
             return;
         }
         mShownOnFinish = shown;
         mFinished = true;
         setInsetsAndAlpha(shown ? mShownInsets : mHiddenInsets, 1f /* alpha */, 1f /* fraction */,
                 true /* allowWhenFinished */);
+
+        if (DEBUG) Log.d(TAG, "notify control request finished for types: " + mTypes);
         mListener.onFinished(this);
     }
 
@@ -225,6 +237,7 @@
         }
         mCancelled = true;
         mListener.onCancelled(mReadyDispatched ? this : null);
+        if (DEBUG) Log.d(TAG, "notify Control request cancelled for types: " + mTypes);
 
         releaseLeashes();
     }
diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java
index 3215b7c..0e71b76 100644
--- a/core/java/android/view/InsetsAnimationThreadControlRunner.java
+++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java
@@ -16,12 +16,14 @@
 
 package android.view;
 
+import static android.view.InsetsController.DEBUG;
 import static android.view.SyncRtSurfaceTransactionApplier.applyParams;
 
 import android.annotation.UiThread;
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Trace;
+import android.util.Log;
 import android.util.SparseArray;
 import android.view.InsetsController.AnimationType;
 import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
@@ -37,6 +39,7 @@
  */
 public class InsetsAnimationThreadControlRunner implements InsetsAnimationControlRunner {
 
+    private static final String TAG = "InsetsAnimThreadRunner";
     private final InsetsAnimationControlImpl mControl;
     private final InsetsAnimationControlCallbacks mOuterCallbacks;
     private final Handler mMainThreadHandler;
@@ -71,6 +74,7 @@
 
         @Override
         public void applySurfaceParams(SurfaceParams... params) {
+            if (DEBUG) Log.d(TAG, "applySurfaceParams");
             SurfaceControl.Transaction t = new SurfaceControl.Transaction();
             for (int i = params.length - 1; i >= 0; i--) {
                 SyncRtSurfaceTransactionApplier.SurfaceParams surfaceParams = params[i];
@@ -82,6 +86,7 @@
 
         @Override
         public void releaseSurfaceControlFromRt(SurfaceControl sc) {
+            if (DEBUG) Log.d(TAG, "releaseSurfaceControlFromRt");
             // Since we don't push the SurfaceParams to the RT we can release directly
             sc.release();
         }
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 758062f..ef9edc6c 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -37,6 +37,7 @@
 import android.os.Handler;
 import android.os.Trace;
 import android.util.ArraySet;
+import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
 import android.view.InsetsSourceConsumer.ShowResult;
@@ -69,6 +70,8 @@
  */
 public class InsetsController implements WindowInsetsController, InsetsAnimationControlCallbacks {
 
+    private int mTypesBeingCancelled;
+
     public interface Host {
 
         Handler getHandler();
@@ -152,8 +155,16 @@
          * Obtains {@link InputMethodManager} instance from host.
          */
         InputMethodManager getInputMethodManager();
+
+        /**
+         * @return title of the rootView, if it has one.
+         * Note: this method is for debugging purposes only.
+         */
+        @Nullable
+        String getRootViewTitle();
     }
 
+    private static final String TAG = "InsetsController";
     private static final int ANIMATION_DURATION_SHOW_MS = 275;
     private static final int ANIMATION_DURATION_HIDE_MS = 340;
 
@@ -171,6 +182,9 @@
     private static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR =
             new PathInterpolator(0.4f, 0f, 1f, 1f);
 
+    static final boolean DEBUG = false;
+    static final boolean WARN = false;
+
     /**
      * Layout mode during insets animation: The views should be laid out as if the changing inset
      * types are fully shown. Before starting the animation, {@link View#onApplyWindowInsets} will
@@ -268,6 +282,7 @@
         @Override
         public void onReady(WindowInsetsAnimationController controller, int types) {
             mController = controller;
+            if (DEBUG) Log.d(TAG, "default animation onReady types: " + types);
 
             mAnimator = ValueAnimator.ofFloat(0f, 1f);
             mAnimator.setDuration(mDurationMs);
@@ -290,6 +305,8 @@
                         sEvaluator.evaluate(insetsFraction, start, end),
                         alphaInterpolator.getInterpolation(alphaFraction),
                         rawFraction);
+                if (DEBUG) Log.d(TAG, "Default animation setInsetsAndAlpha fraction: "
+                        + insetsFraction);
             });
             mAnimator.addListener(new AnimatorListenerAdapter() {
 
@@ -306,6 +323,8 @@
 
         @Override
         public void onFinished(WindowInsetsAnimationController controller) {
+            if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onFinished types:"
+                    + Type.toString(mRequestedTypes));
         }
 
         @Override
@@ -314,6 +333,8 @@
             if (mAnimator != null) {
                 mAnimator.cancel();
             }
+            if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onCancelled types:"
+                    + mRequestedTypes);
         }
 
         Interpolator getInterpolator() {
@@ -348,6 +369,7 @@
 
         protected void onAnimationFinish() {
             mController.finish(mShow);
+            if (DEBUG) Log.d(TAG, "onAnimationFinish showOnFinish: " + mShow);
         }
 
         /**
@@ -420,8 +442,6 @@
         final boolean useInsetsAnimationThread;
     }
 
-    private final String TAG = "InsetsControllerImpl";
-
     /** The local state */
     private final InsetsState mState = new InsetsState();
 
@@ -494,6 +514,7 @@
             InsetsState state = new InsetsState(mState, true /* copySources */);
             for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
                 RunningAnimation runningAnimation = mRunningAnimations.get(i);
+                if (DEBUG) Log.d(TAG, "Running animation type: " + runningAnimation.type);
                 InsetsAnimationControlRunner runner = runningAnimation.runner;
                 if (runner instanceof InsetsAnimationControlImpl) {
                     InsetsAnimationControlImpl control = (InsetsAnimationControlImpl) runner;
@@ -516,6 +537,12 @@
                     mLastDisplayCutout, mLastLegacySoftInputMode, mLastLegacySystemUiFlags,
                     null /* typeSideMap */);
             mHost.dispatchWindowInsetsAnimationProgress(insets, mUnmodifiableTmpRunningAnims);
+            if (DEBUG) {
+                for (WindowInsetsAnimation anim : mUnmodifiableTmpRunningAnims) {
+                    Log.d(TAG, String.format("Running animation type: %d, progress: %f",
+                            anim.getTypeMask(), anim.getInterpolatedFraction()));
+                }
+            }
 
             for (int i = mTmpFinishedControls.size() - 1; i >= 0; i--) {
                 dispatchAnimationEnd(mTmpFinishedControls.get(i).getAnimation());
@@ -553,13 +580,16 @@
         if (!localStateChanged && mLastDispatchedState.equals(state)) {
             return false;
         }
+        if (DEBUG) Log.d(TAG, "onStateChanged: " + state);
         updateState(state);
         mLastDispatchedState.set(state, true /* copySources */);
         applyLocalVisibilityOverride();
         if (localStateChanged) {
+            if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged");
             mHost.notifyInsetsChanged();
         }
         if (!mState.equals(mLastDispatchedState, true /* excludingCaptionInsets */)) {
+            if (DEBUG) Log.d(TAG, "onStateChanged, send state to WM: " + mState);
             updateRequestedState();
         }
         return true;
@@ -683,7 +713,6 @@
 
     @VisibleForTesting
     public void show(@InsetsType int types, boolean fromIme) {
-
         // Handle pending request ready in case there was one set.
         if (fromIme && mPendingImeControlRequest != null) {
             PendingControlRequest pendingRequest = mPendingImeControlRequest;
@@ -711,10 +740,18 @@
                     || animationType == ANIMATION_TYPE_SHOW) {
                 // no-op: already shown or animating in (because window visibility is
                 // applied before starting animation).
+                if (DEBUG) Log.d(TAG, String.format(
+                        "show ignored for type: %d animType: %d requestedVisible: %s",
+                        consumer.getType(), animationType, consumer.isRequestedVisible()));
+                continue;
+            }
+            if (fromIme && animationType == ANIMATION_TYPE_USER) {
+                // App is already controlling the IME, don't cancel it.
                 continue;
             }
             typesReady |= InsetsState.toPublicType(consumer.getType());
         }
+        if (DEBUG) Log.d(TAG, "show typesReady: " + typesReady);
         applyAnimation(typesReady, true /* show */, fromIme);
     }
 
@@ -778,12 +815,20 @@
             @AnimationType int animationType,
             @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
             boolean useInsetsAnimationThread) {
+        if ((types & mTypesBeingCancelled) != 0) {
+            throw new IllegalStateException("Cannot start a new insets animation of "
+                    + Type.toString(types)
+                    + " while an existing " + Type.toString(mTypesBeingCancelled)
+                    + " is being cancelled.");
+        }
         if (types == 0) {
             // nothing to animate.
             listener.onCancelled(null);
+            if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked");
             return;
         }
         cancelExistingControllers(types);
+        if (DEBUG) Log.d(TAG, "controlAnimation types: " + types);
         mLastStartedAnimTypes |= types;
 
         final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
@@ -793,6 +838,8 @@
                 fromIme, internalTypes, controls, animationType);
         int typesReady = typesReadyPair.first;
         boolean imeReady = typesReadyPair.second;
+        if (DEBUG) Log.d(TAG, String.format(
+                "controlAnimationUnchecked, typesReady: %s imeReady: %s", typesReady, imeReady));
         if (!imeReady) {
             // IME isn't ready, all requested types will be animated once IME is ready
             abortPendingImeControlRequest();
@@ -802,9 +849,12 @@
                     useInsetsAnimationThread);
             mPendingImeControlRequest = request;
             mHandler.postDelayed(mPendingControlTimeout, PENDING_CONTROL_TIMEOUT_MS);
+            if (DEBUG) Log.d(TAG, "Ime not ready. Create pending request");
             if (cancellationSignal != null) {
                 cancellationSignal.setOnCancelListener(() -> {
                     if (mPendingImeControlRequest == request) {
+                        if (DEBUG) Log.d(TAG,
+                                "Cancellation signal abortPendingImeControlRequest");
                         abortPendingImeControlRequest();
                     }
                 });
@@ -813,6 +863,7 @@
         }
 
         if (typesReady == 0) {
+            if (DEBUG) Log.d(TAG, "No types ready. onCancelled()");
             listener.onCancelled(null);
             return;
         }
@@ -826,8 +877,12 @@
                         frame, mState, listener, typesReady, this, durationMs, interpolator,
                         animationType);
         mRunningAnimations.add(new RunningAnimation(runner, animationType));
+        if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
+                + useInsetsAnimationThread);
         if (cancellationSignal != null) {
-            cancellationSignal.setOnCancelListener(runner::cancel);
+            cancellationSignal.setOnCancelListener(() -> {
+                cancelAnimation(runner, true /* invokeCallback */);
+            });
         }
         if (layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN) {
             showDirectly(types);
@@ -857,8 +912,11 @@
                         break;
                     case ShowResult.IME_SHOW_DELAYED:
                         imeReady = false;
+                        if (DEBUG) Log.d(TAG, "requestShow IME_SHOW_DELAYED");
                         break;
                     case ShowResult.IME_SHOW_FAILED:
+                        if (WARN) Log.w(TAG, "requestShow IME_SHOW_FAILED. fromIme: "
+                                + fromIme);
                         // IME cannot be shown (since it didn't have focus), proceed
                         // with animation of other types.
                         break;
@@ -873,6 +931,9 @@
                 canRun = true;
             }
             if (!canRun) {
+                if (WARN) Log.w(TAG, String.format(
+                        "collectSourceControls can't continue show for type: %s fromIme: %b",
+                        InsetsState.typeToString(consumer.getType()), fromIme));
                 continue;
             }
             final InsetsSourceControl control = consumer.getControl();
@@ -880,7 +941,8 @@
                 controls.put(consumer.getType(), new InsetsSourceControl(control));
                 typesReady |= toPublicType(consumer.getType());
             } else if (animationType == ANIMATION_TYPE_SHOW) {
-
+                if (DEBUG) Log.d(TAG, "collectSourceControls no control for show(). fromIme: "
+                        + fromIme);
                 // We don't have a control at the moment. However, we still want to update requested
                 // visibility state such that in case we get control, we can apply show animation.
                 consumer.show(fromIme);
@@ -915,14 +977,20 @@
     }
 
     private void cancelExistingControllers(@InsetsType int types) {
-        for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
-            InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
-            if ((control.getTypes() & types) != 0) {
-                cancelAnimation(control, true /* invokeCallback */);
+        final int originalmTypesBeingCancelled = mTypesBeingCancelled;
+        mTypesBeingCancelled |= types;
+        try {
+            for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+                InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
+                if ((control.getTypes() & types) != 0) {
+                    cancelAnimation(control, true /* invokeCallback */);
+                }
             }
-        }
-        if ((types & ime()) != 0) {
-            abortPendingImeControlRequest();
+            if ((types & ime()) != 0) {
+                abortPendingImeControlRequest();
+            }
+        } finally {
+            mTypesBeingCancelled = originalmTypesBeingCancelled;
         }
     }
 
@@ -931,6 +999,7 @@
             mPendingImeControlRequest.listener.onCancelled(null);
             mPendingImeControlRequest = null;
             mHandler.removeCallbacks(mPendingControlTimeout);
+            if (DEBUG) Log.d(TAG, "abortPendingImeControlRequest");
         }
     }
 
@@ -938,6 +1007,7 @@
     @Override
     public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) {
         cancelAnimation(runner, false /* invokeCallback */);
+        if (DEBUG) Log.d(TAG, "notifyFinished. shown: " + shown);
         if (shown) {
             showDirectly(runner.getTypes());
         } else {
@@ -964,6 +1034,8 @@
     }
 
     private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) {
+        if (DEBUG) Log.d(TAG, String.format("cancelAnimation of types: %d, animType: %d",
+                control.getTypes(), control.getAnimationType()));
         if (invokeCallback) {
             control.cancel();
         }
@@ -977,6 +1049,9 @@
                         mHost.notifyInsetsChanged();
                     }
                 }
+                if (invokeCallback && runningAnimation.startDispatched) {
+                    dispatchAnimationEnd(runningAnimation.runner.getAnimation());
+                }
                 break;
             }
         }
@@ -1088,6 +1163,7 @@
     public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme) {
         if (types == 0) {
             // nothing to animate.
+            if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
             return;
         }
 
@@ -1142,6 +1218,7 @@
         mHost.dispatchWindowInsetsAnimationPrepare(animation);
         mHost.addOnPreDrawRunnable(() -> {
             if (controller.isCancelled()) {
+                if (WARN) Log.w(TAG, "startAnimation canceled before preDraw");
                 return;
             }
             Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW,
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index df3ac87..3869484 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -19,11 +19,13 @@
 import static android.view.InsetsController.ANIMATION_TYPE_NONE;
 import static android.view.InsetsController.AnimationType;
 import static android.view.InsetsState.getDefaultVisibility;
+import static android.view.InsetsController.DEBUG;
 import static android.view.InsetsState.toPublicType;
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.graphics.Rect;
+import android.util.Log;
 import android.view.InsetsState.InternalInsetsType;
 import android.view.SurfaceControl.Transaction;
 import android.view.WindowInsets.Type.InsetsType;
@@ -64,6 +66,7 @@
     protected final InsetsState mState;
     protected final @InternalInsetsType int mType;
 
+    private static final String TAG = "InsetsSourceConsumer";
     private final Supplier<Transaction> mTransactionSupplier;
     private @Nullable InsetsSourceControl mSourceControl;
     private boolean mHasWindowFocus;
@@ -103,7 +106,11 @@
 
         final InsetsSourceControl lastControl = mSourceControl;
         mSourceControl = control;
-
+        if (control != null) {
+            if (DEBUG) Log.d(TAG, String.format("setControl -> %s on %s",
+                    InsetsState.typeToString(control.getType()),
+                    mController.getHost().getRootViewTitle()));
+        }
         // We are loosing control
         if (mSourceControl == null) {
             mController.notifyControlRevoked(this);
@@ -118,6 +125,8 @@
             final boolean requestedVisible = isRequestedVisibleAwaitingControl();
             final boolean needAnimation = requestedVisible != mState.getSource(mType).isVisible();
             if (control.getLeash() != null && (needAnimation || mIsAnimationPending)) {
+                if (DEBUG) Log.d(TAG, String.format("Gaining control in %s, requestedVisible: %b",
+                        mController.getHost().getRootViewTitle(), requestedVisible));
                 if (requestedVisible) {
                     showTypes[0] |= toPublicType(getType());
                 } else {
@@ -170,11 +179,15 @@
 
     @VisibleForTesting
     public void show(boolean fromIme) {
+        if (DEBUG) Log.d(TAG, String.format("Call show() for type: %s fromIme: %b ",
+                InsetsState.typeToString(mType), fromIme));
         setRequestedVisible(true);
     }
 
     @VisibleForTesting
     public void hide() {
+        if (DEBUG) Log.d(TAG, String.format("Call hide for %s on %s",
+                InsetsState.typeToString(mType), mController.getHost().getRootViewTitle()));
         setRequestedVisible(false);
     }
 
@@ -212,11 +225,16 @@
 
         // If we don't have control, we are not able to change the visibility.
         if (!hasControl) {
+            if (DEBUG) Log.d(TAG, "applyLocalVisibilityOverride: No control in "
+                    + mController.getHost().getRootViewTitle()
+                    + " requestedVisible " + mRequestedVisible);
             return false;
         }
         if (isVisible == mRequestedVisible) {
             return false;
         }
+        if (DEBUG) Log.d(TAG, String.format("applyLocalVisibilityOverride: %s requestedVisible: %b",
+                mController.getHost().getRootViewTitle(), mRequestedVisible));
         mState.getSource(mType).setVisible(mRequestedVisible);
         return true;
     }
@@ -271,6 +289,7 @@
         newSource.setFrame(source.getFrame());
         newSource.setVisibleFrame(source.getVisibleFrame());
         mState.addSource(newSource);
+        if (DEBUG) Log.d(TAG, "updateSource: " + newSource);
     }
 
     boolean notifyAnimationFinished() {
@@ -293,6 +312,7 @@
         if (mRequestedVisible != requestedVisible) {
             mRequestedVisible = requestedVisible;
             mIsAnimationPending = false;
+            if (DEBUG) Log.d(TAG, "setRequestedVisible: " + requestedVisible);
         }
         if (applyLocalVisibilityOverride()) {
             mController.notifyVisibilityChanged();
@@ -305,6 +325,7 @@
         }
 
         final Transaction t = mTransactionSupplier.get();
+        if (DEBUG) Log.d(TAG, "applyHiddenToControl: " + mRequestedVisible);
         if (mRequestedVisible) {
             t.show(mSourceControl.getLeash());
         } else {
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index e001b66..2c2ecd5 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -22,6 +22,7 @@
 import android.os.Parcelable;
 import android.view.InsetsState.InternalInsetsType;
 
+import java.io.PrintWriter;
 import java.util.function.Consumer;
 
 /**
@@ -101,6 +102,14 @@
         }
     }
 
+    public void dump(String prefix, PrintWriter pw) {
+        pw.print(prefix);
+        pw.print("InsetsSourceControl type="); pw.print(InsetsState.typeToString(mType));
+        pw.print(" mLeash="); pw.print(mLeash);
+        pw.print(" mSurfacePosition="); pw.print(mSurfacePosition);
+        pw.println();
+    }
+
     public static final @android.annotation.NonNull Creator<InsetsSourceControl> CREATOR
             = new Creator<InsetsSourceControl>() {
         public InsetsSourceControl createFromParcel(Parcel in) {
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 9896aa4..3822ee5 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -51,6 +51,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
+import java.util.StringJoiner;
 
 /**
  * Holder for state of system windows that cause window insets for all other windows in the system.
@@ -78,7 +79,9 @@
             ITYPE_TOP_DISPLAY_CUTOUT,
             ITYPE_RIGHT_DISPLAY_CUTOUT,
             ITYPE_BOTTOM_DISPLAY_CUTOUT,
-            ITYPE_IME
+            ITYPE_IME,
+            ITYPE_CLIMATE_BAR,
+            ITYPE_EXTRA_NAVIGATION_BAR
     })
     public @interface InternalInsetsType {}
 
@@ -109,7 +112,11 @@
     /** Input method window. */
     public static final int ITYPE_IME = 13;
 
-    static final int LAST_TYPE = ITYPE_IME;
+    /** Additional system decorations inset type. */
+    public static final int ITYPE_CLIMATE_BAR = 14;
+    public static final int ITYPE_EXTRA_NAVIGATION_BAR = 15;
+
+    static final int LAST_TYPE = ITYPE_EXTRA_NAVIGATION_BAR;
 
     // Derived types
 
@@ -417,8 +424,10 @@
     public static @Type.InsetsType int toPublicType(@InternalInsetsType int type) {
         switch (type) {
             case ITYPE_STATUS_BAR:
+            case ITYPE_CLIMATE_BAR:
                 return Type.STATUS_BARS;
             case ITYPE_NAVIGATION_BAR:
+            case ITYPE_EXTRA_NAVIGATION_BAR:
                 return Type.NAVIGATION_BARS;
             case ITYPE_CAPTION_BAR:
                 return Type.CAPTION_BAR;
@@ -497,6 +506,10 @@
                 return "ITYPE_BOTTOM_DISPLAY_CUTOUT";
             case ITYPE_IME:
                 return "ITYPE_IME";
+            case ITYPE_CLIMATE_BAR:
+                return "ITYPE_CLIMATE_BAR";
+            case ITYPE_EXTRA_NAVIGATION_BAR:
+                return "ITYPE_EXTRA_NAVIGATION_BAR";
             default:
                 return "ITYPE_UNKNOWN_" + type;
         }
@@ -600,10 +613,16 @@
 
     @Override
     public String toString() {
+        StringJoiner joiner = new StringJoiner(", ");
+        for (InsetsSource source : mSources.values()) {
+            if (source != null) {
+                joiner.add(source.toString());
+            }
+        }
         return "InsetsState: {"
                 + "mDisplayFrame=" + mDisplayFrame
-                + ", mSources=" + mSources
-                + "}";
+                + ", mSources= { " + joiner
+                + " }";
     }
 }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 511e755..8b5d033 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -4990,6 +4990,11 @@
                     break;
                 }
                 case MSG_SHOW_INSETS: {
+                    if (mView == null) {
+                        Log.e(TAG,
+                                String.format("Calling showInsets(%d,%b) on window that no longer"
+                                        + " has views.", msg.arg1, msg.arg2 == 1));
+                    }
                     mInsetsController.show(msg.arg1, msg.arg2 == 1);
                     break;
                 }
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index 9674a80..686d561 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import static android.view.InsetsController.DEBUG;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_APPEARANCE_CONTROLLED;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
 
@@ -84,6 +85,7 @@
         if (mViewRoot.mView == null) {
             return null;
         }
+        if (DEBUG) Log.d(TAG, "windowInsetsAnimation started");
         return mViewRoot.mView.dispatchWindowInsetsAnimationStart(animation, bounds);
     }
 
@@ -94,11 +96,18 @@
             // The view has already detached from window.
             return null;
         }
+        if (DEBUG) {
+            for (WindowInsetsAnimation anim : runningAnimations) {
+                Log.d(TAG, "windowInsetsAnimation progress: "
+                        + anim.getInterpolatedFraction());
+            }
+        }
         return mViewRoot.mView.dispatchWindowInsetsAnimationProgress(insets, runningAnimations);
     }
 
     @Override
     public void dispatchWindowInsetsAnimationEnd(@NonNull WindowInsetsAnimation animation) {
+        if (DEBUG) Log.d(TAG, "windowInsetsAnimation ended");
         mViewRoot.mView.dispatchWindowInsetsAnimationEnd(animation);
     }
 
@@ -212,4 +221,12 @@
     public InputMethodManager getInputMethodManager() {
         return mViewRoot.mContext.getSystemService(InputMethodManager.class);
     }
+
+    @Override
+    public String getRootViewTitle() {
+        if (mViewRoot == null) {
+            return null;
+        }
+        return mViewRoot.getTitle().toString();
+    }
 }
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
index 0807f41..7042f29 100644
--- a/core/java/android/webkit/WebChromeClient.java
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -202,9 +202,9 @@
      * <p>To suppress the dialog and allow JavaScript execution to
      * continue, call {@code JsResult.confirm()} immediately and then return
      * {@code true}.
-     * <p>Note that if the {@link WebChromeClient} is {@code null}, the default
-     * dialog will be suppressed and Javascript execution will continue
-     * immediately.
+     * <p>Note that if the {@link WebChromeClient} is set to be {@code null},
+     * or if {@link WebChromeClient} is not set at all, the default dialog will
+     * be suppressed and Javascript execution will continue immediately.
      *
      * @param view The WebView that initiated the callback.
      * @param url The url of the page requesting the dialog.
@@ -236,9 +236,10 @@
      * <p>To suppress the dialog and allow JavaScript execution to continue,
      * call {@code JsResult.confirm()} or {@code JsResult.cancel()} immediately
      * and then return {@code true}.
-     * <p>Note that if the {@link WebChromeClient} is {@code null}, the default
-     * dialog will be suppressed and the default value of {@code false} will be
-     * returned to the JavaScript code immediately.
+     * <p>Note that if the {@link WebChromeClient} is set to be {@code null},
+     * or if {@link WebChromeClient} is not set at all, the default dialog will
+     * be suppressed and the default value of {@code false} will be returned to
+     * the JavaScript code immediately.
      *
      * @param view The WebView that initiated the callback.
      * @param url The url of the page requesting the dialog.
@@ -269,9 +270,10 @@
      * <p>To suppress the dialog and allow JavaScript execution to continue,
      * call {@code JsPromptResult.confirm(result)} immediately and then
      * return {@code true}.
-     * <p>Note that if the {@link WebChromeClient} is {@code null}, the default
-     * dialog will be suppressed and {@code null} will be returned to the
-     * JavaScript code immediately.
+     * <p>Note that if the {@link WebChromeClient} is set to be {@code null},
+     * or if {@link WebChromeClient} is not set at all, the default dialog will
+     * be suppressed and {@code null} will be returned to the JavaScript code
+     * immediately.
      *
      * @param view The WebView that initiated the callback.
      * @param url The url of the page requesting the dialog.
@@ -288,20 +290,32 @@
     }
 
     /**
-     * Tell the client to display a dialog to confirm navigation away from the
-     * current page. This is the result of the onbeforeunload javascript event.
-     * If the client returns {@code true}, WebView will assume that the client will
-     * handle the confirm dialog and call the appropriate JsResult method. If
-     * the client returns {@code false}, a default value of {@code true} will be returned to
-     * javascript to accept navigation away from the current page. The default
-     * behavior is to return {@code false}. Setting the JsResult to {@code true} will navigate
-     * away from the current page, {@code false} will cancel the navigation.
+     * Notify the host application that the web page wants to confirm navigation
+     * from JavaScript {@code onbeforeunload}.
+     * <p>The default behavior if this method returns {@code false} or is not
+     * overridden is to show a dialog containing the message and suspend
+     * JavaScript execution until the dialog is dismissed. The default dialog
+     * will continue the navigation if the user confirms the navigation, and
+     * will stop the navigation if the user wants to stay on the current page.
+     * <p>To show a custom dialog, the app should return {@code true} from this
+     * method, in which case the default dialog will not be shown and JavaScript
+     * execution will be suspended. When the custom dialog is dismissed, the
+     * app should call {@code JsResult.confirm()} to continue the navigation or,
+     * {@code JsResult.cancel()} to stay on the current page.
+     * <p>To suppress the dialog and allow JavaScript execution to continue,
+     * call {@code JsResult.confirm()} or {@code JsResult.cancel()} immediately
+     * and then return {@code true}.
+     * <p>Note that if the {@link WebChromeClient} is set to be {@code null},
+     * or if {@link WebChromeClient} is not set at all, the default dialog will
+     * be suppressed and the navigation will be resumed immediately.
+     *
      * @param view The WebView that initiated the callback.
      * @param url The url of the page requesting the dialog.
      * @param message Message to be displayed in the window.
      * @param result A JsResult used to send the user's response to
      *               javascript.
-     * @return boolean Whether the client will handle the confirm dialog.
+     * @return boolean {@code true} if the request is handled or ignored.
+     * {@code false} if WebView needs to show the default dialog.
      */
     public boolean onJsBeforeUnload(WebView view, String url, String message,
             JsResult result) {
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 8b0dd8a..1f25feb 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -171,17 +171,6 @@
     public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
             = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
 
-    /**
-     * Integer extra to indicate which profile should be automatically selected.
-     * <p>Can only be used if there is a work profile.
-     * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
-     */
-    static final String EXTRA_SELECTED_PROFILE =
-            "com.android.internal.app.ChooserActivity.EXTRA_SELECTED_PROFILE";
-
-    static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
-    static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
-
     private static final String PREF_NUM_SHEET_EXPANSIONS = "pref_num_sheet_expansions";
 
     private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label";
@@ -928,15 +917,8 @@
     }
 
     private int findSelectedProfile() {
-        int selectedProfile;
-        if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) {
-            selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1);
-            if (selectedProfile != PROFILE_PERSONAL && selectedProfile != PROFILE_WORK) {
-                throw new IllegalArgumentException(EXTRA_SELECTED_PROFILE + " has invalid value "
-                        + selectedProfile + ". Must be either ChooserActivity.PROFILE_PERSONAL or "
-                        + "ChooserActivity.PROFILE_WORK.");
-            }
-        } else {
+        int selectedProfile = getSelectedProfileExtra();
+        if (selectedProfile == -1) {
             selectedProfile = getProfileForUser(getUser());
         }
         return selectedProfile;
diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java
index fca156a..e65d1fe 100644
--- a/core/java/com/android/internal/app/IntentForwarderActivity.java
+++ b/core/java/com/android/internal/app/IntentForwarderActivity.java
@@ -18,6 +18,8 @@
 
 import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
 
+import static com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE;
+
 import android.annotation.Nullable;
 import android.annotation.StringRes;
 import android.app.Activity;
@@ -26,6 +28,7 @@
 import android.app.AppGlobals;
 import android.app.admin.DevicePolicyManager;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -74,6 +77,9 @@
 
     private static final String TEL_SCHEME = "tel";
 
+    private static final ComponentName RESOLVER_COMPONENT_NAME =
+            new ComponentName("android", ResolverActivity.class.getName());
+
     private Injector mInjector;
 
     private MetricsLogger mMetricsLogger;
@@ -136,21 +142,50 @@
         }
 
         newIntent.prepareToLeaveUser(callingUserId);
-        maybeShowDisclosureAsync(intentReceived, newIntent, targetUserId, userMessageId);
-        CompletableFuture.runAsync(() ->
-                        startActivityAsCaller(newIntent, targetUserId), mExecutorService)
-                .thenAcceptAsync(result -> finish(), getApplicationContext().getMainExecutor());
+        final CompletableFuture<ResolveInfo> targetResolveInfoFuture =
+                mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId);
+        targetResolveInfoFuture
+                .thenApplyAsync(targetResolveInfo -> {
+                    if (isResolverActivityResolveInfo(targetResolveInfo)) {
+                        launchResolverActivityWithCorrectTab(intentReceived, className, newIntent,
+                                callingUserId, targetUserId);
+                        return targetResolveInfo;
+                    }
+                    startActivityAsCaller(newIntent, targetUserId);
+                    return targetResolveInfo;
+                }, mExecutorService)
+                .thenAcceptAsync(result -> {
+                    maybeShowDisclosure(intentReceived, result, userMessageId);
+                    finish();
+                }, getApplicationContext().getMainExecutor());
     }
 
-    private void maybeShowDisclosureAsync(
-            Intent intentReceived, Intent newIntent, int userId, int messageId) {
-        final CompletableFuture<ResolveInfo> resolveInfoFuture =
-                mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, userId);
-        resolveInfoFuture.thenAcceptAsync(ri -> {
-            if (shouldShowDisclosure(ri, intentReceived)) {
-                mInjector.showToast(messageId, Toast.LENGTH_LONG);
-            }
-        }, getApplicationContext().getMainExecutor());
+    private boolean isIntentForwarderResolveInfo(ResolveInfo resolveInfo) {
+        if (resolveInfo == null) {
+            return false;
+        }
+        ActivityInfo activityInfo = resolveInfo.activityInfo;
+        if (activityInfo == null) {
+            return false;
+        }
+        if (!"android".equals(activityInfo.packageName)) {
+            return false;
+        }
+        return activityInfo.name.equals(FORWARD_INTENT_TO_PARENT)
+                || activityInfo.name.equals(FORWARD_INTENT_TO_MANAGED_PROFILE);
+    }
+
+    private boolean isResolverActivityResolveInfo(@Nullable ResolveInfo resolveInfo) {
+        return resolveInfo != null
+                && resolveInfo.activityInfo != null
+                && RESOLVER_COMPONENT_NAME.equals(resolveInfo.activityInfo.getComponentName());
+    }
+
+    private void maybeShowDisclosure(
+            Intent intentReceived, ResolveInfo resolveInfo, int messageId) {
+        if (shouldShowDisclosure(resolveInfo, intentReceived)) {
+            mInjector.showToast(messageId, Toast.LENGTH_LONG);
+        }
     }
 
     private void startActivityAsCaller(Intent newIntent, int userId) {
@@ -185,7 +220,7 @@
         // when cross-profile intents are disabled.
         int selectedProfile = findSelectedProfile(className);
         sanitizeIntent(intentReceived);
-        intentReceived.putExtra(ChooserActivity.EXTRA_SELECTED_PROFILE, selectedProfile);
+        intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
         Intent innerIntent = intentReceived.getParcelableExtra(Intent.EXTRA_INTENT);
         if (innerIntent == null) {
             Slog.wtf(TAG, "Cannot start a chooser intent with no extra " + Intent.EXTRA_INTENT);
@@ -196,6 +231,25 @@
         finish();
     }
 
+    private void launchResolverActivityWithCorrectTab(Intent intentReceived, String className,
+            Intent newIntent, int callingUserId, int targetUserId) {
+        // When showing the intent resolver, instead of forwarding to the other profile,
+        // we launch it in the current user and select the other tab. This fixes b/155874820.
+        //
+        // In the case when there are 0 targets in the current profile and >1 apps in the other
+        // profile, the package manager launches the intent resolver in the other profile.
+        // If that's the case, we launch the resolver in the target user instead (other profile).
+        ResolveInfo callingResolveInfo = mInjector.resolveActivityAsUser(
+                newIntent, MATCH_DEFAULT_ONLY, callingUserId).join();
+        int userId = isIntentForwarderResolveInfo(callingResolveInfo)
+                ? targetUserId : callingUserId;
+        int selectedProfile = findSelectedProfile(className);
+        sanitizeIntent(intentReceived);
+        intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
+        startActivityAsCaller(intentReceived, null, null, false, userId);
+        finish();
+    }
+
     private int findSelectedProfile(String className) {
         if (className.equals(FORWARD_INTENT_TO_PARENT)) {
             return ChooserActivity.PROFILE_PERSONAL;
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 182c7f2..66d850e 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -179,6 +179,17 @@
     // Intent extra for connected audio devices
     public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
 
+    /**
+     * Integer extra to indicate which profile should be automatically selected.
+     * <p>Can only be used if there is a work profile.
+     * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
+     */
+    static final String EXTRA_SELECTED_PROFILE =
+            "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
+
+    static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
+    static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+
     private BroadcastReceiver mWorkProfileStateReceiver;
     private UserHandle mHeaderCreatorUser;
 
@@ -475,6 +486,10 @@
                 selectedProfile = PROFILE_WORK;
             }
         }
+        int selectedProfileExtra = getSelectedProfileExtra();
+        if (selectedProfileExtra != -1) {
+            selectedProfile = selectedProfileExtra;
+        }
         // We only show the default app for the profile of the current user. The filterLastUsed
         // flag determines whether to show a default app and that app is not shown in the
         // resolver list. So filterLastUsed should be false for the other profile.
@@ -512,6 +527,25 @@
     }
 
     /**
+     * Returns {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK} if the {@link
+     * #EXTRA_SELECTED_PROFILE} extra was supplied, or {@code -1} if no extra was supplied.
+     * @throws IllegalArgumentException if the value passed to the {@link #EXTRA_SELECTED_PROFILE}
+     * extra is not {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}
+     */
+    int getSelectedProfileExtra() {
+        int selectedProfile = -1;
+        if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) {
+            selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1);
+            if (selectedProfile != PROFILE_PERSONAL && selectedProfile != PROFILE_WORK) {
+                throw new IllegalArgumentException(EXTRA_SELECTED_PROFILE + " has invalid value "
+                        + selectedProfile + ". Must be either ResolverActivity.PROFILE_PERSONAL or "
+                        + "ResolverActivity.PROFILE_WORK.");
+            }
+        }
+        return selectedProfile;
+    }
+
+    /**
      * Returns the user id of the user that the starting intent originated from.
      * <p>This is not necessarily equal to {@link #getUserId()} or {@link UserHandle#myUserId()},
      * as there are edge cases when the intent resolver is launched in the other profile.
diff --git a/core/java/com/android/internal/content/NativeLibraryHelper.java b/core/java/com/android/internal/content/NativeLibraryHelper.java
index 02cf25a..476198b 100644
--- a/core/java/com/android/internal/content/NativeLibraryHelper.java
+++ b/core/java/com/android/internal/content/NativeLibraryHelper.java
@@ -506,7 +506,8 @@
         }
 
         for (int i = 0; i < apkPaths.length; i++) {
-            if (!incrementalStorage.configureNativeBinaries(apkPaths[i], libRelativeDir, abi)) {
+            if (!incrementalStorage.configureNativeBinaries(apkPaths[i], libRelativeDir, abi,
+                    handle.extractNativeLibs)) {
                 return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
             }
         }
diff --git a/core/java/com/android/internal/net/VpnProfile.java b/core/java/com/android/internal/net/VpnProfile.java
index 829bd8a..8ea5aa8 100644
--- a/core/java/com/android/internal/net/VpnProfile.java
+++ b/core/java/com/android/internal/net/VpnProfile.java
@@ -136,13 +136,19 @@
     public boolean isMetered = false;                            // 21
     public int maxMtu = PlatformVpnProfile.MAX_MTU_DEFAULT;      // 22
     public boolean areAuthParamsInline = false;                  // 23
+    public final boolean isRestrictedToTestNetworks;             // 24
 
     // Helper fields.
     @UnsupportedAppUsage
     public transient boolean saveLogin = false;
 
     public VpnProfile(String key) {
+        this(key, false);
+    }
+
+    public VpnProfile(String key, boolean isRestrictedToTestNetworks) {
         this.key = key;
+        this.isRestrictedToTestNetworks = isRestrictedToTestNetworks;
     }
 
     @UnsupportedAppUsage
@@ -171,6 +177,7 @@
         isMetered = in.readBoolean();
         maxMtu = in.readInt();
         areAuthParamsInline = in.readBoolean();
+        isRestrictedToTestNetworks = in.readBoolean();
     }
 
     /**
@@ -220,6 +227,7 @@
         out.writeBoolean(isMetered);
         out.writeInt(maxMtu);
         out.writeBoolean(areAuthParamsInline);
+        out.writeBoolean(isRestrictedToTestNetworks);
     }
 
     /**
@@ -237,12 +245,21 @@
             String[] values = new String(value, StandardCharsets.UTF_8).split(VALUE_DELIMITER, -1);
             // Acceptable numbers of values are:
             // 14-19: Standard profile, with option for serverCert, proxy
-            // 24: Standard profile with serverCert, proxy and platform-VPN parameters.
-            if ((values.length < 14 || values.length > 19) && values.length != 24) {
+            // 24: Standard profile with serverCert, proxy and platform-VPN parameters
+            // 25: Standard profile with platform-VPN parameters and isRestrictedToTestNetworks
+            if ((values.length < 14 || values.length > 19)
+                    && values.length != 24 && values.length != 25) {
                 return null;
             }
 
-            VpnProfile profile = new VpnProfile(key);
+            final boolean isRestrictedToTestNetworks;
+            if (values.length >= 25) {
+                isRestrictedToTestNetworks = Boolean.parseBoolean(values[24]);
+            } else {
+                isRestrictedToTestNetworks = false;
+            }
+
+            VpnProfile profile = new VpnProfile(key, isRestrictedToTestNetworks);
             profile.name = values[0];
             profile.type = Integer.parseInt(values[1]);
             if (profile.type < 0 || profile.type > TYPE_MAX) {
@@ -283,6 +300,8 @@
                 profile.areAuthParamsInline = Boolean.parseBoolean(values[23]);
             }
 
+            // isRestrictedToTestNetworks (values[24]) assigned as part of the constructor
+
             profile.saveLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
             return profile;
         } catch (Exception e) {
@@ -330,6 +349,7 @@
         builder.append(VALUE_DELIMITER).append(isMetered);
         builder.append(VALUE_DELIMITER).append(maxMtu);
         builder.append(VALUE_DELIMITER).append(areAuthParamsInline);
+        builder.append(VALUE_DELIMITER).append(isRestrictedToTestNetworks);
 
         return builder.toString().getBytes(StandardCharsets.UTF_8);
     }
@@ -421,7 +441,8 @@
         return Objects.hash(
             key, type, server, username, password, dnsServers, searchDomains, routes, mppe,
             l2tpSecret, ipsecIdentifier, ipsecSecret, ipsecUserCert, ipsecCaCert, ipsecServerCert,
-            proxy, mAllowedAlgorithms, isBypassable, isMetered, maxMtu, areAuthParamsInline);
+            proxy, mAllowedAlgorithms, isBypassable, isMetered, maxMtu, areAuthParamsInline,
+            isRestrictedToTestNetworks);
     }
 
     /** Checks VPN profiles for interior equality. */
@@ -453,7 +474,8 @@
                 && isBypassable == other.isBypassable
                 && isMetered == other.isMetered
                 && maxMtu == other.maxMtu
-                && areAuthParamsInline == other.areAuthParamsInline;
+                && areAuthParamsInline == other.areAuthParamsInline
+                && isRestrictedToTestNetworks == other.isRestrictedToTestNetworks;
     }
 
     @NonNull
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 415e210..5a1af84 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -9875,6 +9875,10 @@
         mPlatformIdleStateCallback = cb;
         mRailEnergyDataCallback = railStatsCb;
         mUserInfoProvider = userInfoProvider;
+
+        // Notify statsd that the system is initially not in doze.
+        mDeviceIdleMode = DEVICE_IDLE_MODE_OFF;
+        FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mDeviceIdleMode);
     }
 
     @UnsupportedAppUsage
diff --git a/core/res/res/values-sw600dp/config.xml b/core/res/res/values-sw600dp/config.xml
index 368e307..34b6a54 100644
--- a/core/res/res/values-sw600dp/config.xml
+++ b/core/res/res/values-sw600dp/config.xml
@@ -48,5 +48,8 @@
 
     <!-- Set to true to enable the user switcher on the keyguard. -->
     <bool name="config_keyguardUserSwitcher">true</bool>
+
+    <!-- If true, show multiuser switcher by default unless the user specifically disables it. -->
+    <bool name="config_showUserSwitcherByDefault">true</bool>
 </resources>
 
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2dc3996..9959de8 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4444,6 +4444,9 @@
     <!-- Set to true to enable the user switcher on the keyguard. -->
     <bool name="config_keyguardUserSwitcher">false</bool>
 
+    <!-- If true, show multiuser switcher by default unless the user specifically disables it. -->
+    <bool name="config_showUserSwitcherByDefault">false</bool>
+
     <!-- Set to true to make assistant show in front of the dream/screensaver. -->
     <bool name="config_assistantOnTopOfDream">false</bool>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 6ce25d4..61a6524 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4006,6 +4006,9 @@
   <!-- Set to true to enable the user switcher on the keyguard. -->
   <java-symbol type="bool" name="config_keyguardUserSwitcher" />
 
+  <!-- If true, show multiuser switcher by default unless the user specifically disables it. -->
+  <java-symbol type="bool" name="config_showUserSwitcherByDefault" />
+
   <!-- Set to true to make assistant show in front of the dream/screensaver. -->
   <java-symbol type="bool" name="config_assistantOnTopOfDream"/>
 
diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java
index 2884777..daaf31a 100644
--- a/core/tests/coretests/src/android/view/InsetsStateTest.java
+++ b/core/tests/coretests/src/android/view/InsetsStateTest.java
@@ -20,6 +20,8 @@
 import static android.view.InsetsState.ISIDE_TOP;
 import static android.view.InsetsState.ITYPE_BOTTOM_GESTURES;
 import static android.view.InsetsState.ITYPE_CAPTION_BAR;
+import static android.view.InsetsState.ITYPE_CLIMATE_BAR;
+import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_IME;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
@@ -183,6 +185,38 @@
     }
 
     @Test
+    public void testCalculateInsets_extraNavRightStatusTop() throws Exception {
+        try (InsetsModeSession session =
+                     new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_FULL)) {
+            mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
+            mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
+            mState.getSource(ITYPE_EXTRA_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300));
+            mState.getSource(ITYPE_EXTRA_NAVIGATION_BAR).setVisible(true);
+            WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
+                    false, DisplayCutout.NO_CUTOUT, 0, 0, null);
+            assertEquals(Insets.of(0, 100, 20, 0), insets.getSystemWindowInsets());
+            assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(Type.statusBars()));
+            assertEquals(Insets.of(0, 0, 20, 0), insets.getInsets(Type.navigationBars()));
+        }
+    }
+
+    @Test
+    public void testCalculateInsets_navigationRightClimateTop() throws Exception {
+        try (InsetsModeSession session =
+                     new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_FULL)) {
+            mState.getSource(ITYPE_CLIMATE_BAR).setFrame(new Rect(0, 0, 100, 100));
+            mState.getSource(ITYPE_CLIMATE_BAR).setVisible(true);
+            mState.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300));
+            mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(true);
+            WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
+                    false, DisplayCutout.NO_CUTOUT, 0, 0, null);
+            assertEquals(Insets.of(0, 100, 20, 0), insets.getSystemWindowInsets());
+            assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(Type.statusBars()));
+            assertEquals(Insets.of(0, 0, 20, 0), insets.getInsets(Type.navigationBars()));
+        }
+    }
+
+    @Test
     public void testStripForDispatch() {
         mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
         mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index bdb6bcc..8d73f8a 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -409,6 +409,8 @@
         <permission name="android.permission.ACCESS_TV_DESCRAMBLER" />
         <permission name="android.permission.ACCESS_TV_TUNER" />
         <permission name="android.permission.TUNER_RESOURCE_ACCESS" />
+        <!-- Permissions required for CTS test - TVInputManagerTest -->
+        <permission name="android.permission.TV_INPUT_HARDWARE" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index b5c19a8..bfc623f 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -253,6 +253,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/DisplayRotation.java"
     },
+    "-1554521902": {
+      "message": "showInsets(ime) was requested by different window: %s ",
+      "level": "WARN",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/ImeInsetsSourceProvider.java"
+    },
     "-1545962566": {
       "message": "View server did not start",
       "level": "WARN",
@@ -367,6 +373,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-1312861660": {
+      "message": "notifyInsetsControlChanged for %s ",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/WindowState.java"
+    },
     "-1292329638": {
       "message": "Added starting %s: startingWindow=%s startingView=%s",
       "level": "VERBOSE",
@@ -817,6 +829,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/TaskSnapshotSurface.java"
     },
+    "-395922585": {
+      "message": "InsetsSource setWin %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
     "-393505149": {
       "message": "unable to update pointer icon",
       "level": "WARN",
@@ -859,6 +877,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowSurfaceController.java"
     },
+    "-322743468": {
+      "message": "setInputMethodInputTarget %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "-322035974": {
       "message": "App freeze timeout expired.",
       "level": "WARN",
@@ -925,6 +949,12 @@
       "group": "WM_DEBUG_SCREEN_ON",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-112805366": {
+      "message": "InsetsSource updateVisibility serverVisible: %s clientVisible: %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
     "-106400104": {
       "message": "Preload recents with %s",
       "level": "DEBUG",
@@ -1003,6 +1033,12 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimationController.java"
     },
+    "29780972": {
+      "message": "InsetsSource Control %s for target %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
     "38267433": {
       "message": "Attempted to reset replacing window on non-existing app token %s",
       "level": "WARN",
@@ -1027,6 +1063,18 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "73987756": {
+      "message": "ControlAdapter onAnimationCancelled mSource: %s mControlTarget: %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
+    "75707221": {
+      "message": "ControlAdapter startAnimation mSource: %s controlTarget: %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
     "83950285": {
       "message": "removeAnimation(%d)",
       "level": "DEBUG",
@@ -1285,12 +1333,6 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "438102669": {
-      "message": "call showInsets(ime) on %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_IME",
-      "at": "com\/android\/server\/wm\/ImeInsetsSourceProvider.java"
-    },
     "457951957": {
       "message": "\tNot visible=%s",
       "level": "DEBUG",
@@ -1369,6 +1411,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowSurfaceController.java"
     },
+    "585839596": {
+      "message": "call showInsets(ime) on %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/ImeInsetsSourceProvider.java"
+    },
     "594260577": {
       "message": "createWallpaperAnimations()",
       "level": "DEBUG",
@@ -1873,6 +1921,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "1533154777": {
+      "message": "notifyInsetsChanged for %s ",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/WindowState.java"
+    },
     "1563755163": {
       "message": "Permission Denial: %s from pid=%d, uid=%d requires %s",
       "level": "WARN",
@@ -1897,6 +1951,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS_ANIM",
       "at": "com\/android\/server\/wm\/AppTransition.java"
     },
+    "1591969812": {
+      "message": "updateImeControlTarget %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "1628345525": {
       "message": "Now opening app %s",
       "level": "VERBOSE",
@@ -1927,6 +1987,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "1658605381": {
+      "message": "onImeControlTargetChanged %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/InsetsStateController.java"
+    },
     "1671994402": {
       "message": "Nulling last startingData",
       "level": "VERBOSE",
@@ -2173,6 +2239,12 @@
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "2119122320": {
+      "message": "setInputMethodTarget %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_IME",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "2128604122": {
       "message": "findFocusedWindow: No focusable windows.",
       "level": "VERBOSE",
diff --git a/data/sounds/AudioTv.mk b/data/sounds/AudioTv.mk
index 2a31e4c..fd53aff 100644
--- a/data/sounds/AudioTv.mk
+++ b/data/sounds/AudioTv.mk
@@ -16,6 +16,7 @@
 
 PRODUCT_COPY_FILES += \
     $(LOCAL_PATH)/Alarm_Beep_01.ogg:$(TARGET_COPY_OUT_PRODUCT)/media/audio/alarms/Alarm_Beep_02.ogg \
+    $(LOCAL_PATH)/effects/ogg/Effect_Tick_48k.ogg:$(TARGET_COPY_OUT_PRODUCT)/media/audio/ui/Effect_Tick.ogg \
     $(LOCAL_PATH)/effects/ogg/KeypressDelete_120_48k.ogg:$(TARGET_COPY_OUT_PRODUCT)/media/audio/ui/KeypressDelete.ogg \
     $(LOCAL_PATH)/effects/ogg/KeypressInvalid_120_48k.ogg:$(TARGET_COPY_OUT_PRODUCT)/media/audio/ui/KeypressInvalid.ogg \
     $(LOCAL_PATH)/effects/ogg/KeypressReturn_120_48k.ogg:$(TARGET_COPY_OUT_PRODUCT)/media/audio/ui/KeypressReturn.ogg \
diff --git a/graphics/java/android/graphics/pdf/PdfDocument.java b/graphics/java/android/graphics/pdf/PdfDocument.java
index 1b8336f..58421ab 100644
--- a/graphics/java/android/graphics/pdf/PdfDocument.java
+++ b/graphics/java/android/graphics/pdf/PdfDocument.java
@@ -46,8 +46,8 @@
  * // create a new document
  * PdfDocument document = new PdfDocument();
  *
- * // crate a page description
- * PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1).create();
+ * // create a page description
+ * PageInfo pageInfo = new PageInfo.Builder(100, 100, 1).create();
  *
  * // start a page
  * Page page = document.startPage(pageInfo);
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index 508a46f4..1fbb672 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -111,4 +111,8 @@
     // For preview channels and programs
     void sendTvInputNotifyIntent(in Intent intent, int userId);
     void requestChannelBrowsable(in Uri channelUri, int userId);
+
+    // For CTS purpose only. Add/remove a TvInputHardware device
+    void addHardwareDevice(in int deviceId);
+    void removeHardwareDevice(in int deviceId);
 }
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index e701055..98a01a4 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -23,6 +23,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
@@ -1801,6 +1802,40 @@
                 executor, callback);
     }
 
+    /**
+     * API to add a hardware device in the TvInputHardwareManager for CTS testing
+     * purpose.
+     *
+     * @param deviceId Id of the adding hardware device.
+     *
+     * @hide
+     */
+    @TestApi
+    public void addHardwareDevice(int deviceId) {
+        try {
+            mService.addHardwareDevice(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * API to remove a hardware device in the TvInputHardwareManager for CTS testing
+     * purpose.
+     *
+     * @param deviceId Id of the removing hardware device.
+     *
+     * @hide
+     */
+    @TestApi
+    public void removeHardwareDevice(int deviceId) {
+        try {
+            mService.removeHardwareDevice(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     private Hardware acquireTvInputHardwareInternal(int deviceId, TvInputInfo info,
             String tvInputSessionId, int priorityHint,
             Executor executor, final HardwareCallback callback) {
diff --git a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
index 9971c84..68071b0 100644
--- a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
+++ b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
@@ -105,28 +105,33 @@
 
 
     /**
-     * Attaches a filter to DVR interface for recording.
+     * Attaches a filter to DVR interface for playback.
      *
-     * <p>There can be multiple filters attached. Attached filters are independent, so the order
-     * doesn't matter.
+     * <p>This method will be deprecated. Now it's a no-op.
+     * <p>Filters opened by {@link Tuner#openFilter} are used for DVR playback.
      *
      * @param filter the filter to be attached.
      * @return result status of the operation.
      */
     @Result
     public int attachFilter(@NonNull Filter filter) {
-        return nativeAttachFilter(filter);
+        // no-op
+        return Tuner.RESULT_UNAVAILABLE;
     }
 
     /**
      * Detaches a filter from DVR interface.
      *
+     * <p>This method will be deprecated. Now it's a no-op.
+     * <p>Filters opened by {@link Tuner#openFilter} are used for DVR playback.
+     *
      * @param filter the filter to be detached.
      * @return result status of the operation.
      */
     @Result
     public int detachFilter(@NonNull Filter filter) {
-        return nativeDetachFilter(filter);
+        // no-op
+        return Tuner.RESULT_UNAVAILABLE;
     }
 
     /**
diff --git a/media/jni/soundpool/Android.bp b/media/jni/soundpool/Android.bp
index aa32793..6141308 100644
--- a/media/jni/soundpool/Android.bp
+++ b/media/jni/soundpool/Android.bp
@@ -1,5 +1,92 @@
+tidy_errors = [
+    // https://clang.llvm.org/extra/clang-tidy/checks/list.html
+    // For many categories, the checks are too many to specify individually.
+    // Feel free to disable as needed - as warnings are generally ignored,
+    // we treat warnings as errors.
+    "android-*",
+    "bugprone-*",
+    "cert-*",
+    "clang-analyzer-security*",
+    "google-*",
+    "misc-*",
+    //"modernize-*",  // explicitly list the modernize as they can be subjective.
+    "modernize-avoid-bind",
+    //"modernize-avoid-c-arrays", // std::array<> can be verbose
+    "modernize-concat-nested-namespaces",
+    //"modernize-deprecated-headers", // C headers still ok even if there is C++ equivalent.
+    "modernize-deprecated-ios-base-aliases",
+    "modernize-loop-convert",
+    "modernize-make-shared",
+    "modernize-make-unique",
+    "modernize-pass-by-value",
+    "modernize-raw-string-literal",
+    "modernize-redundant-void-arg",
+    "modernize-replace-auto-ptr",
+    "modernize-replace-random-shuffle",
+    "modernize-return-braced-init-list",
+    "modernize-shrink-to-fit",
+    "modernize-unary-static-assert",
+    "modernize-use-auto",  // debatable - auto can obscure type
+    "modernize-use-bool-literals",
+    "modernize-use-default-member-init",
+    "modernize-use-emplace",
+    "modernize-use-equals-default",
+    "modernize-use-equals-delete",
+    "modernize-use-nodiscard",
+    "modernize-use-noexcept",
+    "modernize-use-nullptr",
+    "modernize-use-override",
+    //"modernize-use-trailing-return-type", // not necessarily more readable
+    "modernize-use-transparent-functors",
+    "modernize-use-uncaught-exceptions",
+    "modernize-use-using",
+    "performance-*",
+
+    // Remove some pedantic stylistic requirements.
+    "-google-readability-casting", // C++ casts not always necessary and may be verbose
+    "-google-readability-todo",    // do not require TODO(info)
+    "-google-build-using-namespace", // Reenable and fix later.
+]
+
+cc_defaults {
+    name: "soundpool_flags_defaults",
+    // https://clang.llvm.org/docs/UsersManual.html#command-line-options
+    // https://clang.llvm.org/docs/DiagnosticsReference.html
+    cflags: [
+        "-Wall",
+        "-Wdeprecated",
+        "-Werror",
+        "-Werror=implicit-fallthrough",
+        "-Werror=sometimes-uninitialized",
+        //"-Werror=conditional-uninitialized",
+        "-Wextra",
+        "-Wredundant-decls",
+        "-Wshadow",
+        "-Wstrict-aliasing",
+        "-fstrict-aliasing",
+        "-Wthread-safety",
+        //"-Wthread-safety-negative", // experimental - looks broken in R.
+        "-Wunreachable-code",
+        "-Wunreachable-code-break",
+        "-Wunreachable-code-return",
+        "-Wunused",
+        "-Wused-but-marked-unused",
+    ],
+    // https://clang.llvm.org/extra/clang-tidy/
+    tidy: true,
+    tidy_checks: tidy_errors,
+    tidy_checks_as_errors: tidy_errors,
+    tidy_flags: [
+      "-format-style='file'",
+      "--header-filter='frameworks/base/media/jni/soundpool'",
+    ],
+}
+
 cc_library_shared {
     name: "libsoundpool",
+    defaults: [
+        "soundpool_flags_defaults",
+    ],
 
     srcs: [
         "android_media_SoundPool.cpp",
diff --git a/media/jni/soundpool/Sound.cpp b/media/jni/soundpool/Sound.cpp
index 0bbc3e4..c3abdc2 100644
--- a/media/jni/soundpool/Sound.cpp
+++ b/media/jni/soundpool/Sound.cpp
@@ -31,7 +31,7 @@
 
 Sound::Sound(int32_t soundID, int fd, int64_t offset, int64_t length)
     : mSoundID(soundID)
-    , mFd(dup(fd))
+    , mFd(fcntl(fd, F_DUPFD_CLOEXEC)) // like dup(fd) but closes on exec to prevent leaks.
     , mOffset(offset)
     , mLength(length)
 {
@@ -47,7 +47,7 @@
 
 static status_t decode(int fd, int64_t offset, int64_t length,
         uint32_t *rate, int32_t *channelCount, audio_format_t *audioFormat,
-        audio_channel_mask_t *channelMask, sp<MemoryHeapBase> heap,
+        audio_channel_mask_t *channelMask, const sp<MemoryHeapBase>& heap,
         size_t *sizeInBytes) {
     ALOGV("%s(fd=%d, offset=%lld, length=%lld, ...)",
             __func__, fd, (long long)offset, (long long)length);
@@ -81,7 +81,7 @@
 
             bool sawInputEOS = false;
             bool sawOutputEOS = false;
-            uint8_t* writePos = static_cast<uint8_t*>(heap->getBase());
+            auto writePos = static_cast<uint8_t*>(heap->getBase());
             size_t available = heap->getSize();
             size_t written = 0;
             format.reset(AMediaCodec_getOutputFormat(codec.get())); // update format.
@@ -204,7 +204,7 @@
         int32_t channelCount;
         audio_format_t format;
         audio_channel_mask_t channelMask;
-        status_t status = decode(mFd.get(), mOffset, mLength, &sampleRate, &channelCount, &format,
+        status = decode(mFd.get(), mOffset, mLength, &sampleRate, &channelCount, &format,
                         &channelMask, mHeap, &mSizeInBytes);
         ALOGV("%s: close(%d)", __func__, mFd.get());
         mFd.reset();  // close
diff --git a/media/jni/soundpool/SoundDecoder.cpp b/media/jni/soundpool/SoundDecoder.cpp
index 12200ef..6614fdb 100644
--- a/media/jni/soundpool/SoundDecoder.cpp
+++ b/media/jni/soundpool/SoundDecoder.cpp
@@ -57,7 +57,7 @@
     mThreadPool->quit();
 }
 
-void SoundDecoder::run(int32_t id __unused /* ALOGV only */)
+void SoundDecoder::run(int32_t id)
 {
     ALOGV("%s(%d): entering", __func__, id);
     std::unique_lock lock(mLock);
diff --git a/media/jni/soundpool/SoundDecoder.h b/media/jni/soundpool/SoundDecoder.h
index 1288943..7b62114 100644
--- a/media/jni/soundpool/SoundDecoder.h
+++ b/media/jni/soundpool/SoundDecoder.h
@@ -30,21 +30,22 @@
 public:
     SoundDecoder(SoundManager* soundManager, size_t threads);
     ~SoundDecoder();
-    void loadSound(int32_t soundID);
+    void loadSound(int32_t soundID) NO_THREAD_SAFETY_ANALYSIS; // uses unique_lock
     void quit();
 
 private:
-    void run(int32_t id);                       // The decode thread function.
+    // The decode thread function.
+    void run(int32_t id) NO_THREAD_SAFETY_ANALYSIS; // uses unique_lock
 
     SoundManager* const     mSoundManager;      // set in constructor, has own lock
     std::unique_ptr<ThreadPool> mThreadPool;    // set in constructor, has own lock
 
     std::mutex              mLock;
-    std::condition_variable mQueueSpaceAvailable;
-    std::condition_variable mQueueDataAvailable;
+    std::condition_variable mQueueSpaceAvailable GUARDED_BY(mLock);
+    std::condition_variable mQueueDataAvailable GUARDED_BY(mLock);
 
-    std::deque<int32_t>     mSoundIDs;            // GUARDED_BY(mLock);
-    bool                    mQuit = false;        // GUARDED_BY(mLock);
+    std::deque<int32_t>     mSoundIDs GUARDED_BY(mLock);
+    bool                    mQuit GUARDED_BY(mLock) = false;
 };
 
 } // end namespace android::soundpool
diff --git a/media/jni/soundpool/SoundManager.cpp b/media/jni/soundpool/SoundManager.cpp
index 3c625bf..5b16174 100644
--- a/media/jni/soundpool/SoundManager.cpp
+++ b/media/jni/soundpool/SoundManager.cpp
@@ -43,7 +43,7 @@
     mSounds.clear();
 }
 
-int32_t SoundManager::load(int fd, int64_t offset, int64_t length, int32_t priority __unused)
+int32_t SoundManager::load(int fd, int64_t offset, int64_t length, int32_t priority)
 {
     ALOGV("%s(fd=%d, offset=%lld, length=%lld, priority=%d)",
             __func__, fd, (long long)offset, (long long)length, priority);
diff --git a/media/jni/soundpool/SoundManager.h b/media/jni/soundpool/SoundManager.h
index 9201e78..4a4e3b8 100644
--- a/media/jni/soundpool/SoundManager.h
+++ b/media/jni/soundpool/SoundManager.h
@@ -21,6 +21,8 @@
 #include <mutex>
 #include <unordered_map>
 
+#include <android-base/thread_annotations.h>
+
 namespace android {
 
 class SoundPool;
@@ -91,20 +93,21 @@
         }
     private:
         mutable std::recursive_mutex  mCallbackLock; // allow mCallback to setCallback().
+                                          // No thread-safety checks in R for recursive_mutex.
         SoundPool*          mSoundPool = nullptr; // GUARDED_BY(mCallbackLock)
         SoundPoolCallback*  mCallback = nullptr;  // GUARDED_BY(mCallbackLock)
         void*               mUserData = nullptr;  // GUARDED_BY(mCallbackLock)
     };
 
-    std::shared_ptr<Sound> findSound_l(int32_t soundID) const;
+    std::shared_ptr<Sound> findSound_l(int32_t soundID) const REQUIRES(mSoundManagerLock);
 
     // The following variables are initialized in constructor and can be accessed anytime.
-    CallbackHandler         mCallbackHandler;              // has its own lock
-    const std::unique_ptr<SoundDecoder> mDecoder;          // has its own lock
+    CallbackHandler mCallbackHandler;              // has its own lock
+    const std::unique_ptr<SoundDecoder> mDecoder;  // has its own lock
 
-    mutable std::mutex      mSoundManagerLock;
-    std::unordered_map<int, std::shared_ptr<Sound>> mSounds; // GUARDED_BY(mSoundManagerLock)
-    int32_t                 mNextSoundID = 0;    // GUARDED_BY(mSoundManagerLock)
+    mutable std::mutex mSoundManagerLock;
+    std::unordered_map<int, std::shared_ptr<Sound>> mSounds GUARDED_BY(mSoundManagerLock);
+    int32_t mNextSoundID GUARDED_BY(mSoundManagerLock) = 0;
 };
 
 } // namespace android::soundpool
diff --git a/media/jni/soundpool/Stream.cpp b/media/jni/soundpool/Stream.cpp
index e3152d6..a6d9758 100644
--- a/media/jni/soundpool/Stream.cpp
+++ b/media/jni/soundpool/Stream.cpp
@@ -105,7 +105,7 @@
     if (streamID == mStreamID) {
         mRate = rate;
         if (mAudioTrack != nullptr && mSound != nullptr) {
-            const uint32_t sampleRate = uint32_t(float(mSound->getSampleRate()) * rate + 0.5);
+            const auto sampleRate = (uint32_t)lround(double(mSound->getSampleRate()) * rate);
             mAudioTrack->setSampleRate(sampleRate);
         }
     }
@@ -214,8 +214,11 @@
 
 void Stream::clearAudioTrack()
 {
+    sp<AudioTrack> release;  // release outside of lock.
+    std::lock_guard lock(mLock);
     // This will invoke the destructor which waits for the AudioTrack thread to join,
     // and is currently the only safe way to ensure there are no callbacks afterwards.
+    release = mAudioTrack;  // or std::swap if we had move semantics.
     mAudioTrack.clear();
 }
 
@@ -288,7 +291,7 @@
         const audio_stream_type_t streamType =
                 AudioSystem::attributesToStreamType(*mStreamManager->getAttributes());
         const int32_t channelCount = sound->getChannelCount();
-        const uint32_t sampleRate = uint32_t(float(sound->getSampleRate()) * rate + 0.5);
+        const auto sampleRate = (uint32_t)lround(double(sound->getSampleRate()) * rate);
         size_t frameCount = 0;
 
         if (loop) {
@@ -307,7 +310,7 @@
                         __func__, mAudioTrack.get(), sound->getSoundID());
             }
         }
-        if (newTrack == 0) {
+        if (newTrack == nullptr) {
             // mToggle toggles each time a track is started on a given stream.
             // The toggle is concatenated with the Stream address and passed to AudioTrack
             // as callback user data. This enables the detection of callbacks received from the old
@@ -380,9 +383,9 @@
 /* static */
 void Stream::staticCallback(int event, void* user, void* info)
 {
-    const uintptr_t userAsInt = (uintptr_t)user;
-    Stream* stream = reinterpret_cast<Stream*>(userAsInt & ~1);
-    stream->callback(event, info, userAsInt & 1, 0 /* tries */);
+    const auto userAsInt = (uintptr_t)user;
+    auto stream = reinterpret_cast<Stream*>(userAsInt & ~1);
+    stream->callback(event, info, int(userAsInt & 1), 0 /* tries */);
 }
 
 void Stream::callback(int event, void* info, int toggle, int tries)
diff --git a/media/jni/soundpool/Stream.h b/media/jni/soundpool/Stream.h
index 82d2690..fd92921 100644
--- a/media/jni/soundpool/Stream.h
+++ b/media/jni/soundpool/Stream.h
@@ -18,6 +18,7 @@
 
 #include "Sound.h"
 
+#include <android-base/thread_annotations.h>
 #include <audio_utils/clock.h>
 #include <media/AudioTrack.h>
 
@@ -104,47 +105,55 @@
 
     // The following getters are not locked and have weak consistency.
     // These are considered advisory only - being stale is of nuisance.
-    int32_t getPriority() const { return mPriority; }
-    int32_t getPairPriority() const { return getPairStream()->getPriority(); }
-    int64_t getStopTimeNs() const { return mStopTimeNs; }
+    int32_t getPriority() const NO_THREAD_SAFETY_ANALYSIS { return mPriority; }
+    int32_t getPairPriority() const NO_THREAD_SAFETY_ANALYSIS {
+        return getPairStream()->getPriority();
+    }
+    int64_t getStopTimeNs() const NO_THREAD_SAFETY_ANALYSIS { return mStopTimeNs; }
 
-    int32_t getStreamID() const { return mStreamID; }  // Can change with setPlay()
-    int32_t getSoundID() const { return mSoundID; }    // Can change with play_l()
-    bool hasSound() const { return mSound.get() != nullptr; }
+    // Can change with setPlay()
+    int32_t getStreamID() const NO_THREAD_SAFETY_ANALYSIS { return mStreamID; }
 
-    Stream* getPairStream() const;  // this never changes.  See top of header.
+    // Can change with play_l()
+    int32_t getSoundID() const NO_THREAD_SAFETY_ANALYSIS { return mSoundID; }
+
+    bool hasSound() const NO_THREAD_SAFETY_ANALYSIS { return mSound.get() != nullptr; }
+
+    // This never changes.  See top of header.
+    Stream* getPairStream() const;
 
 private:
     void play_l(const std::shared_ptr<Sound>& sound, int streamID,
             float leftVolume, float rightVolume, int priority, int loop, float rate,
-            sp<AudioTrack> releaseTracks[2]);
-    void stop_l();
-    void setVolume_l(float leftVolume, float rightVolume);
+            sp<AudioTrack> releaseTracks[2]) REQUIRES(mLock);
+    void stop_l() REQUIRES(mLock);
+    void setVolume_l(float leftVolume, float rightVolume) REQUIRES(mLock);
 
     // For use with AudioTrack callback.
     static void staticCallback(int event, void* user, void* info);
-    void callback(int event, void* info, int toggle, int tries);
+    void callback(int event, void* info, int toggle, int tries)
+            NO_THREAD_SAFETY_ANALYSIS; // uses unique_lock
 
     // StreamManager should be set on construction and not changed.
     // release mLock before calling into StreamManager
     StreamManager*     mStreamManager = nullptr;
 
     mutable std::mutex  mLock;
-    std::atomic_int32_t mStreamID = 0;          // Note: valid streamIDs are always positive.
-    int                 mState = IDLE;
-    std::shared_ptr<Sound> mSound;              // Non-null if playing.
-    int32_t             mSoundID = 0;           // The sound ID associated with the AudioTrack.
-    float               mLeftVolume = 0.f;
-    float               mRightVolume = 0.f;
-    int32_t             mPriority = INT32_MIN;
-    int32_t             mLoop = 0;
-    float               mRate = 0.f;
-    bool                mAutoPaused = false;
-    bool                mMuted = false;
+    std::atomic_int32_t mStreamID GUARDED_BY(mLock) = 0; // Valid streamIDs are always positive.
+    int                 mState GUARDED_BY(mLock) = IDLE;
+    std::shared_ptr<Sound> mSound GUARDED_BY(mLock);    // Non-null if playing.
+    int32_t             mSoundID GUARDED_BY(mLock) = 0; // SoundID associated with AudioTrack.
+    float               mLeftVolume GUARDED_BY(mLock) = 0.f;
+    float               mRightVolume GUARDED_BY(mLock) = 0.f;
+    int32_t             mPriority GUARDED_BY(mLock) = INT32_MIN;
+    int32_t             mLoop GUARDED_BY(mLock) = 0;
+    float               mRate GUARDED_BY(mLock) = 0.f;
+    bool                mAutoPaused GUARDED_BY(mLock) = false;
+    bool                mMuted GUARDED_BY(mLock) = false;
 
-    sp<AudioTrack>      mAudioTrack;
-    int                 mToggle = 0;
-    int64_t             mStopTimeNs = 0;        // if nonzero, time to wait for stop.
+    sp<AudioTrack>      mAudioTrack GUARDED_BY(mLock);
+    int                 mToggle GUARDED_BY(mLock) = 0;
+    int64_t             mStopTimeNs GUARDED_BY(mLock) = 0;  // if nonzero, time to wait for stop.
 };
 
 } // namespace android::soundpool
diff --git a/media/jni/soundpool/StreamManager.cpp b/media/jni/soundpool/StreamManager.cpp
index c612218..9ff4254 100644
--- a/media/jni/soundpool/StreamManager.cpp
+++ b/media/jni/soundpool/StreamManager.cpp
@@ -55,7 +55,7 @@
         streams = 1;
     }
     mStreamPoolSize = streams * 2;
-    mStreamPool.reset(new Stream[mStreamPoolSize]);
+    mStreamPool = std::make_unique<Stream[]>(mStreamPoolSize); // create array of streams.
     // we use a perfect hash table with 2x size to map StreamIDs to Stream pointers.
     mPerfectHash = std::make_unique<PerfectHash<int32_t, Stream *>>(roundup(mStreamPoolSize * 2));
 }
@@ -69,7 +69,7 @@
 size_t StreamMap::streamPosition(const Stream* stream) const
 {
     ptrdiff_t index = stream - mStreamPool.get();
-    LOG_ALWAYS_FATAL_IF(index < 0 || index >= mStreamPoolSize,
+    LOG_ALWAYS_FATAL_IF(index < 0 || (size_t)index >= mStreamPoolSize,
             "%s: stream position out of range: %td", __func__, index);
     return (size_t)index;
 }
@@ -92,6 +92,11 @@
 
 ////////////
 
+// Thread safety analysis is supposed to be disabled for constructors and destructors
+// but clang in R seems to have a bug.  We use pragma to disable.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wthread-safety-analysis"
+
 StreamManager::StreamManager(
         int32_t streams, size_t threads, const audio_attributes_t* attributes)
     : StreamMap(streams)
@@ -110,6 +115,8 @@
             "SoundPool_");
 }
 
+#pragma clang diagnostic pop
+
 StreamManager::~StreamManager()
 {
     ALOGV("%s", __func__);
diff --git a/media/jni/soundpool/StreamManager.h b/media/jni/soundpool/StreamManager.h
index 30ad220..59ae2f9 100644
--- a/media/jni/soundpool/StreamManager.h
+++ b/media/jni/soundpool/StreamManager.h
@@ -183,9 +183,9 @@
     std::atomic_size_t      mActiveThreadCount = 0;
 
     std::mutex              mThreadLock;
-    bool                    mQuit = false;           // GUARDED_BY(mThreadLock)
-    int32_t                 mNextThreadId = 0;       // GUARDED_BY(mThreadLock)
-    std::list<std::unique_ptr<JavaThread>> mThreads; // GUARDED_BY(mThreadLock)
+    bool                    mQuit GUARDED_BY(mThreadLock) = false;
+    int32_t                 mNextThreadId GUARDED_BY(mThreadLock) = 0;
+    std::list<std::unique_ptr<JavaThread>> mThreads GUARDED_BY(mThreadLock);
 };
 
 /**
@@ -263,7 +263,7 @@
     mutable std::mutex          mHashLock;
     const size_t                mHashCapacity; // size of mK2V no lock needed.
     std::unique_ptr<std::atomic<V>[]> mK2V;    // no lock needed for read access.
-    K                           mNextKey{};    // GUARDED_BY(mHashLock)
+    K                           mNextKey GUARDED_BY(mHashLock) {};
 };
 
 /**
@@ -392,7 +392,8 @@
     // Returns positive streamID on success, 0 on failure.  This is locked.
     int32_t queueForPlay(const std::shared_ptr<Sound> &sound,
             int32_t soundID, float leftVolume, float rightVolume,
-            int32_t priority, int32_t loop, float rate);
+            int32_t priority, int32_t loop, float rate)
+            NO_THREAD_SAFETY_ANALYSIS; // uses unique_lock
 
     ///////////////////////////////////////////////////////////////////////
     // Called from soundpool::Stream
@@ -407,11 +408,11 @@
 
 private:
 
-    void run(int32_t id);                        // worker thread, takes lock internally.
+    void run(int32_t id) NO_THREAD_SAFETY_ANALYSIS; // worker thread, takes unique_lock.
     void dump() const;                           // no lock needed
 
     // returns true if more worker threads are needed.
-    bool needMoreThreads_l() {
+    bool needMoreThreads_l() REQUIRES(mStreamManagerLock) {
         return mRestartStreams.size() > 0 &&
                 (mThreadPool->getActiveThreadCount() == 0
                 || std::distance(mRestartStreams.begin(),
@@ -420,14 +421,16 @@
     }
 
     // returns true if the stream was added.
-    bool moveToRestartQueue_l(Stream* stream, int32_t activeStreamIDToMatch = 0);
+    bool moveToRestartQueue_l(
+            Stream* stream, int32_t activeStreamIDToMatch = 0) REQUIRES(mStreamManagerLock);
     // returns number of queues the stream was removed from (should be 0 or 1);
     // a special code of -1 is returned if activeStreamIDToMatch is > 0 and
     // the stream wasn't found on the active queue.
-    ssize_t removeFromQueues_l(Stream* stream, int32_t activeStreamIDToMatch = 0);
-    void addToRestartQueue_l(Stream *stream);
-    void addToActiveQueue_l(Stream *stream);
-    void sanityCheckQueue_l() const;
+    ssize_t removeFromQueues_l(
+            Stream* stream, int32_t activeStreamIDToMatch = 0) REQUIRES(mStreamManagerLock);
+    void addToRestartQueue_l(Stream *stream) REQUIRES(mStreamManagerLock);
+    void addToActiveQueue_l(Stream *stream) REQUIRES(mStreamManagerLock);
+    void sanityCheckQueue_l() const REQUIRES(mStreamManagerLock);
 
     const audio_attributes_t mAttributes;
     std::unique_ptr<ThreadPool> mThreadPool;                  // locked internally
@@ -436,9 +439,9 @@
     // 4 stream queues by the Manager Thread or by the user initiated play().
     // A stream pair has exactly one stream on exactly one of the queues.
     std::mutex                  mStreamManagerLock;
-    std::condition_variable     mStreamManagerCondition;
+    std::condition_variable     mStreamManagerCondition GUARDED_BY(mStreamManagerLock);
 
-    bool                        mQuit = false;      // GUARDED_BY(mStreamManagerLock)
+    bool                        mQuit GUARDED_BY(mStreamManagerLock) = false;
 
     // There are constructor arg "streams" pairs of streams, only one of each
     // pair on the 4 stream queues below.  The other stream in the pair serves as
@@ -452,24 +455,24 @@
     // The paired stream may be active (but with no AudioTrack), and will be restarted
     // with an active AudioTrack when the current stream is stopped.
     std::multimap<int64_t /* stopTimeNs */, Stream*>
-                                mRestartStreams;    // GUARDED_BY(mStreamManagerLock)
+                                mRestartStreams GUARDED_BY(mStreamManagerLock);
 
     // 2) mActiveStreams: Streams that are active.
     // The paired stream will be inactive.
     // This is in order of specified by kStealActiveStream_OldestFirst
-    std::list<Stream*>          mActiveStreams;     // GUARDED_BY(mStreamManagerLock)
+    std::list<Stream*>          mActiveStreams GUARDED_BY(mStreamManagerLock);
 
     // 3) mAvailableStreams: Streams that are inactive.
     // The paired stream will also be inactive.
     // No particular order.
-    std::unordered_set<Stream*> mAvailableStreams;  // GUARDED_BY(mStreamManagerLock)
+    std::unordered_set<Stream*> mAvailableStreams GUARDED_BY(mStreamManagerLock);
 
     // 4) mProcessingStreams: Streams that are being processed by the ManagerThreads
     // When on this queue, the stream and its pair are not available for stealing.
     // Each ManagerThread will have at most one stream on the mProcessingStreams queue.
     // The paired stream may be active or restarting.
     // No particular order.
-    std::unordered_set<Stream*> mProcessingStreams; // GUARDED_BY(mStreamManagerLock)
+    std::unordered_set<Stream*> mProcessingStreams GUARDED_BY(mStreamManagerLock);
 };
 
 } // namespace android::soundpool
diff --git a/media/jni/soundpool/android_media_SoundPool.cpp b/media/jni/soundpool/android_media_SoundPool.cpp
index f670636..8f6df3d 100644
--- a/media/jni/soundpool/android_media_SoundPool.cpp
+++ b/media/jni/soundpool/android_media_SoundPool.cpp
@@ -52,7 +52,7 @@
 {
     ALOGV("android_media_SoundPool_load_FD");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return 0;
+    if (ap == nullptr) return 0;
     return (jint) ap->load(jniGetFDFromFileDescriptor(env, fileDescriptor),
             int64_t(offset), int64_t(length), int(priority));
 }
@@ -61,7 +61,7 @@
 android_media_SoundPool_unload(JNIEnv *env, jobject thiz, jint sampleID) {
     ALOGV("android_media_SoundPool_unload\n");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return JNI_FALSE;
+    if (ap == nullptr) return JNI_FALSE;
     return ap->unload(sampleID) ? JNI_TRUE : JNI_FALSE;
 }
 
@@ -72,7 +72,7 @@
 {
     ALOGV("android_media_SoundPool_play\n");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return 0;
+    if (ap == nullptr) return 0;
     return (jint) ap->play(sampleID, leftVolume, rightVolume, priority, loop, rate);
 }
 
@@ -81,7 +81,7 @@
 {
     ALOGV("android_media_SoundPool_pause");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->pause(channelID);
 }
 
@@ -90,7 +90,7 @@
 {
     ALOGV("android_media_SoundPool_resume");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->resume(channelID);
 }
 
@@ -99,7 +99,7 @@
 {
     ALOGV("android_media_SoundPool_autoPause");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->autoPause();
 }
 
@@ -108,7 +108,7 @@
 {
     ALOGV("android_media_SoundPool_autoResume");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->autoResume();
 }
 
@@ -117,7 +117,7 @@
 {
     ALOGV("android_media_SoundPool_stop");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->stop(channelID);
 }
 
@@ -127,7 +127,7 @@
 {
     ALOGV("android_media_SoundPool_setVolume");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->setVolume(channelID, (float) leftVolume, (float) rightVolume);
 }
 
@@ -136,7 +136,7 @@
 {
     ALOGV("android_media_SoundPool_mute(%d)", muting);
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->mute(muting == JNI_TRUE);
 }
 
@@ -146,7 +146,7 @@
 {
     ALOGV("android_media_SoundPool_setPriority");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->setPriority(channelID, (int) priority);
 }
 
@@ -156,7 +156,7 @@
 {
     ALOGV("android_media_SoundPool_setLoop");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->setLoop(channelID, loop);
 }
 
@@ -166,7 +166,7 @@
 {
     ALOGV("android_media_SoundPool_setRate");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap == NULL) return;
+    if (ap == nullptr) return;
     ap->setRate(channelID, (float) rate);
 }
 
@@ -174,24 +174,26 @@
 {
     ALOGV("callback: (%d, %d, %d, %p, %p)", event.mMsg, event.mArg1, event.mArg2, soundPool, user);
     JNIEnv *env = AndroidRuntime::getJNIEnv();
-    env->CallStaticVoidMethod(fields.mSoundPoolClass, fields.mPostEvent, user, event.mMsg, event.mArg1, event.mArg2, NULL);
+    env->CallStaticVoidMethod(
+            fields.mSoundPoolClass, fields.mPostEvent, user, event.mMsg, event.mArg1, event.mArg2,
+            nullptr /* object */);
 }
 
 static jint
 android_media_SoundPool_native_setup(JNIEnv *env, jobject thiz, jobject weakRef,
         jint maxChannels, jobject jaa)
 {
-    if (jaa == 0) {
+    if (jaa == nullptr) {
         ALOGE("Error creating SoundPool: invalid audio attributes");
         return -1;
     }
 
-    audio_attributes_t *paa = NULL;
+    audio_attributes_t *paa = nullptr;
     // read the AudioAttributes values
     paa = (audio_attributes_t *) calloc(1, sizeof(audio_attributes_t));
-    const jstring jtags =
+    const auto jtags =
             (jstring) env->GetObjectField(jaa, javaAudioAttrFields.fieldFormattedTags);
-    const char* tags = env->GetStringUTFChars(jtags, NULL);
+    const char* tags = env->GetStringUTFChars(jtags, nullptr);
     // copying array size -1, char array for tags was calloc'd, no need to NULL-terminate it
     strncpy(paa->tags, tags, AUDIO_ATTRIBUTES_TAGS_MAX_SIZE - 1);
     env->ReleaseStringUTFChars(jtags, tags);
@@ -201,8 +203,8 @@
     paa->flags = env->GetIntField(jaa, javaAudioAttrFields.fieldFlags);
 
     ALOGV("android_media_SoundPool_native_setup");
-    SoundPool *ap = new SoundPool(maxChannels, paa);
-    if (ap == NULL) {
+    auto *ap = new SoundPool(maxChannels, paa);
+    if (ap == nullptr) {
         return -1;
     }
 
@@ -224,12 +226,12 @@
 {
     ALOGV("android_media_SoundPool_release");
     SoundPool *ap = MusterSoundPool(env, thiz);
-    if (ap != NULL) {
+    if (ap != nullptr) {
 
         // release weak reference and clear callback
-        jobject weakRef = (jobject) ap->getUserData();
-        ap->setCallback(NULL, NULL);
-        if (weakRef != NULL) {
+        auto weakRef = (jobject) ap->getUserData();
+        ap->setCallback(nullptr /* callback */, nullptr /* user */);
+        if (weakRef != nullptr) {
             env->DeleteGlobalRef(weakRef);
         }
 
@@ -309,7 +311,7 @@
 
 jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
 {
-    JNIEnv* env = NULL;
+    JNIEnv* env = nullptr;
     jint result = -1;
     jclass clazz;
 
@@ -317,23 +319,23 @@
         ALOGE("ERROR: GetEnv failed\n");
         return result;
     }
-    assert(env != NULL);
+    assert(env != nullptr);
 
     clazz = env->FindClass(kClassPathName);
-    if (clazz == NULL) {
+    if (clazz == nullptr) {
         ALOGE("Can't find %s", kClassPathName);
         return result;
     }
 
     fields.mNativeContext = env->GetFieldID(clazz, "mNativeContext", "J");
-    if (fields.mNativeContext == NULL) {
+    if (fields.mNativeContext == nullptr) {
         ALOGE("Can't find SoundPool.mNativeContext");
         return result;
     }
 
     fields.mPostEvent = env->GetStaticMethodID(clazz, "postEventFromNative",
                                                "(Ljava/lang/Object;IIILjava/lang/Object;)V");
-    if (fields.mPostEvent == NULL) {
+    if (fields.mPostEvent == nullptr) {
         ALOGE("Can't find android/media/SoundPool.postEventFromNative");
         return result;
     }
@@ -342,16 +344,18 @@
     // since it's a static object.
     fields.mSoundPoolClass = (jclass) env->NewGlobalRef(clazz);
 
-    if (AndroidRuntime::registerNativeMethods(env, kClassPathName, gMethods, NELEM(gMethods)) < 0)
+    if (AndroidRuntime::registerNativeMethods(
+                env, kClassPathName, gMethods, NELEM(gMethods)) < 0) {
         return result;
+    }
 
     // Get the AudioAttributes class and fields
     jclass audioAttrClass = env->FindClass(kAudioAttributesClassPathName);
-    if (audioAttrClass == NULL) {
+    if (audioAttrClass == nullptr) {
         ALOGE("Can't find %s", kAudioAttributesClassPathName);
         return result;
     }
-    jclass audioAttributesClassRef = (jclass)env->NewGlobalRef(audioAttrClass);
+    auto audioAttributesClassRef = (jclass)env->NewGlobalRef(audioAttrClass);
     javaAudioAttrFields.fieldUsage = env->GetFieldID(audioAttributesClassRef, "mUsage", "I");
     javaAudioAttrFields.fieldContentType
                                    = env->GetFieldID(audioAttributesClassRef, "mContentType", "I");
@@ -359,9 +363,10 @@
     javaAudioAttrFields.fieldFormattedTags =
             env->GetFieldID(audioAttributesClassRef, "mFormattedTags", "Ljava/lang/String;");
     env->DeleteGlobalRef(audioAttributesClassRef);
-    if (javaAudioAttrFields.fieldUsage == NULL || javaAudioAttrFields.fieldContentType == NULL
-            || javaAudioAttrFields.fieldFlags == NULL
-            || javaAudioAttrFields.fieldFormattedTags == NULL) {
+    if (javaAudioAttrFields.fieldUsage == nullptr
+            || javaAudioAttrFields.fieldContentType == nullptr
+            || javaAudioAttrFields.fieldFlags == nullptr
+            || javaAudioAttrFields.fieldFormattedTags == nullptr) {
         ALOGE("Can't initialize AudioAttributes fields");
         return result;
     }
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/statusbar/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/car/statusbar/CarStatusBar.java
index d18eadd..d692487 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/statusbar/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/statusbar/CarStatusBar.java
@@ -45,7 +45,6 @@
 import com.android.systemui.car.navigationbar.CarNavigationBarController;
 import com.android.systemui.classifier.FalsingLog;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.keyguard.DismissCallbackRegistry;
@@ -173,7 +172,6 @@
             DisplayMetrics displayMetrics,
             MetricsLogger metricsLogger,
             @UiBackground Executor uiBgExecutor,
-            @Main Executor mainExecutor,
             NotificationMediaManager notificationMediaManager,
             NotificationLockscreenUserManager lockScreenUserManager,
             NotificationRemoteInputManager remoteInputManager,
@@ -254,7 +252,6 @@
                 displayMetrics,
                 metricsLogger,
                 uiBgExecutor,
-                mainExecutor,
                 notificationMediaManager,
                 lockScreenUserManager,
                 remoteInputManager,
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/statusbar/CarStatusBarModule.java b/packages/CarSystemUI/src/com/android/systemui/car/statusbar/CarStatusBarModule.java
index 4f6890e..dc2eb04 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/statusbar/CarStatusBarModule.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/statusbar/CarStatusBarModule.java
@@ -33,7 +33,6 @@
 import com.android.systemui.car.CarDeviceProvisionedController;
 import com.android.systemui.car.navigationbar.CarNavigationBarController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -148,7 +147,6 @@
             DisplayMetrics displayMetrics,
             MetricsLogger metricsLogger,
             @UiBackground Executor uiBgExecutor,
-            @Main Executor mainExecutor,
             NotificationMediaManager notificationMediaManager,
             NotificationLockscreenUserManager lockScreenUserManager,
             NotificationRemoteInputManager remoteInputManager,
@@ -228,7 +226,6 @@
                 displayMetrics,
                 metricsLogger,
                 uiBgExecutor,
-                mainExecutor,
                 notificationMediaManager,
                 lockScreenUserManager,
                 remoteInputManager,
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index af72888..1d4cfdc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -500,6 +500,20 @@
         }
     }
 
+    /**
+     * To generate and cache the label description.
+     *
+     * @param entry contain the entries of an app
+     */
+    public void ensureLabelDescription(AppEntry entry) {
+        if (entry.labelDescription != null) {
+            return;
+        }
+        synchronized (entry) {
+            entry.ensureLabelDescriptionLocked(mContext);
+        }
+    }
+
     public void requestSize(String packageName, int userId) {
         if (DEBUG_LOCKING) Log.v(TAG, "requestSize about to acquire lock...");
         synchronized (mEntriesMap) {
@@ -1524,6 +1538,7 @@
         public long size;
         public long internalSize;
         public long externalSize;
+        public String labelDescription;
 
         public boolean mounted;
 
@@ -1616,6 +1631,24 @@
                 return "";
             }
         }
+
+        /**
+         * Get the label description which distinguishes a personal app from a work app for
+         * accessibility purpose. If the app is in a work profile, then add a "work" prefix to the
+         * app label.
+         *
+         * @param context The application context
+         */
+        public void ensureLabelDescriptionLocked(Context context) {
+            final int userId = UserHandle.getUserId(this.info.uid);
+            if (UserManager.get(context).isManagedProfile(userId)) {
+                this.labelDescription = context.getString(
+                        com.android.settingslib.R.string.accessibility_work_profile_app_description,
+                        this.label);
+            } else {
+                this.labelDescription = this.label;
+            }
+        }
     }
 
     private static boolean hasFlag(int flags, int flag) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 959c42fe..b83a9c4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -219,6 +219,33 @@
     }
 
     /**
+     * Get the MediaDevice list that can be removed from current media session.
+     *
+     * @return list of MediaDevice
+     */
+    List<MediaDevice> getDeselectableMediaDevice() {
+        final List<MediaDevice> deviceList = new ArrayList<>();
+        if (TextUtils.isEmpty(mPackageName)) {
+            Log.d(TAG, "getDeselectableMediaDevice() package name is null or empty!");
+            return deviceList;
+        }
+
+        final RoutingSessionInfo info = getRoutingSessionInfo();
+        if (info != null) {
+            for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) {
+                deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
+                        route, mPackageName));
+                Log.d(TAG, route.getName() + " is deselectable for " + mPackageName);
+            }
+            return deviceList;
+        }
+        Log.d(TAG, "getDeselectableMediaDevice() cannot found deselectable MediaDevice from : "
+                + mPackageName);
+
+        return deviceList;
+    }
+
+    /**
      * Get the MediaDevice list that has been selected to current media.
      *
      * @return list of MediaDevice
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index f34903c..e89f628 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -308,6 +308,15 @@
     }
 
     /**
+     * Get the MediaDevice list that can be removed from current media session.
+     *
+     * @return list of MediaDevice
+     */
+    public List<MediaDevice> getDeselectableMediaDevice() {
+        return mInfoMediaManager.getDeselectableMediaDevice();
+    }
+
+    /**
      * Release session to stop playing media on MediaDevice.
      */
     public boolean releaseSession() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
index 9dc454f..d5f1ece 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
@@ -22,6 +22,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.Rect;
@@ -40,18 +41,31 @@
  * also includes shadows, which are only appropriate on top of wallpaper, not embedded in UI.
  */
 public class ConversationIconFactory extends BaseIconFactory {
-    // Geometry of the various parts of the design. All values are 1dp on a 48x48dp icon grid.
+    // Geometry of the various parts of the design. All values are 1dp on a 56x56dp icon grid.
     // Space is left around the "head" (main avatar) for
     // ........
     // .HHHHHH.
     // .HHHrrrr
     // .HHHrBBr
     // ....rrrr
+    // This is trying to recreate the view layout in notification_template_material_conversation.xml
 
-    private static final float BASE_ICON_SIZE = 48f;
-    private static final float RING_STROKE_WIDTH = 2f;
-    private static final float HEAD_SIZE = BASE_ICON_SIZE - RING_STROKE_WIDTH * 2 - 2; // 40
-    private static final float BADGE_SIZE = HEAD_SIZE * 0.4f; // 16
+    private static final float HEAD_SIZE = 52f;
+    private static final float BADGE_SIZE = 12f;
+    private static final float BADGE_CENTER = 46f;
+    private static final float CIRCLE_MARGIN = 36f;
+    private static final float BADGE_ORIGIN = HEAD_SIZE - BADGE_SIZE; // 40f
+    private static final float BASE_ICON_SIZE = 56f;
+
+    private static final float OUT_CIRCLE_DIA = (BASE_ICON_SIZE - CIRCLE_MARGIN); // 20f
+    private static final float INN_CIRCLE_DIA = (float) Math.sqrt(2 * BADGE_SIZE * BADGE_SIZE) ;
+    private static final float OUT_CIRCLE_RAD = OUT_CIRCLE_DIA / 2;
+    private static final float INN_CIRCLE_RAD = INN_CIRCLE_DIA / 2;
+    // Android draws strokes centered on the radius, so our actual radius is an avg of the outside
+    // and inside of the ring stroke
+    private static final float CIRCLE_RADIUS =
+            INN_CIRCLE_RAD + ((OUT_CIRCLE_RAD - INN_CIRCLE_RAD) / 2);
+    private static final float RING_STROKE_WIDTH = (OUT_CIRCLE_DIA - INN_CIRCLE_DIA) / 2;
 
     final LauncherApps mLauncherApps;
     final PackageManager mPackageManager;
@@ -125,6 +139,7 @@
         private int mIconSize;
         private Paint mRingPaint;
         private boolean mShowRing;
+        private Paint mPaddingPaint;
 
         public ConversationIconDrawable(Drawable baseIcon,
                 Drawable badgeIcon,
@@ -138,6 +153,9 @@
             mRingPaint = new Paint();
             mRingPaint.setStyle(Paint.Style.STROKE);
             mRingPaint.setColor(ringColor);
+            mPaddingPaint = new Paint();
+            mPaddingPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+            mPaddingPaint.setColor(Color.WHITE);
         }
 
         /**
@@ -165,40 +183,38 @@
         public void draw(Canvas canvas) {
             final Rect bounds = getBounds();
 
-            // scale to our internal 48x48 grid
+            // scale to our internal grid
             final float scale = bounds.width() / BASE_ICON_SIZE;
-            final int centerX = bounds.centerX();
-            final int centerY = bounds.centerX();
             final int ringStrokeWidth = (int) (RING_STROKE_WIDTH * scale);
             final int headSize = (int) (HEAD_SIZE * scale);
-            final int badgeSize = (int) (BADGE_SIZE * scale);
+            final int badgePadding = (int) (BADGE_ORIGIN * scale);
+            final int badgeCenter = (int) (BADGE_CENTER * scale);
 
+            mPaddingPaint.setStrokeWidth(ringStrokeWidth);
+            final float radius = (int) (CIRCLE_RADIUS * scale); // stroke outside
             if (mBaseIcon != null) {
-                mBaseIcon.setBounds(
-                        centerX - headSize / 2,
-                        centerY - headSize / 2,
-                        centerX + headSize / 2,
-                        centerY + headSize / 2);
+                mBaseIcon.setBounds(0,
+                        0,
+                        headSize ,
+                        headSize);
                 mBaseIcon.draw(canvas);
             } else {
                 Log.w("ConversationIconFactory", "ConversationIconDrawable has null base icon");
             }
             if (mBadgeIcon != null) {
+                canvas.drawCircle(badgeCenter, badgeCenter, radius, mPaddingPaint);
                 mBadgeIcon.setBounds(
-                        bounds.right - badgeSize - ringStrokeWidth,
-                        bounds.bottom - badgeSize - ringStrokeWidth,
-                        bounds.right - ringStrokeWidth,
-                        bounds.bottom - ringStrokeWidth);
+                        badgePadding,
+                        badgePadding,
+                        headSize,
+                        headSize);
                 mBadgeIcon.draw(canvas);
             } else {
                 Log.w("ConversationIconFactory", "ConversationIconDrawable has null badge icon");
             }
             if (mShowRing) {
                 mRingPaint.setStrokeWidth(ringStrokeWidth);
-                final float radius = badgeSize * 0.5f + ringStrokeWidth * 0.5f; // stroke outside
-                final float cx = bounds.right - badgeSize * 0.5f - ringStrokeWidth;
-                final float cy = bounds.bottom - badgeSize * 0.5f - ringStrokeWidth;
-                canvas.drawCircle(cx, cy, radius, mRingPaint);
+                canvas.drawCircle(badgeCenter, badgeCenter, radius, mRingPaint);
             }
         }
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index c514671..248eb5b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -416,6 +416,31 @@
     }
 
     @Test
+    public void getDeselectableMediaDevice_packageNameIsNull_returnFalse() {
+        mInfoMediaManager.mPackageName = null;
+
+        assertThat(mInfoMediaManager.getDeselectableMediaDevice()).isEmpty();
+    }
+
+    @Test
+    public void getDeselectableMediaDevice_checkList() {
+        final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
+        final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
+        routingSessionInfos.add(info);
+        final List<MediaRoute2Info> mediaRoute2Infos = new ArrayList<>();
+        final MediaRoute2Info mediaRoute2Info = mock(MediaRoute2Info.class);
+        mediaRoute2Infos.add(mediaRoute2Info);
+        mShadowRouter2Manager.setRoutingSessions(routingSessionInfos);
+        mShadowRouter2Manager.setDeselectableRoutes(mediaRoute2Infos);
+        when(mediaRoute2Info.getName()).thenReturn(TEST_NAME);
+
+        final List<MediaDevice> mediaDevices = mInfoMediaManager.getDeselectableMediaDevice();
+
+        assertThat(mediaDevices.size()).isEqualTo(1);
+        assertThat(mediaDevices.get(0).getName()).isEqualTo(TEST_NAME);
+    }
+
+    @Test
     public void adjustSessionVolume_routingSessionInfoIsNull_noCrash() {
         mInfoMediaManager.adjustSessionVolume(null, 10);
     }
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
index db0cb06..5bb5500 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
@@ -15,6 +15,7 @@
  */
 package com.android.settingslib.testutils.shadow;
 
+import android.annotation.NonNull;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
 import android.media.RoutingSessionInfo;
@@ -31,6 +32,7 @@
 
     private List<MediaRoute2Info> mAvailableRoutes;
     private List<MediaRoute2Info> mAllRoutes;
+    private List<MediaRoute2Info> mDeselectableRoutes;
     private List<RoutingSessionInfo> mActiveSessions;
     private List<RoutingSessionInfo> mRoutingSessions;
 
@@ -70,6 +72,15 @@
         mRoutingSessions = infos;
     }
 
+    @Implementation
+    public List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo sessionInfo) {
+        return mDeselectableRoutes;
+    }
+
+    public void setDeselectableRoutes(List<MediaRoute2Info> routes) {
+        mDeselectableRoutes = routes;
+    }
+
     public static ShadowRouter2Manager getShadow() {
         return (ShadowRouter2Manager) Shadow.extract(
                 MediaRouter2Manager.getInstance(RuntimeEnvironment.application));
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index b85c771..a0130f8 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -300,6 +300,9 @@
     <!-- Permissions needed to test shared libraries -->
     <uses-permission android:name="android.permission.ACCESS_SHARED_LIBRARIES" />
 
+    <!-- Permissions required for CTS test - TVInputManagerTest -->
+    <uses-permission android:name="android.permission.TV_INPUT_HARDWARE" />
+
     <application android:label="@string/app_label"
                 android:theme="@android:style/Theme.DeviceDefault.DayNight"
                 android:defaultToDeviceProtectedStorage="true"
diff --git a/packages/SystemUI/res/layout/notification_conversation_info.xml b/packages/SystemUI/res/layout/notification_conversation_info.xml
index 9f0a9bf..3bd7e04 100644
--- a/packages/SystemUI/res/layout/notification_conversation_info.xml
+++ b/packages/SystemUI/res/layout/notification_conversation_info.xml
@@ -21,33 +21,33 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:focusable="true"
-    android:clipChildren="false"
+    android:clipChildren="true"
     android:clipToPadding="true"
     android:orientation="vertical"
     android:background="@color/notification_material_background_color"
-    android:paddingStart="@*android:dimen/notification_content_margin_start">
+    android:paddingStart="12dp">
 
     <!-- Package Info -->
     <LinearLayout
         android:id="@+id/header"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/notification_guts_conversation_header_height"
+        android:layout_height="wrap_content"
         android:gravity="center_vertical"
         android:clipChildren="false"
-        android:clipToPadding="false">
+        android:paddingTop="8dp"
+        android:clipToPadding="true">
         <ImageView
             android:id="@+id/conversation_icon"
             android:layout_width="@dimen/notification_guts_conversation_icon_size"
             android:layout_height="@dimen/notification_guts_conversation_icon_size"
-            android:layout_centerVertical="true"
+            android:layout_centerVertical="false"
             android:layout_alignParentStart="true"
-            android:layout_marginEnd="15dp" />
+            android:layout_marginEnd="12dp" />
         <LinearLayout
             android:id="@+id/names"
             android:layout_weight="1"
             android:layout_width="0dp"
             android:orientation="vertical"
-
             android:layout_height="wrap_content"
             android:minHeight="@dimen/notification_guts_conversation_icon_size"
             android:layout_centerVertical="true"
@@ -63,6 +63,8 @@
                     android:id="@+id/parent_channel_name"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
+                    android:ellipsize="end"
+                    android:textDirection="locale"
                     android:layout_weight="1"
                     style="@style/TextAppearance.NotificationImportanceChannel"/>
                 <TextView
diff --git a/packages/SystemUI/res/layout/notification_info.xml b/packages/SystemUI/res/layout/notification_info.xml
index e8e0133..af5a8f4 100644
--- a/packages/SystemUI/res/layout/notification_info.xml
+++ b/packages/SystemUI/res/layout/notification_info.xml
@@ -46,7 +46,6 @@
             android:layout_weight="1"
             android:layout_width="0dp"
             android:orientation="vertical"
-
             android:layout_height="wrap_content"
             android:minHeight="@dimen/notification_guts_conversation_icon_size"
             android:layout_centerVertical="true"
@@ -57,6 +56,7 @@
                 android:id="@+id/channel_name"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:textDirection="locale"
                 style="@style/TextAppearance.NotificationImportanceChannel"/>
             <LinearLayout
                 android:layout_width="match_parent"
@@ -84,6 +84,7 @@
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_weight="1"
+                    android:textDirection="locale"
                     style="@style/TextAppearance.NotificationImportanceChannelGroup"/>
             </LinearLayout>
             <TextView
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 07de701..7f1763d 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -221,7 +221,7 @@
     <dimen name="notification_guts_button_horizontal_spacing">8dp</dimen>
 
     <dimen name="notification_guts_conversation_header_height">84dp</dimen>
-    <dimen name="notification_guts_conversation_icon_size">52dp</dimen>
+    <dimen name="notification_guts_conversation_icon_size">56dp</dimen>
     <dimen name="notification_guts_conversation_action_height">56dp</dimen>
     <dimen name="notification_guts_conversation_action_text_padding_start">32dp</dimen>
 
@@ -907,6 +907,8 @@
     <dimen name="screen_pinning_nav_highlight_size">56dp</dimen>
     <!-- Screen pinning inner nav bar outer circle size -->
     <dimen name="screen_pinning_nav_highlight_outer_size">84dp</dimen>
+    <!-- Screen pinning description bullet gap width -->
+    <dimen name="screen_pinning_description_bullet_gap_width">6sp</dimen>
 
     <!-- Padding to be used on the bottom of the fingerprint icon on Keyguard so it better aligns
          with the other icons. -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f7f1ef6..21a2435 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1522,7 +1522,7 @@
     <string name="accessibility_output_chooser">Switch output device</string>
 
     <!-- Screen pinning dialog title. -->
-    <string name="screen_pinning_title">Screen is pinned</string>
+    <string name="screen_pinning_title">App is pinned</string>
     <!-- Screen pinning dialog description. -->
     <string name="screen_pinning_description">This keeps it in view until you unpin. Touch &amp; hold Back and Overview to unpin.</string>
     <string name="screen_pinning_description_recents_invisible">This keeps it in view until you unpin. Touch &amp; hold Back and Home to unpin.</string>
@@ -1530,20 +1530,24 @@
     <!-- Screen pinning dialog description. -->
     <string name="screen_pinning_description_accessible">This keeps it in view until you unpin. Touch &amp; hold Overview to unpin.</string>
     <string name="screen_pinning_description_recents_invisible_accessible">This keeps it in view until you unpin. Touch &amp; hold Home to unpin.</string>
+    <!-- Screen pinning security warning: personal data, email, contacts may be exposed while screen is pinned. [CHAR LIMIT=NONE] -->
+    <string name="screen_pinning_exposes_personal_data">Personal data may be accessible (such as contacts and email content).</string>
+    <!-- Screen pinning security warning: a pinned app can still launch other apps. [CHAR LIMIT=NONE] -->
+    <string name="screen_pinning_can_open_other_apps">Pinned app may open other apps.</string>
     <!-- Notify use that they are in Lock-to-app -->
-    <string name="screen_pinning_toast">To unpin this screen, touch &amp; hold Back and Overview
+    <string name="screen_pinning_toast">To unpin this app, touch &amp; hold Back and Overview
         buttons</string>
-    <string name="screen_pinning_toast_recents_invisible">To unpin this screen, touch &amp; hold Back
+    <string name="screen_pinning_toast_recents_invisible">To unpin this app, touch &amp; hold Back
         and Home buttons</string>
     <!-- Notify (in toast) user how to unpin screen in gesture navigation mode [CHAR LIMIT=NONE] -->
-    <string name="screen_pinning_toast_gesture_nav">To unpin this screen, swipe up &amp; hold</string>
+    <string name="screen_pinning_toast_gesture_nav">To unpin this app, swipe up &amp; hold</string>
     <!-- Screen pinning positive response. -->
     <string name="screen_pinning_positive">Got it</string>
     <!-- Screen pinning negative response. -->
     <string name="screen_pinning_negative">No thanks</string>
     <!-- Enter/Exiting screen pinning indication. -->
-    <string name="screen_pinning_start">Screen pinned</string>
-    <string name="screen_pinning_exit">Screen unpinned</string>
+    <string name="screen_pinning_start">App pinned</string>
+    <string name="screen_pinning_exit">App unpinned</string>
 
 
     <!-- Hide quick settings tile confirmation title -->
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index ecd8b45..15eda06 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -15,7 +15,7 @@
  */
 package com.android.systemui.bubbles;
 
-
+import static android.app.Notification.FLAG_BUBBLE;
 import static android.os.AsyncTask.Status.FINISHED;
 import static android.view.Display.INVALID_DISPLAY;
 
@@ -27,6 +27,7 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
@@ -55,6 +56,11 @@
 class Bubble implements BubbleViewProvider {
     private static final String TAG = "Bubble";
 
+    /**
+     * NotificationEntry associated with the bubble. A null value implies this bubble is loaded
+     * from disk.
+     */
+    @Nullable
     private NotificationEntry mEntry;
     private final String mKey;
 
@@ -96,11 +102,18 @@
     private Bitmap mBadgedImage;
     private int mDotColor;
     private Path mDotPath;
+    private int mFlags;
 
-    // TODO: Decouple Bubble from NotificationEntry and transform ShortcutInfo into Bubble
-    Bubble(ShortcutInfo shortcutInfo) {
+    /**
+     * Create a bubble with limited information based on given {@link ShortcutInfo}.
+     * Note: Currently this is only being used when the bubble is persisted to disk.
+     */
+    Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo) {
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(shortcutInfo);
         mShortcutInfo = shortcutInfo;
-        mKey = shortcutInfo.getId();
+        mKey = key;
+        mFlags = 0;
     }
 
     /** Used in tests when no UI is required. */
@@ -111,6 +124,7 @@
         mKey = e.getKey();
         mLastUpdated = e.getSbn().getPostTime();
         mSuppressionListener = listener;
+        mFlags = e.getSbn().getNotification().flags;
     }
 
     @Override
@@ -118,12 +132,22 @@
         return mKey;
     }
 
+    @Nullable
     public NotificationEntry getEntry() {
         return mEntry;
     }
 
+    @Nullable
+    public UserHandle getUser() {
+        if (mEntry != null) return mEntry.getSbn().getUser();
+        if (mShortcutInfo != null) return mShortcutInfo.getUserHandle();
+        return null;
+    }
+
     public String getPackageName() {
-        return mEntry.getSbn().getPackageName();
+        return mEntry == null
+                ? mShortcutInfo == null ? null : mShortcutInfo.getPackage()
+                : mEntry.getSbn().getPackageName();
     }
 
     @Override
@@ -167,6 +191,18 @@
         return mExpandedView;
     }
 
+    @Nullable
+    public String getTitle() {
+        final CharSequence titleCharSeq;
+        if (mEntry == null) {
+            titleCharSeq = null;
+        } else {
+            titleCharSeq = mEntry.getSbn().getNotification().extras.getCharSequence(
+                    Notification.EXTRA_TITLE);
+        }
+        return titleCharSeq != null ? titleCharSeq.toString() : null;
+    }
+
     /**
      * Call when the views should be removed, ensure this is called to clean up ActivityView
      * content.
@@ -207,7 +243,8 @@
     void inflate(BubbleViewInfoTask.Callback callback,
             Context context,
             BubbleStackView stackView,
-            BubbleIconFactory iconFactory) {
+            BubbleIconFactory iconFactory,
+            boolean skipInflation) {
         if (isBubbleLoading()) {
             mInflationTask.cancel(true /* mayInterruptIfRunning */);
         }
@@ -215,6 +252,7 @@
                 context,
                 stackView,
                 iconFactory,
+                skipInflation,
                 callback);
         if (mInflateSynchronously) {
             mInflationTask.onPostExecute(mInflationTask.doInBackground());
@@ -327,6 +365,7 @@
      * Whether this notification should be shown in the shade.
      */
     boolean showInShade() {
+        if (mEntry == null) return false;
         return !shouldSuppressNotification() || !mEntry.isClearable();
     }
 
@@ -334,8 +373,8 @@
      * Sets whether this notification should be suppressed in the shade.
      */
     void setSuppressNotification(boolean suppressNotification) {
+        if (mEntry == null) return;
         boolean prevShowInShade = showInShade();
-
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         int flags = data.getFlags();
         if (suppressNotification) {
@@ -366,6 +405,7 @@
      */
     @Override
     public boolean showDot() {
+        if (mEntry == null) return false;
         return mShowBubbleUpdateDot
                 && !mEntry.shouldSuppressNotificationDot()
                 && !shouldSuppressNotification();
@@ -375,6 +415,7 @@
      * Whether the flyout for the bubble should be shown.
      */
     boolean showFlyout() {
+        if (mEntry == null) return false;
         return !mSuppressFlyout && !mEntry.shouldSuppressPeek()
                 && !shouldSuppressNotification()
                 && !mEntry.shouldSuppressNotificationList();
@@ -394,6 +435,7 @@
     }
 
     float getDesiredHeight(Context context) {
+        if (mEntry == null) return 0;
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         boolean useRes = data.getDesiredHeightResId() != 0;
         if (useRes) {
@@ -407,6 +449,7 @@
     }
 
     String getDesiredHeightString() {
+        if (mEntry == null) return String.valueOf(0);
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         boolean useRes = data.getDesiredHeightResId() != 0;
         if (useRes) {
@@ -423,11 +466,13 @@
      * To populate the icon use {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)}.
      */
     boolean usingShortcutInfo() {
-        return mEntry.getBubbleMetadata().getShortcutId() != null;
+        return mEntry != null && mEntry.getBubbleMetadata().getShortcutId() != null
+                || mShortcutInfo != null;
     }
 
     @Nullable
     PendingIntent getBubbleIntent() {
+        if (mEntry == null) return null;
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         if (data != null) {
             return data.getIntent();
@@ -435,16 +480,32 @@
         return null;
     }
 
-    Intent getSettingsIntent() {
+    Intent getSettingsIntent(final Context context) {
         final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
         intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
-        intent.putExtra(Settings.EXTRA_APP_UID, mEntry.getSbn().getUid());
+        final int uid = getUid(context);
+        if (uid != -1) {
+            intent.putExtra(Settings.EXTRA_APP_UID, uid);
+        }
         intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
         return intent;
     }
 
+    private int getUid(final Context context) {
+        if (mEntry != null) return mEntry.getSbn().getUid();
+        final PackageManager pm = context.getPackageManager();
+        if (pm == null) return -1;
+        try {
+            final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0);
+            return info.uid;
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "cannot find uid", e);
+        }
+        return -1;
+    }
+
     private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) {
         PackageManager pm = context.getPackageManager();
         Resources r;
@@ -466,11 +527,13 @@
     }
 
     private boolean shouldSuppressNotification() {
+        if (mEntry == null) return false;
         return mEntry.getBubbleMetadata() != null
                 && mEntry.getBubbleMetadata().isNotificationSuppressed();
     }
 
     boolean shouldAutoExpand() {
+        if (mEntry == null) return false;
         Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata();
         return (metadata != null && metadata.getAutoExpandBubble()) ||  mShouldAutoExpand;
     }
@@ -479,6 +542,19 @@
         mShouldAutoExpand = shouldAutoExpand;
     }
 
+    public boolean isBubble() {
+        if (mEntry == null) return (mFlags & FLAG_BUBBLE) != 0;
+        return (mEntry.getSbn().getNotification().flags & FLAG_BUBBLE) != 0;
+    }
+
+    public void enable(int option) {
+        mFlags |= option;
+    }
+
+    public void disable(int option) {
+        mFlags &= ~option;
+    }
+
     @Override
     public String toString() {
         return "Bubble{" + mKey + '}';
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 5f157c1..d447596 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -42,6 +42,7 @@
 import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
+import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.INotificationManager;
@@ -113,6 +114,7 @@
 import java.lang.annotation.Target;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
@@ -243,13 +245,13 @@
          * This can happen when an app cancels a bubbled notification or when the user dismisses a
          * bubble.
          */
-        void removeNotification(NotificationEntry entry, int reason);
+        void removeNotification(@NonNull NotificationEntry entry, int reason);
 
         /**
          * Called when a bubbled notification has changed whether it should be
          * filtered from the shade.
          */
-        void invalidateNotifications(String reason);
+        void invalidateNotifications(@NonNull String reason);
 
         /**
          * Called on a bubbled entry that has been removed when there are no longer
@@ -259,7 +261,7 @@
          * removes all remnants of the group's summary from the notification pipeline.
          * TODO: (b/145659174) Only old pipeline needs this - delete post-migration.
          */
-        void maybeCancelSummary(NotificationEntry entry);
+        void maybeCancelSummary(@NonNull NotificationEntry entry);
     }
 
     /**
@@ -755,10 +757,12 @@
         mBubbleIconFactory = new BubbleIconFactory(mContext);
         // Reload each bubble
         for (Bubble b: mBubbleData.getBubbles()) {
-            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory);
+            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory,
+                    false /* skipInflation */);
         }
         for (Bubble b: mBubbleData.getOverflowBubbles()) {
-            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory);
+            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory,
+                    false /* skipInflation */);
         }
     }
 
@@ -845,7 +849,7 @@
 
     void promoteBubbleFromOverflow(Bubble bubble) {
         bubble.setInflateSynchronously(mInflateSynchronously);
-        setIsBubble(bubble.getEntry(), /* isBubble */ true);
+        setIsBubble(bubble, /* isBubble */ true);
         mBubbleData.promoteBubbleFromOverflow(bubble, mStackView, mBubbleIconFactory);
     }
 
@@ -895,10 +899,30 @@
         updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
     }
 
+    /**
+     * Fills the overflow bubbles by loading them from disk.
+     */
+    void loadOverflowBubblesFromDisk() {
+        if (!mBubbleData.getOverflowBubbles().isEmpty()) {
+            // we don't need to load overflow bubbles from disk if it is already in memory
+            return;
+        }
+        mDataRepository.loadBubbles((bubbles) -> {
+            bubbles.forEach(bubble -> {
+                if (mBubbleData.getBubbles().contains(bubble)) {
+                    // if the bubble is already active, there's no need to push it to overflow
+                    return;
+                }
+                bubble.inflate((b) -> mBubbleData.overflowBubble(DISMISS_AGED, bubble),
+                        mContext, mStackView, mBubbleIconFactory, true /* skipInflation */);
+            });
+            return null;
+        });
+    }
+
     void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
         // Lazy init stack view when a bubble is created
         ensureStackViewCreated();
-
         // If this is an interruptive notif, mark that it's interrupted
         if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
             notif.setInterruption();
@@ -918,11 +942,11 @@
                             return;
                         }
                         mHandler.post(
-                                () -> removeBubble(bubble.getEntry(),
+                                () -> removeBubble(bubble.getKey(),
                                         BubbleController.DISMISS_INVALID_INTENT));
                     });
                 },
-                mContext, mStackView, mBubbleIconFactory);
+                mContext, mStackView, mBubbleIconFactory, false /* skipInflation */);
     }
 
     /**
@@ -934,7 +958,10 @@
      * @param entry the notification to change bubble state for.
      * @param shouldBubble whether the notification should show as a bubble or not.
      */
-    public void onUserChangedBubble(NotificationEntry entry, boolean shouldBubble) {
+    public void onUserChangedBubble(@Nullable final NotificationEntry entry, boolean shouldBubble) {
+        if (entry == null) {
+            return;
+        }
         NotificationChannel channel = entry.getChannel();
         final String appPkg = entry.getSbn().getPackageName();
         final int appUid = entry.getSbn().getUid();
@@ -973,14 +1000,14 @@
     }
 
     /**
-     * Removes the bubble with the given NotificationEntry.
+     * Removes the bubble with the given key.
      * <p>
      * Must be called from the main thread.
      */
     @MainThread
-    void removeBubble(NotificationEntry entry, int reason) {
-        if (mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
-            mBubbleData.notificationEntryRemoved(entry, reason);
+    void removeBubble(String key, int reason) {
+        if (mBubbleData.hasAnyBubbleWithKey(key)) {
+            mBubbleData.notificationEntryRemoved(key, reason);
         }
     }
 
@@ -998,7 +1025,7 @@
                 && canLaunchInActivityView(mContext, entry);
         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
             // It was previously a bubble but no longer a bubble -- lets remove it
-            removeBubble(entry, DISMISS_NO_LONGER_BUBBLE);
+            removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
         } else if (shouldBubble && entry.isBubble()) {
             updateBubble(entry);
         }
@@ -1012,10 +1039,10 @@
             // Remove any associated bubble children with the summary
             final List<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey);
             for (int i = 0; i < bubbleChildren.size(); i++) {
-                removeBubble(bubbleChildren.get(i).getEntry(), DISMISS_GROUP_CANCELLED);
+                removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED);
             }
         } else {
-            removeBubble(entry, DISMISS_NOTIF_CANCEL);
+            removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL);
         }
     }
 
@@ -1037,7 +1064,8 @@
             rankingMap.getRanking(key, mTmpRanking);
             boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key);
             if (isActiveBubble && !mTmpRanking.canBubble()) {
-                mBubbleData.notificationEntryRemoved(entry, BubbleController.DISMISS_BLOCKED);
+                mBubbleData.notificationEntryRemoved(entry.getKey(),
+                        BubbleController.DISMISS_BLOCKED);
             } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) {
                 entry.setFlagBubble(true);
                 onEntryUpdated(entry);
@@ -1045,7 +1073,8 @@
         }
     }
 
-    private void setIsBubble(NotificationEntry entry, boolean isBubble) {
+    private void setIsBubble(@NonNull final NotificationEntry entry, final boolean isBubble) {
+        Objects.requireNonNull(entry);
         if (isBubble) {
             entry.getSbn().getNotification().flags |= FLAG_BUBBLE;
         } else {
@@ -1058,11 +1087,31 @@
         }
     }
 
+    private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) {
+        Objects.requireNonNull(b);
+        if (isBubble) {
+            b.enable(FLAG_BUBBLE);
+        } else {
+            b.disable(FLAG_BUBBLE);
+        }
+        if (b.getEntry() != null) {
+            setIsBubble(b.getEntry(), isBubble);
+        } else {
+            try {
+                mBarService.onNotificationBubbleChanged(b.getKey(), isBubble, 0);
+            } catch (RemoteException e) {
+                // Bad things have happened
+            }
+        }
+    }
+
     @SuppressWarnings("FieldCanBeLocal")
     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
 
         @Override
         public void applyUpdate(BubbleData.Update update) {
+            // Lazy load overflow bubbles from disk
+            loadOverflowBubblesFromDisk();
             // Update bubbles in overflow.
             if (mOverflowCallback != null) {
                 mOverflowCallback.run();
@@ -1097,23 +1146,27 @@
                         // The bubble is now gone & the notification is hidden from the shade, so
                         // time to actually remove it
                         for (NotifCallback cb : mCallbacks) {
-                            cb.removeNotification(bubble.getEntry(), REASON_CANCEL);
+                            if (bubble.getEntry() != null) {
+                                cb.removeNotification(bubble.getEntry(), REASON_CANCEL);
+                            }
                         }
                     } else {
-                        if (bubble.getEntry().isBubble() && bubble.showInShade()) {
-                            setIsBubble(bubble.getEntry(), false /* isBubble */);
+                        if (bubble.isBubble() && bubble.showInShade()) {
+                            setIsBubble(bubble, false /* isBubble */);
                         }
-                        if (bubble.getEntry().getRow() != null) {
+                        if (bubble.getEntry() != null && bubble.getEntry().getRow() != null) {
                             bubble.getEntry().getRow().updateBubbleButton();
                         }
                     }
 
                 }
-                final String groupKey = bubble.getEntry().getSbn().getGroupKey();
-                if (mBubbleData.getBubblesInGroup(groupKey).isEmpty()) {
-                    // Time to potentially remove the summary
-                    for (NotifCallback cb : mCallbacks) {
-                        cb.maybeCancelSummary(bubble.getEntry());
+                if (bubble.getEntry() != null) {
+                    final String groupKey = bubble.getEntry().getSbn().getGroupKey();
+                    if (mBubbleData.getBubblesInGroup(groupKey).isEmpty()) {
+                        // Time to potentially remove the summary
+                        for (NotifCallback cb : mCallbacks) {
+                            cb.maybeCancelSummary(bubble.getEntry());
+                        }
                     }
                 }
             }
@@ -1138,7 +1191,7 @@
 
             if (update.selectionChanged) {
                 mStackView.setSelectedBubble(update.selectedBubble);
-                if (update.selectedBubble != null) {
+                if (update.selectedBubble != null && update.selectedBubble.getEntry() != null) {
                     mNotificationGroupManager.updateSuppression(
                             update.selectedBubble.getEntry());
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 35647b0..857f1dd 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -21,6 +21,7 @@
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 
+import android.annotation.NonNull;
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -214,7 +215,7 @@
                     notificationEntryUpdated(bubble, false /* suppressFlyout */,
                             true /* showInShade */);
                 },
-                mContext, stack, factory);
+                mContext, stack, factory, false /* skipInflation */);
     }
 
     void setShowingOverflow(boolean showingOverflow) {
@@ -268,7 +269,8 @@
         }
         mPendingBubbles.remove(bubble); // No longer pending once we're here
         Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
-        suppressFlyout |= !bubble.getEntry().getRanking().visuallyInterruptive();
+        suppressFlyout |= bubble.getEntry() == null
+                || !bubble.getEntry().getRanking().visuallyInterruptive();
 
         if (prevBubble == null) {
             // Create a new bubble
@@ -297,11 +299,14 @@
         dispatchPendingChanges();
     }
 
-    public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
+    /**
+     * Called when a notification associated with a bubble is removed.
+     */
+    public void notificationEntryRemoved(String key, @DismissReason int reason) {
         if (DEBUG_BUBBLE_DATA) {
-            Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason);
+            Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason);
         }
-        doRemove(entry.getKey(), reason);
+        doRemove(key, reason);
         dispatchPendingChanges();
     }
 
@@ -349,7 +354,7 @@
             return bubbleChildren;
         }
         for (Bubble b : mBubbles) {
-            if (groupKey.equals(b.getEntry().getSbn().getGroupKey())) {
+            if (b.getEntry() != null && groupKey.equals(b.getEntry().getSbn().getGroupKey())) {
                 bubbleChildren.add(b);
             }
         }
@@ -447,7 +452,9 @@
             Bubble newSelected = mBubbles.get(newIndex);
             setSelectedBubbleInternal(newSelected);
         }
-        maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
+        if (bubbleToRemove.getEntry() != null) {
+            maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
+        }
     }
 
     void overflowBubble(@DismissReason int reason, Bubble bubble) {
@@ -615,7 +622,8 @@
         return true;
     }
 
-    private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) {
+    private void maybeSendDeleteIntent(@DismissReason int reason,
+            @NonNull final NotificationEntry entry) {
         if (reason == BubbleController.DISMISS_USER_GESTURE) {
             Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
             PendingIntent deleteIntent = bubbleMetadata != null
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
index ba93f41..1c5e98b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
@@ -74,8 +74,10 @@
 
     private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleEntity> {
         return bubbles.mapNotNull { b ->
-            val shortcutId = b.shortcutInfo?.id ?: return@mapNotNull null
-            BubbleEntity(userId, b.packageName, shortcutId)
+            var shortcutId = b.shortcutInfo?.id
+            if (shortcutId == null) shortcutId = b.entry?.bubbleMetadata?.shortcutId
+            if (shortcutId == null) return@mapNotNull null
+            BubbleEntity(userId, b.packageName, shortcutId, b.key)
         }
     }
 
@@ -108,7 +110,6 @@
     /**
      * Load bubbles from disk.
      */
-    // TODO: call this method from BubbleController and update UI
     @SuppressLint("WrongConstant")
     fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch {
         /**
@@ -132,17 +133,17 @@
         val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet()
         /**
          * Retrieve shortcuts with given userId/packageName combination, then construct a mapping
-         * between BubbleEntity and ShortcutInfo.
+         * from the userId/packageName pair to a list of associated ShortcutInfo.
          * e.g.
          * {
-         *     BubbleEntity(0, "com.example.messenger", "id-0") ->
+         *     ShortcutKey(0, "com.example.messenger") -> [
          *         ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"),
-         *     BubbleEntity(0, "com.example.messenger", "id-2") ->
-         *         ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2"),
-         *     BubbleEntity(10, "com.example.chat", "id-1") ->
+         *         ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2")
+         *     ]
+         *     ShortcutKey(10, "com.example.chat") -> [
          *         ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"),
-         *     BubbleEntity(10, "com.example.chat", "id-3") ->
          *         ShortcutInfo(userId=10, pkg="com.example.chat", id="id-3")
+         *     ]
          * }
          */
         val shortcutMap = shortcutKeys.flatMap { key ->
@@ -150,11 +151,15 @@
                     LauncherApps.ShortcutQuery()
                             .setPackage(key.pkg)
                             .setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId))
-                    ?.map { BubbleEntity(key.userId, key.pkg, it.id) to it } ?: emptyList()
-        }.toMap()
+                    ?: emptyList()
+        }.groupBy { ShortcutKey(it.userId, it.`package`) }
         // For each entity loaded from xml, find the corresponding ShortcutInfo then convert them
         // into Bubble.
-        val bubbles = entities.mapNotNull { entity -> shortcutMap[entity]?.let { Bubble(it) } }
+        val bubbles = entities.mapNotNull { entity ->
+            shortcutMap[ShortcutKey(entity.userId, entity.packageName)]
+                    ?.first { shortcutInfo -> entity.shortcutId == shortcutInfo.id }
+                    ?.let { shortcutInfo -> Bubble(entity.key, shortcutInfo) }
+        }
         uiScope.launch { cb(bubbles) }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index c4b4f43..494a51d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -65,7 +65,6 @@
 import com.android.systemui.R;
 import com.android.systemui.recents.TriangleShape;
 import com.android.systemui.statusbar.AlphaOptimizedButton;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
 /**
  * Container for the expanded bubble view, handles rendering the caret and settings icon.
@@ -161,7 +160,7 @@
                             // the bubble again so we'll just remove it.
                             Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
                                     + ", " + e.getMessage() + "; removing bubble");
-                            mBubbleController.removeBubble(getBubbleEntry(),
+                            mBubbleController.removeBubble(getBubbleKey(),
                                     BubbleController.DISMISS_INVALID_INTENT);
                         }
                     });
@@ -205,7 +204,7 @@
             }
             if (mBubble != null) {
                 // Must post because this is called from a binder thread.
-                post(() -> mBubbleController.removeBubble(mBubble.getEntry(),
+                post(() -> mBubbleController.removeBubble(mBubble.getKey(),
                         BubbleController.DISMISS_TASK_FINISHED));
             }
         }
@@ -297,10 +296,6 @@
         return mBubble != null ? mBubble.getKey() : "null";
     }
 
-    private NotificationEntry getBubbleEntry() {
-        return mBubble != null ? mBubble.getEntry() : null;
-    }
-
     void setManageClickListener(OnClickListener manageClickListener) {
         findViewById(R.id.settings_button).setOnClickListener(manageClickListener);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
index 8fec338..2109a7b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
@@ -21,7 +21,6 @@
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 
 import android.app.Activity;
-import android.app.Notification;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -260,12 +259,9 @@
             mPromoteBubbleFromOverflow.accept(b);
         });
 
-        final CharSequence titleCharSeq =
-                b.getEntry().getSbn().getNotification().extras.getCharSequence(
-                        Notification.EXTRA_TITLE);
-        String titleStr = mContext.getResources().getString(R.string.notification_bubble_title);
-        if (titleCharSeq != null) {
-            titleStr = titleCharSeq.toString();
+        String titleStr = b.getTitle();
+        if (titleStr == null) {
+            titleStr = mContext.getResources().getString(R.string.notification_bubble_title);
         }
         vh.iconView.setContentDescription(mContext.getResources().getString(
                 R.string.bubble_content_description_single, titleStr, b.getAppName()));
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 418cc50..6ba1aa8 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -31,7 +31,6 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.SuppressLint;
-import android.app.Notification;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -50,7 +49,6 @@
 import android.graphics.Region;
 import android.os.Bundle;
 import android.provider.Settings;
-import android.service.notification.StatusBarNotification;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.DisplayCutout;
@@ -941,10 +939,10 @@
                     showManageMenu(false /* show */);
                     final Bubble bubble = mBubbleData.getSelectedBubble();
                     if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
-                        final Intent intent = bubble.getSettingsIntent();
+                        final Intent intent = bubble.getSettingsIntent(mContext);
                         collapseStack(() -> {
-                            mContext.startActivityAsUser(
-                                    intent, bubble.getEntry().getSbn().getUser());
+
+                            mContext.startActivityAsUser(intent, bubble.getUser());
                             logBubbleClickEvent(
                                     bubble,
                                     SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
@@ -1202,13 +1200,10 @@
         for (int i = 0; i < mBubbleData.getBubbles().size(); i++) {
             final Bubble bubble = mBubbleData.getBubbles().get(i);
             final String appName = bubble.getAppName();
-            final Notification notification = bubble.getEntry().getSbn().getNotification();
-            final CharSequence titleCharSeq =
-                    notification.extras.getCharSequence(Notification.EXTRA_TITLE);
 
-            String titleStr = getResources().getString(R.string.notification_bubble_title);
-            if (titleCharSeq != null) {
-                titleStr = titleCharSeq.toString();
+            String titleStr = bubble.getTitle();
+            if (titleStr == null) {
+                titleStr = getResources().getString(R.string.notification_bubble_title);
             }
 
             if (bubble.getIconView() != null) {
@@ -1821,7 +1816,7 @@
     private void dismissBubbleIfExists(@Nullable Bubble bubble) {
         if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
             mBubbleData.notificationEntryRemoved(
-                    bubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                    bubble.getKey(), BubbleController.DISMISS_USER_GESTURE);
         }
     }
 
@@ -2319,18 +2314,12 @@
      * @param action the user interaction enum.
      */
     private void logBubbleClickEvent(Bubble bubble, int action) {
-        StatusBarNotification notification = bubble.getEntry().getSbn();
-        SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
-                notification.getPackageName(),
-                notification.getNotification().getChannelId(),
-                notification.getId(),
-                getBubbleIndex(getExpandedBubble()),
+        bubble.logUIEvent(
                 getBubbleCount(),
                 action,
                 getNormalizedXPosition(),
                 getNormalizedYPosition(),
-                bubble.showInShade(),
-                false /* isOngoing (unused) */,
-                false /* isAppForeground (unused) */);
+                getBubbleIndex(getExpandedBubble())
+        );
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
index 8a57a73..525d5b5 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
@@ -37,6 +37,7 @@
 import android.graphics.drawable.Icon;
 import android.os.AsyncTask;
 import android.os.Parcelable;
+import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
 import android.util.Log;
@@ -74,6 +75,7 @@
     private WeakReference<Context> mContext;
     private WeakReference<BubbleStackView> mStackView;
     private BubbleIconFactory mIconFactory;
+    private boolean mSkipInflation;
     private Callback mCallback;
 
     /**
@@ -84,17 +86,20 @@
             Context context,
             BubbleStackView stackView,
             BubbleIconFactory factory,
+            boolean skipInflation,
             Callback c) {
         mBubble = b;
         mContext = new WeakReference<>(context);
         mStackView = new WeakReference<>(stackView);
         mIconFactory = factory;
+        mSkipInflation = skipInflation;
         mCallback = c;
     }
 
     @Override
     protected BubbleViewInfo doInBackground(Void... voids) {
-        return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble);
+        return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble,
+                mSkipInflation);
     }
 
     @Override
@@ -123,11 +128,36 @@
 
         @Nullable
         static BubbleViewInfo populate(Context c, BubbleStackView stackView,
-                BubbleIconFactory iconFactory, Bubble b) {
+                BubbleIconFactory iconFactory, Bubble b, boolean skipInflation) {
+            final NotificationEntry entry = b.getEntry();
+            if (entry == null) {
+                // populate from ShortcutInfo when NotificationEntry is not available
+                final ShortcutInfo s = b.getShortcutInfo();
+                return populate(c, stackView, iconFactory, skipInflation || b.isInflated(),
+                        s.getPackage(), s.getUserHandle(), s, null);
+            }
+            final StatusBarNotification sbn = entry.getSbn();
+            final String bubbleShortcutId =  entry.getBubbleMetadata().getShortcutId();
+            final ShortcutInfo si = bubbleShortcutId == null
+                    ? null : entry.getRanking().getShortcutInfo();
+            return populate(
+                    c, stackView, iconFactory, skipInflation || b.isInflated(),
+                    sbn.getPackageName(), sbn.getUser(), si, entry);
+        }
+
+        private static BubbleViewInfo populate(
+                @NonNull final Context c,
+                @NonNull final BubbleStackView stackView,
+                @NonNull final BubbleIconFactory iconFactory,
+                final boolean isInflated,
+                @NonNull final String packageName,
+                @NonNull final UserHandle user,
+                @Nullable final ShortcutInfo shortcutInfo,
+                @Nullable final NotificationEntry entry) {
             BubbleViewInfo info = new BubbleViewInfo();
 
             // View inflation: only should do this once per bubble
-            if (!b.isInflated()) {
+            if (!isInflated) {
                 LayoutInflater inflater = LayoutInflater.from(c);
                 info.imageView = (BadgedImageView) inflater.inflate(
                         R.layout.bubble_view, stackView, false /* attachToRoot */);
@@ -137,12 +167,8 @@
                 info.expandedView.setStackView(stackView);
             }
 
-            StatusBarNotification sbn = b.getEntry().getSbn();
-            String packageName = sbn.getPackageName();
-
-            String bubbleShortcutId =  b.getEntry().getBubbleMetadata().getShortcutId();
-            if (bubbleShortcutId != null) {
-                info.shortcutInfo = b.getEntry().getRanking().getShortcutInfo();
+            if (shortcutInfo != null) {
+                info.shortcutInfo = shortcutInfo;
             }
 
             // App name & app icon
@@ -161,7 +187,7 @@
                     info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
                 }
                 appIcon = pm.getApplicationIcon(packageName);
-                badgedIcon = pm.getUserBadgedIcon(appIcon, sbn.getUser());
+                badgedIcon = pm.getUserBadgedIcon(appIcon, user);
             } catch (PackageManager.NameNotFoundException exception) {
                 // If we can't find package... don't think we should show the bubble.
                 Log.w(TAG, "Unable to find package: " + packageName);
@@ -170,7 +196,7 @@
 
             // Badged bubble image
             Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo,
-                    b.getEntry().getBubbleMetadata());
+                    entry == null ? null : entry.getBubbleMetadata());
             if (bubbleDrawable == null) {
                 // Default to app icon
                 bubbleDrawable = appIcon;
@@ -196,7 +222,9 @@
                     Color.WHITE, WHITE_SCRIM_ALPHA);
 
             // Flyout
-            info.flyoutMessage = extractFlyoutMessage(c, b.getEntry());
+            if (entry != null) {
+                info.flyoutMessage = extractFlyoutMessage(c, entry);
+            }
             return info;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
index 4690a8e..4348261 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
@@ -20,5 +20,6 @@
 data class BubbleEntity(
     @UserIdInt val userId: Int,
     val packageName: String,
-    val shortcutId: String
+    val shortcutId: String,
+    val key: String
 )
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
index 821b64c..1df9f72 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
@@ -30,6 +30,7 @@
 private const val ATTR_USER_ID = "uid"
 private const val ATTR_PACKAGE = "pkg"
 private const val ATTR_SHORTCUT_ID = "sid"
+private const val ATTR_KEY = "key"
 
 /**
  * Writes the bubbles in xml format into given output stream.
@@ -48,7 +49,7 @@
 /**
  * Creates a xml entry for given bubble in following format:
  * ```
- * <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" />
+ * <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" key="my-key" />
  * ```
  */
 private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleEntity) {
@@ -57,6 +58,7 @@
         serializer.attribute(null, ATTR_USER_ID, bubble.userId.toString())
         serializer.attribute(null, ATTR_PACKAGE, bubble.packageName)
         serializer.attribute(null, ATTR_SHORTCUT_ID, bubble.shortcutId)
+        serializer.attribute(null, ATTR_KEY, bubble.key)
         serializer.endTag(null, TAG_BUBBLE)
     } catch (e: IOException) {
         throw RuntimeException(e)
@@ -83,7 +85,8 @@
     return BubbleEntity(
             parser.getAttributeWithName(ATTR_USER_ID)?.toInt() ?: return null,
             parser.getAttributeWithName(ATTR_PACKAGE) ?: return null,
-            parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null
+            parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null,
+            parser.getAttributeWithName(ATTR_KEY) ?: return null
     )
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
new file mode 100644
index 0000000..9a5b960
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.controls.dagger
+
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.ControlsUiController
+import dagger.Lazy
+import java.util.Optional
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Pseudo-component to inject into classes outside `com.android.systemui.controls`.
+ *
+ * If `featureEnabled` is false, all the optionals should be empty. The controllers will only be
+ * instantiated if `featureEnabled` is true.
+ */
+@Singleton
+class ControlsComponent @Inject constructor(
+    @ControlsFeatureEnabled private val featureEnabled: Boolean,
+    private val lazyControlsController: Lazy<ControlsController>,
+    private val lazyControlsUiController: Lazy<ControlsUiController>,
+    private val lazyControlsListingController: Lazy<ControlsListingController>
+) {
+    fun getControlsController(): Optional<ControlsController> {
+        return if (featureEnabled) Optional.of(lazyControlsController.get()) else Optional.empty()
+    }
+
+    fun getControlsUiController(): Optional<ControlsUiController> {
+        return if (featureEnabled) Optional.of(lazyControlsUiController.get()) else Optional.empty()
+    }
+
+    fun getControlsListingController(): Optional<ControlsListingController> {
+        return if (featureEnabled) {
+            Optional.of(lazyControlsListingController.get())
+        } else {
+            Optional.empty()
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsFeatureEnabled.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsFeatureEnabled.kt
new file mode 100644
index 0000000..dd061c5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsFeatureEnabled.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.controls.dagger
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ControlsFeatureEnabled
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
index 5765be5..4760d29 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.controls.dagger
 
 import android.app.Activity
+import android.content.pm.PackageManager
 import com.android.systemui.controls.controller.ControlsBindingController
 import com.android.systemui.controls.controller.ControlsBindingControllerImpl
 import com.android.systemui.controls.controller.ControlsController
@@ -28,19 +29,39 @@
 import com.android.systemui.controls.management.ControlsListingControllerImpl
 import com.android.systemui.controls.management.ControlsProviderSelectorActivity
 import com.android.systemui.controls.management.ControlsRequestDialog
-import com.android.systemui.controls.ui.ControlsUiController
-import com.android.systemui.controls.ui.ControlsUiControllerImpl
 import com.android.systemui.controls.ui.ControlActionCoordinator
 import com.android.systemui.controls.ui.ControlActionCoordinatorImpl
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.controls.ui.ControlsUiControllerImpl
 import dagger.Binds
 import dagger.BindsOptionalOf
 import dagger.Module
+import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
+import javax.inject.Singleton
 
+/**
+ * Module for injecting classes in `com.android.systemui.controls`-
+ *
+ * Classes provided by this module should only be injected directly into other classes in this
+ * module. For injecting outside of this module (for example, [GlobalActionsDialog], inject
+ * [ControlsComponent] and obtain the corresponding optionals from it.
+ */
 @Module
 abstract class ControlsModule {
 
+    @Module
+    companion object {
+        @JvmStatic
+        @Provides
+        @Singleton
+        @ControlsFeatureEnabled
+        fun providesControlsFeatureEnabled(pm: PackageManager): Boolean {
+            return pm.hasSystemFeature(PackageManager.FEATURE_CONTROLS)
+        }
+    }
+
     @Binds
     abstract fun provideControlsListingController(
         controller: ControlsListingControllerImpl
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt
index 0d23557..bf84d77 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt
@@ -55,6 +55,9 @@
     }
 
     override fun onReceive(context: Context, intent: Intent) {
+        if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_CONTROLS)) {
+            return
+        }
 
         val packageName = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
                 ?.packageName
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index ab33291..606e947 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -60,7 +60,6 @@
 import com.android.systemui.globalactions.GlobalActionsPopupMenu
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.ShadeController
-import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import dagger.Lazy
 import java.text.Collator
@@ -80,7 +79,6 @@
     @Main val sharedPreferences: SharedPreferences,
     val controlActionCoordinator: ControlActionCoordinator,
     private val activityStarter: ActivityStarter,
-    private val keyguardStateController: KeyguardStateController,
     private val shadeController: ShadeController
 ) : ControlsUiController {
 
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 61c9a96..d66b9ac 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -120,8 +120,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.controls.ControlsServiceInfo;
 import com.android.systemui.controls.controller.ControlsController;
+import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.controls.management.ControlsAnimations;
-import com.android.systemui.controls.management.ControlsListingController;
 import com.android.systemui.controls.ui.ControlsUiController;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -140,6 +140,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
@@ -234,11 +235,12 @@
     private final IStatusBarService mStatusBarService;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private GlobalActionsPanelPlugin mWalletPlugin;
-    private ControlsUiController mControlsUiController;
+    private Optional<ControlsUiController> mControlsUiControllerOptional;
     private final IWindowManager mIWindowManager;
     private final Executor mBackgroundExecutor;
     private List<ControlsServiceInfo> mControlsServiceInfos = new ArrayList<>();
-    private ControlsController mControlsController;
+    private Optional<ControlsController> mControlsControllerOptional;
+    private SharedPreferences mControlsPreferences;
     private final RingerModeTracker mRingerModeTracker;
     private int mDialogPressDelay = DIALOG_PRESS_DELAY; // ms
     private Handler mMainHandler;
@@ -298,11 +300,11 @@
             NotificationShadeDepthController depthController, SysuiColorExtractor colorExtractor,
             IStatusBarService statusBarService,
             NotificationShadeWindowController notificationShadeWindowController,
-            ControlsUiController controlsUiController, IWindowManager iWindowManager,
+            IWindowManager iWindowManager,
             @Background Executor backgroundExecutor,
-            ControlsListingController controlsListingController,
-            ControlsController controlsController, UiEventLogger uiEventLogger,
+            UiEventLogger uiEventLogger,
             RingerModeTracker ringerModeTracker, SysUiState sysUiState, @Main Handler handler,
+            ControlsComponent controlsComponent,
             CurrentUserContextTracker currentUserContextTracker) {
         mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
         mWindowManagerFuncs = windowManagerFuncs;
@@ -325,11 +327,11 @@
         mSysuiColorExtractor = colorExtractor;
         mStatusBarService = statusBarService;
         mNotificationShadeWindowController = notificationShadeWindowController;
-        mControlsUiController = controlsUiController;
+        mControlsUiControllerOptional = controlsComponent.getControlsUiController();
         mIWindowManager = iWindowManager;
         mBackgroundExecutor = backgroundExecutor;
         mRingerModeTracker = ringerModeTracker;
-        mControlsController = controlsController;
+        mControlsControllerOptional = controlsComponent.getControlsController();
         mSysUiState = sysUiState;
         mMainHandler = handler;
         mCurrentUserContextTracker = currentUserContextTracker;
@@ -374,7 +376,7 @@
                         mDialog.mWalletViewController.onDeviceLockStateChanged(!unlocked);
                     }
                     if (!mDialog.isShowingControls() && shouldShowControls()) {
-                        mDialog.showControls(mControlsUiController);
+                        mDialog.showControls(mControlsUiControllerOptional.get());
                     }
                     if (unlocked) {
                         mDialog.hideLockMessage();
@@ -383,7 +385,16 @@
             }
         });
 
-        controlsListingController.addCallback(list -> mControlsServiceInfos = list);
+        if (controlsComponent.getControlsListingController().isPresent()) {
+            controlsComponent.getControlsListingController().get()
+                    .addCallback(list -> mControlsServiceInfos = list);
+        }
+
+        // Need to be user-specific with the context to make sure we read the correct prefs
+        Context userContext = context.createContextAsUser(
+                new UserHandle(mUserManager.getUserHandle()), 0);
+        mControlsPreferences = userContext.getSharedPreferences(PREFS_CONTROLS_FILE,
+            Context.MODE_PRIVATE);
 
         // Listen for changes to show controls on the power menu while locked
         onPowerMenuLockScreenSettingsChanged();
@@ -399,8 +410,9 @@
     }
 
     private void seedFavorites() {
+        if (!mControlsControllerOptional.isPresent()) return;
         if (mControlsServiceInfos.isEmpty()
-                || mControlsController.getFavorites().size() > 0) {
+                || mControlsControllerOptional.get().getFavorites().size() > 0) {
             return;
         }
 
@@ -433,7 +445,7 @@
             return;
         }
 
-        mControlsController.seedFavoritesForComponent(
+        mControlsControllerOptional.get().seedFavoritesForComponent(
                 preferredComponent,
                 (accepted) -> {
                     Log.i(TAG, "Controls seeded: " + accepted);
@@ -636,10 +648,14 @@
         mDepthController.setShowingHomeControls(true);
         GlobalActionsPanelPlugin.PanelViewController walletViewController =
                 getWalletViewController();
+        ControlsUiController uiController = null;
+        if (mControlsUiControllerOptional.isPresent() && shouldShowControls()) {
+            uiController = mControlsUiControllerOptional.get();
+        }
         ActionsDialog dialog = new ActionsDialog(mContext, mAdapter, mOverflowAdapter,
                 walletViewController, mDepthController, mSysuiColorExtractor,
                 mStatusBarService, mNotificationShadeWindowController,
-                controlsAvailable(), shouldShowControls() ? mControlsUiController : null,
+                controlsAvailable(), uiController,
                 mSysUiState, this::onRotate, mKeyguardShowing);
         boolean walletViewAvailable = walletViewController != null
                 && walletViewController.getPanelContent() != null;
@@ -2403,7 +2419,8 @@
 
     private boolean controlsAvailable() {
         return mDeviceProvisioned
-                && mControlsUiController.getAvailable()
+                && mControlsUiControllerOptional.isPresent()
+                && mControlsUiControllerOptional.get().getAvailable()
                 && !mControlsServiceInfos.isEmpty();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 009f549..cf7fbfa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.notification.MediaNotificationProcessor
 import com.android.systemui.statusbar.notification.row.HybridGroupManager
+import com.android.systemui.util.Assert
 import com.android.systemui.util.Utils
 import java.io.IOException
 import java.util.concurrent.Executor
@@ -85,6 +86,7 @@
 
     fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
         if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
+            Assert.isMainThread()
             if (!mediaEntries.containsKey(key)) {
                 mediaEntries.put(key, LOADING)
             }
@@ -269,19 +271,23 @@
     }
 
     fun onMediaDataLoaded(key: String, data: MediaData) {
+        Assert.isMainThread()
         if (mediaEntries.containsKey(key)) {
             // Otherwise this was removed already
             mediaEntries.put(key, data)
-            listeners.forEach {
+            val listenersCopy = listeners.toSet()
+            listenersCopy.forEach {
                 it.onMediaDataLoaded(key, data)
             }
         }
     }
 
     fun onNotificationRemoved(key: String) {
+        Assert.isMainThread()
         val removed = mediaEntries.remove(key)
         if (removed != null) {
-            listeners.forEach {
+            val listenersCopy = listeners.toSet()
+            listenersCopy.forEach {
                 it.onMediaDataRemoved(key)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
index fe84d818..3874903 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
@@ -31,6 +31,8 @@
 import android.graphics.drawable.ColorDrawable;
 import android.os.Binder;
 import android.os.RemoteException;
+import android.text.SpannableStringBuilder;
+import android.text.style.BulletSpan;
 import android.util.DisplayMetrics;
 import android.view.Gravity;
 import android.view.View;
@@ -293,8 +295,20 @@
                         .setImageDrawable(navigationBarView.getHomeDrawable());
             }
 
-            ((TextView) mLayout.findViewById(R.id.screen_pinning_description))
-                    .setText(descriptionStringResId);
+            // Create a bulleted list of the default description plus the two security notes.
+            int gapWidth = getResources().getDimensionPixelSize(
+                    R.dimen.screen_pinning_description_bullet_gap_width);
+            SpannableStringBuilder description = new SpannableStringBuilder();
+            description.append(getContext().getText(descriptionStringResId),
+                    new BulletSpan(gapWidth), /* flags */ 0);
+            description.append(System.lineSeparator());
+            description.append(getContext().getText(R.string.screen_pinning_exposes_personal_data),
+                    new BulletSpan(gapWidth), /* flags */ 0);
+            description.append(System.lineSeparator());
+            description.append(getContext().getText(R.string.screen_pinning_can_open_other_apps),
+                    new BulletSpan(gapWidth), /* flags */ 0);
+            ((TextView) mLayout.findViewById(R.id.screen_pinning_description)).setText(description);
+
             final int backBgVisibility = touchExplorationEnabled ? View.INVISIBLE : View.VISIBLE;
             mLayout.findViewById(R.id.screen_pinning_back_bg).setVisibility(backBgVisibility);
             mLayout.findViewById(R.id.screen_pinning_back_bg_light).setVisibility(backBgVisibility);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index f2d2eb3..33d692f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -431,7 +431,13 @@
         data.createDeleteAction = false;
 
         if (mSaveInBgTask != null) {
-            mSaveInBgTask.ignoreResult();
+            // just log success/failure for the pre-existing screenshot
+            mSaveInBgTask.setActionsReadyListener(new ActionsReadyListener() {
+                @Override
+                void onActionsReady(SavedImageData imageData) {
+                    logSuccessOnActionsReady(imageData);
+                }
+            });
         }
 
         mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data);
@@ -637,6 +643,52 @@
     }
 
     /**
+     * Sets up the action shade and its entrance animation, once we get the screenshot URI.
+     */
+    private void showUiOnActionsReady(SavedImageData imageData) {
+        logSuccessOnActionsReady(imageData);
+        if (imageData.uri != null) {
+            mScreenshotHandler.post(() -> {
+                if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
+                    mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            super.onAnimationEnd(animation);
+                            createScreenshotActionsShadeAnimation(imageData).start();
+                        }
+                    });
+                } else {
+                    createScreenshotActionsShadeAnimation(imageData).start();
+                }
+
+                AccessibilityManager accessibilityManager = (AccessibilityManager)
+                        mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+                long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis(
+                        SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS,
+                        AccessibilityManager.FLAG_CONTENT_CONTROLS);
+
+                mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
+                mScreenshotHandler.sendMessageDelayed(
+                        mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
+                        timeoutMs);
+            });
+        }
+    }
+
+    /**
+     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
+     */
+    private void logSuccessOnActionsReady(SavedImageData imageData) {
+        if (imageData.uri == null) {
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
+            mNotificationsController.notifyScreenshotError(
+                    R.string.screenshot_failed_to_capture_text);
+        } else {
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
+        }
+    }
+
+    /**
      * Starts the animation after taking the screenshot
      */
     private void startAnimation(final Consumer<Uri> finisher, int w, int h,
@@ -651,43 +703,11 @@
         mScreenshotAnimation = createScreenshotDropInAnimation(w, h, screenRect);
 
         saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() {
-            @Override
-            void onActionsReady(SavedImageData imageData) {
-                finisher.accept(imageData.uri);
-                if (imageData.uri == null) {
-                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
-                    mNotificationsController.notifyScreenshotError(
-                            R.string.screenshot_failed_to_capture_text);
-                } else {
-                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
-                    mScreenshotHandler.post(() -> {
-                        if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
-                            mScreenshotAnimation.addListener(
-                                    new AnimatorListenerAdapter() {
-                                        @Override
-                                        public void onAnimationEnd(Animator animation) {
-                                            super.onAnimationEnd(animation);
-                                            createScreenshotActionsShadeAnimation(imageData)
-                                                    .start();
-                                        }
-                                    });
-                        } else {
-                            createScreenshotActionsShadeAnimation(imageData).start();
-                        }
-                        AccessibilityManager accessibilityManager = (AccessibilityManager)
-                                mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
-                        long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis(
-                                SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS,
-                                AccessibilityManager.FLAG_CONTENT_CONTROLS);
-
-                        mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
-                        mScreenshotHandler.sendMessageDelayed(
-                                mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
-                                timeoutMs);
-                    });
-                }
-            }
-        });
+                    @Override
+                    void onActionsReady(SavedImageData imageData) {
+                        showUiOnActionsReady(imageData);
+                    }
+                });
         mScreenshotHandler.post(() -> {
             if (!mScreenshotLayout.isAttachedToWindow()) {
                 mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index f0a81e9..a5bab21 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -214,6 +214,7 @@
             mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
 
             mParams.mActionsReadyListener.onActionsReady(mImageData);
+            mParams.finisher.accept(mImageData.uri);
             mParams.image = null;
             mParams.errorMsgResId = 0;
         } catch (Exception e) {
@@ -224,22 +225,18 @@
             mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
             mImageData.reset();
             mParams.mActionsReadyListener.onActionsReady(mImageData);
+            mParams.finisher.accept(null);
         }
 
         return null;
     }
 
     /**
-     * If we get a new screenshot request while this one is saving, we want to continue saving in
-     * the background but not return anything.
+     * Update the listener run when the saving task completes. Used to avoid showing UI for the
+     * first screenshot when a second one is taken.
      */
-    void ignoreResult() {
-        mParams.mActionsReadyListener = new GlobalScreenshot.ActionsReadyListener() {
-            @Override
-            void onActionsReady(GlobalScreenshot.SavedImageData imageData) {
-                // do nothing
-            }
-        };
+    void setActionsReadyListener(GlobalScreenshot.ActionsReadyListener listener) {
+        mParams.mActionsReadyListener = listener;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt
index 2ed04eb..9dbec10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt
@@ -22,7 +22,9 @@
         entryManager.addCollectionListener(this)
     }
 
-    fun hasUserInteractedWith(key: String): Boolean = key in interactions
+    fun hasUserInteractedWith(key: String): Boolean {
+        return interactions[key] ?: false
+    }
 
     override fun onEntryAdded(entry: NotificationEntry) {
         interactions[entry.key] = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
index 85560fe..6aef6b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
@@ -19,7 +19,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.graphics.Matrix;
 import android.graphics.Rect;
@@ -42,8 +41,6 @@
 import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController;
 
-import java.util.concurrent.Executor;
-
 /**
  * A class that allows activities to be launched in a seamless way where the notification
  * transforms nicely into the starting window.
@@ -62,7 +59,6 @@
     private final float mWindowCornerRadius;
     private final NotificationShadeWindowViewController mNotificationShadeWindowViewController;
     private final NotificationShadeDepthController mDepthController;
-    private final Executor mMainExecutor;
     private Callback mCallback;
     private final Runnable mTimeoutRunnable = () -> {
         setAnimationPending(false);
@@ -77,14 +73,12 @@
             Callback callback,
             NotificationPanelViewController notificationPanel,
             NotificationShadeDepthController depthController,
-            NotificationListContainer container,
-            Executor mainExecutor) {
+            NotificationListContainer container) {
         mNotificationPanel = notificationPanel;
         mNotificationContainer = container;
         mDepthController = depthController;
         mNotificationShadeWindowViewController = notificationShadeWindowViewController;
         mCallback = callback;
-        mMainExecutor = mainExecutor;
         mWindowCornerRadius = ScreenDecorationsUtils
                 .getWindowCornerRadius(mNotificationShadeWindowViewController.getView()
                         .getResources());
@@ -97,7 +91,7 @@
             return null;
         }
         AnimationRunner animationRunner = new AnimationRunner(
-                (ExpandableNotificationRow) sourceView, mMainExecutor);
+                (ExpandableNotificationRow) sourceView);
         return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION,
                 ANIMATION_DURATION - 150 /* statusBarTransitionDelay */);
     }
@@ -140,18 +134,17 @@
 
     class AnimationRunner extends IRemoteAnimationRunner.Stub {
 
-        private final ExpandAnimationParameters mParams = new ExpandAnimationParameters();
+        private final ExpandableNotificationRow mSourceNotification;
+        private final ExpandAnimationParameters mParams;
         private final Rect mWindowCrop = new Rect();
         private final float mNotificationCornerRadius;
-        private final Executor mMainExecutor;
-        @Nullable private ExpandableNotificationRow mSourceNotification;
-        @Nullable private SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier;
         private float mCornerRadius;
         private boolean mIsFullScreenLaunch = true;
+        private final SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier;
 
-        AnimationRunner(ExpandableNotificationRow sourceNotification, Executor mainExecutor) {
-            mMainExecutor = mainExecutor;
-            mSourceNotification = sourceNotification;
+        public AnimationRunner(ExpandableNotificationRow sourceNofitication) {
+            mSourceNotification = sourceNofitication;
+            mParams = new ExpandAnimationParameters();
             mSyncRtTransactionApplier = new SyncRtSurfaceTransactionApplier(mSourceNotification);
             mNotificationCornerRadius = Math.max(mSourceNotification.getCurrentTopRoundness(),
                     mSourceNotification.getCurrentBottomRoundness());
@@ -162,15 +155,13 @@
                 RemoteAnimationTarget[] remoteAnimationWallpaperTargets,
                 IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)
                     throws RemoteException {
-            mMainExecutor.execute(() -> {
+            mSourceNotification.post(() -> {
                 RemoteAnimationTarget primary = getPrimaryRemoteAnimationTarget(
                         remoteAnimationTargets);
-                if (primary == null || mSourceNotification == null) {
+                if (primary == null) {
                     setAnimationPending(false);
                     invokeCallback(iRemoteAnimationFinishedCallback);
                     mNotificationPanel.collapse(false /* delayed */, 1.0f /* speedUpFactor */);
-                    mSourceNotification = null;
-                    mSyncRtTransactionApplier = null;
                     return;
                 }
 
@@ -181,14 +172,28 @@
                 if (!mIsFullScreenLaunch) {
                     mNotificationPanel.collapseWithDuration(ANIMATION_DURATION);
                 }
-                mParams.initFrom(mSourceNotification);
-                final int targetWidth = primary.sourceContainerBounds.width();
-                final int notificationHeight;
-                final int notificationWidth;
-                notificationHeight = mSourceNotification.getActualHeight()
-                        - mSourceNotification.getClipBottomAmount();
-                notificationWidth = mSourceNotification.getWidth();
                 ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+                mParams.startPosition = mSourceNotification.getLocationOnScreen();
+                mParams.startTranslationZ = mSourceNotification.getTranslationZ();
+                mParams.startClipTopAmount = mSourceNotification.getClipTopAmount();
+                if (mSourceNotification.isChildInGroup()) {
+                    int parentClip = mSourceNotification
+                            .getNotificationParent().getClipTopAmount();
+                    mParams.parentStartClipTopAmount = parentClip;
+                    // We need to calculate how much the child is clipped by the parent
+                    // because children always have 0 clipTopAmount
+                    if (parentClip != 0) {
+                        float childClip = parentClip
+                                - mSourceNotification.getTranslationY();
+                        if (childClip > 0.0f) {
+                            mParams.startClipTopAmount = (int) Math.ceil(childClip);
+                        }
+                    }
+                }
+                int targetWidth = primary.sourceContainerBounds.width();
+                int notificationHeight = mSourceNotification.getActualHeight()
+                        - mSourceNotification.getClipBottomAmount();
+                int notificationWidth = mSourceNotification.getWidth();
                 anim.setDuration(ANIMATION_DURATION);
                 anim.setInterpolator(Interpolators.LINEAR);
                 anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@@ -226,11 +231,6 @@
             });
         }
 
-        @Nullable
-        ExpandableNotificationRow getRow() {
-            return mSourceNotification;
-        }
-
         private void invokeCallback(IRemoteAnimationFinishedCallback callback) {
             try {
                 callback.onAnimationFinished();
@@ -253,9 +253,7 @@
 
         private void setExpandAnimationRunning(boolean running) {
             mNotificationPanel.setLaunchingNotification(running);
-            if (mSourceNotification != null) {
-                mSourceNotification.setExpandAnimationRunning(running);
-            }
+            mSourceNotification.setExpandAnimationRunning(running);
             mNotificationShadeWindowViewController.setExpandAnimationRunning(running);
             mNotificationContainer.setExpandingNotification(running ? mSourceNotification : null);
             mAnimationRunning = running;
@@ -263,8 +261,6 @@
                 mCallback.onExpandAnimationFinished(mIsFullScreenLaunch);
                 applyParamsToNotification(null);
                 applyParamsToNotificationShade(null);
-                mSourceNotification = null;
-                mSyncRtTransactionApplier = null;
             }
 
         }
@@ -276,9 +272,7 @@
         }
 
         private void applyParamsToNotification(ExpandAnimationParameters params) {
-            if (mSourceNotification != null) {
-                mSourceNotification.applyExpandAnimationParams(params);
-            }
+            mSourceNotification.applyExpandAnimationParams(params);
         }
 
         private void applyParamsToWindow(RemoteAnimationTarget app) {
@@ -293,18 +287,14 @@
                     .withCornerRadius(mCornerRadius)
                     .withVisibility(true)
                     .build();
-            if (mSyncRtTransactionApplier != null) {
-                mSyncRtTransactionApplier.scheduleApply(true /* earlyWakeup */, params);
-            }
+            mSyncRtTransactionApplier.scheduleApply(true /* earlyWakeup */, params);
         }
 
         @Override
         public void onAnimationCancelled() throws RemoteException {
-            mMainExecutor.execute(() -> {
+            mSourceNotification.post(() -> {
                 setAnimationPending(false);
                 mCallback.onLaunchAnimationCancelled();
-                mSourceNotification = null;
-                mSyncRtTransactionApplier = null;
             });
         }
     };
@@ -369,28 +359,6 @@
         public float getStartTranslationZ() {
             return startTranslationZ;
         }
-
-        /** Initialize with data pulled from the row. */
-        void initFrom(@Nullable ExpandableNotificationRow row) {
-            if (row == null) {
-                return;
-            }
-            startPosition = row.getLocationOnScreen();
-            startTranslationZ = row.getTranslationZ();
-            startClipTopAmount = row.getClipTopAmount();
-            if (row.isChildInGroup()) {
-                int parentClip = row.getNotificationParent().getClipTopAmount();
-                parentStartClipTopAmount = parentClip;
-                // We need to calculate how much the child is clipped by the parent
-                // because children always have 0 clipTopAmount
-                if (parentClip != 0) {
-                    float childClip = parentClip - row.getTranslationY();
-                    if (childClip > 0.0f) {
-                        startClipTopAmount = (int) Math.ceil(childClip);
-                    }
-                }
-            }
-        }
     }
 
     public interface Callback {
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 1eadd9e..3377144 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
@@ -41,6 +41,7 @@
 import android.app.NotificationChannel;
 import android.app.NotificationManager.Policy;
 import android.app.Person;
+import android.app.RemoteInput;
 import android.app.RemoteInputHistoryItem;
 import android.content.Context;
 import android.content.pm.ShortcutInfo;
@@ -69,6 +70,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
 import com.android.systemui.statusbar.notification.stack.PriorityBucket;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -132,7 +134,7 @@
     private ShortcutInfo mShortcutInfo;
 
     /**
-     * If {@link android.app.RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is
+     * If {@link RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is
      * currently editing a choice (smart reply), then this field contains the information about the
      * suggestion being edited. Otherwise <code>null</code>.
      */
@@ -174,6 +176,8 @@
     private boolean mPulseSupressed;
     private boolean mAllowFgsDismissal;
     private int mBucket = BUCKET_ALERTING;
+    @Nullable private Long mPendingAnimationDuration;
+    private boolean mIsMarkedForUserTriggeredMovement;
 
     /**
      * @param sbn the StatusBarNotification from system server
@@ -193,7 +197,7 @@
             boolean allowFgsDismissal,
             long creationTime
     ) {
-        super(requireNonNull(Objects.requireNonNull(sbn).getKey()));
+        super(requireNonNull(requireNonNull(sbn).getKey()));
 
         requireNonNull(ranking);
 
@@ -441,7 +445,7 @@
      * Get the children that are actually attached to this notification's row.
      *
      * TODO: Seems like most callers here should probably be using
-     * {@link com.android.systemui.statusbar.phone.NotificationGroupManager#getChildren}
+     * {@link NotificationGroupManager#getChildren}
      */
     public @Nullable List<NotificationEntry> getAttachedNotifChildren() {
         if (row == null) {
@@ -809,7 +813,7 @@
         }
 
         if ((mSbn.getNotification().flags
-                & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+                & FLAG_FOREGROUND_SERVICE) != 0) {
             return true;
         }
         if (mSbn.getNotification().isMediaNotification()) {
@@ -942,6 +946,19 @@
         mPulseSupressed = suppressed;
     }
 
+    /** Whether or not this entry has been marked for a user-triggered movement. */
+    public boolean isMarkedForUserTriggeredMovement() {
+        return mIsMarkedForUserTriggeredMovement;
+    }
+
+    /**
+     * Mark this entry for movement triggered by a user action (ex: changing the priorirty of a
+     * conversation). This can then be used for custom animations.
+     */
+    public void markForUserTriggeredMovement(boolean marked) {
+        mIsMarkedForUserTriggeredMovement = marked;
+    }
+
     /** Information about a suggestion that is being edited. */
     public static class EditedSuggestionInfo {
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index f55ce77..033a638 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -25,6 +25,7 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -105,6 +106,7 @@
             VisualStabilityManager visualStabilityManager,
             Lazy<StatusBar> statusBarLazy,
             @Main Handler mainHandler,
+            @Background Handler bgHandler,
             AccessibilityManager accessibilityManager,
             HighPriorityProvider highPriorityProvider,
             INotificationManager notificationManager,
@@ -118,6 +120,7 @@
                 visualStabilityManager,
                 statusBarLazy,
                 mainHandler,
+                bgHandler,
                 accessibilityManager,
                 highPriorityProvider,
                 notificationManager,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
index e0583be..9217756 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.notification.row;
 
-import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION;
 import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
 import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
 import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
@@ -43,9 +42,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
-import android.graphics.drawable.Icon;
 import android.os.Handler;
-import android.os.Parcelable;
 import android.os.RemoteException;
 import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
@@ -65,15 +62,16 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.settingslib.notification.ConversationIconFactory;
-import com.android.systemui.Dependency;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.statusbar.notification.NotificationChannelHelper;
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 
 import java.lang.annotation.Retention;
-import java.util.List;
 
 import javax.inject.Provider;
 
@@ -86,10 +84,12 @@
 
 
     private INotificationManager mINotificationManager;
-    ShortcutManager mShortcutManager;
+    private ShortcutManager mShortcutManager;
     private PackageManager mPm;
     private ConversationIconFactory mIconFactory;
     private VisualStabilityManager mVisualStabilityManager;
+    private Handler mMainHandler;
+    private Handler mBgHandler;
 
     private String mPackageName;
     private String mAppName;
@@ -97,6 +97,7 @@
     private String mDelegatePkg;
     private NotificationChannel mNotificationChannel;
     private ShortcutInfo mShortcutInfo;
+    private NotificationEntry mEntry;
     private StatusBarNotification mSbn;
     @Nullable private Notification.BubbleMetadata mBubbleMetadata;
     private Context mUserContext;
@@ -213,11 +214,14 @@
             ConversationIconFactory conversationIconFactory,
             Context userContext,
             Provider<PriorityOnboardingDialogController.Builder> builderProvider,
-            boolean isDeviceProvisioned) {
+            boolean isDeviceProvisioned,
+            @Main Handler mainHandler,
+            @Background Handler bgHandler) {
         mSelectedAction = -1;
         mINotificationManager = iNotificationManager;
         mVisualStabilityManager = visualStabilityManager;
         mPackageName = pkg;
+        mEntry = entry;
         mSbn = entry.getSbn();
         mPm = pm;
         mAppName = mPackageName;
@@ -231,7 +235,8 @@
         mUserContext = userContext;
         mBubbleMetadata = bubbleMetadata;
         mBuilderProvider = builderProvider;
-
+        mMainHandler = mainHandler;
+        mBgHandler = bgHandler;
         mShortcutManager = shortcutManager;
         mShortcutInfo = entry.getRanking().getShortcutInfo();
         if (mShortcutInfo == null) {
@@ -494,11 +499,13 @@
     }
 
     private void updateChannel() {
-        Handler bgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER));
-        bgHandler.post(
+        mBgHandler.post(
                 new UpdateChannelRunnable(mINotificationManager, mPackageName,
                         mAppUid, mSelectedAction, mNotificationChannel));
-        mVisualStabilityManager.temporarilyAllowReordering();
+        mMainHandler.postDelayed(() -> {
+            mEntry.markForUserTriggeredMovement(true);
+            mVisualStabilityManager.temporarilyAllowReordering();
+        }, StackStateAnimator.ANIMATION_DURATION_STANDARD);
     }
 
     private boolean shouldShowPriorityOnboarding() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 3f7c7ca..1caf8f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -46,6 +46,7 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -111,6 +112,7 @@
 
     private final Lazy<StatusBar> mStatusBarLazy;
     private final Handler mMainHandler;
+    private final Handler mBgHandler;
     private Runnable mOpenRunnable;
     private final INotificationManager mNotificationManager;
     private final LauncherApps mLauncherApps;
@@ -122,7 +124,7 @@
      * Injected constructor. See {@link NotificationsModule}.
      */
     public NotificationGutsManager(Context context, VisualStabilityManager visualStabilityManager,
-            Lazy<StatusBar> statusBarLazy, @Main Handler mainHandler,
+            Lazy<StatusBar> statusBarLazy, @Main Handler mainHandler, @Background Handler bgHandler,
             AccessibilityManager accessibilityManager,
             HighPriorityProvider highPriorityProvider,
             INotificationManager notificationManager,
@@ -135,6 +137,7 @@
         mVisualStabilityManager = visualStabilityManager;
         mStatusBarLazy = statusBarLazy;
         mMainHandler = mainHandler;
+        mBgHandler = bgHandler;
         mAccessibilityManager = accessibilityManager;
         mHighPriorityProvider = highPriorityProvider;
         mNotificationManager = notificationManager;
@@ -463,7 +466,9 @@
                 iconFactoryLoader,
                 mContextTracker.getCurrentUserContext(),
                 mBuilderProvider,
-                mDeviceProvisionedController.isDeviceProvisioned());
+                mDeviceProvisionedController.isDeviceProvisioned(),
+                mMainHandler,
+                mBgHandler);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index bcafd0ee..a877bc1c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -3655,8 +3655,19 @@
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private void generatePositionChangeEvents() {
         for (ExpandableView child : mChildrenChangingPositions) {
-            mAnimationEvents.add(new AnimationEvent(child,
-                    AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
+            Integer duration = null;
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                if (row.getEntry().isMarkedForUserTriggeredMovement()) {
+                    duration = StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE;
+                    row.getEntry().markForUserTriggeredMovement(false);
+                }
+            }
+            AnimationEvent animEvent = duration == null
+                    ? new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)
+                    : new AnimationEvent(
+                            child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION, duration);
+            mAnimationEvents.add(animEvent);
         }
         mChildrenChangingPositions.clear();
         if (mGenerateChildOrderChangedEvent) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index 7785082..d4add95 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -56,6 +56,7 @@
     public static final int ANIMATION_DURATION_PULSE_APPEAR =
             KeyguardSliceView.DEFAULT_ANIM_DURATION;
     public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
+    public static final int ANIMATION_DURATION_PRIORITY_CHANGE = 500;
     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
index dcc3107..67b7e97 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
@@ -18,10 +18,8 @@
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 
-import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.os.UserManager;
-import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
@@ -97,33 +95,9 @@
     }
 
     public boolean isMultiUserEnabled() {
-        // Short-circuiting from UserManager. Needs to be extracted because of SystemUI boolean flag
-        // qs_show_user_switcher_for_single_user
-
         // TODO(b/138661450) Move IPC calls to background
-        return whitelistIpcs(() -> {
-            // The default in UserManager is to show the switcher. We want to not show it unless the
-            // user explicitly requests it in Settings
-            final boolean userSwitcherEnabled = Settings.Global.getInt(
-                    mContext.getContentResolver(),
-                    Settings.Global.USER_SWITCHER_ENABLED, 0) != 0;
-
-            if (!userSwitcherEnabled
-                    || !UserManager.supportsMultipleUsers()
-                    || UserManager.isDeviceInDemoMode(mContext)
-                    || mUserManager.hasUserRestriction(UserManager.DISALLOW_USER_SWITCH)) {
-                return false;
-            }
-
-            final boolean guestEnabled = !mContext.getSystemService(DevicePolicyManager.class)
-                    .getGuestUserDisabled(null);
-            return mUserSwitcherController.getSwitchableUserCount() > 1
-                    // If we cannot add guests even if they are enabled, do not show
-                    || (guestEnabled && !mUserManager.hasUserRestriction(
-                    UserManager.DISALLOW_ADD_USER))
-                    || mContext.getResources().getBoolean(
-                    R.bool.qs_show_user_switcher_for_single_user);
-        });
+        return whitelistIpcs(() -> mUserManager.isUserSwitcherEnabled(
+                mContext.getResources().getBoolean(R.bool.qs_show_user_switcher_for_single_user)));
     }
 
     private void registerListener() {
@@ -175,7 +149,7 @@
     private void refreshContentDescription() {
         String currentUser = null;
         // TODO(b/138661450)
-        if (whitelistIpcs(mUserManager::isUserSwitcherEnabled)
+        if (whitelistIpcs(() -> mUserManager.isUserSwitcherEnabled())
                 && mUserSwitcherController != null) {
             currentUser = mUserSwitcherController.getCurrentUserName(mContext);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 662c744..46c873d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -93,7 +93,6 @@
 
     private boolean mIsVertical;
     private boolean mAlternativeOrder;
-    private boolean mUsingCustomLayout;
 
     private OverviewProxyService mOverviewProxyService;
     private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
@@ -145,7 +144,6 @@
     @Override
     public void onNavigationModeChanged(int mode) {
         mNavBarMode = mode;
-        onLikelyDefaultLayoutChange();
     }
 
     @Override
@@ -154,17 +152,7 @@
         super.onDetachedFromWindow();
     }
 
-    public void setNavigationBarLayout(String layoutValue) {
-        if (!Objects.equals(mCurrentLayout, layoutValue)) {
-            mUsingCustomLayout = layoutValue != null;
-            clearViews();
-            inflateLayout(layoutValue);
-        }
-    }
-
     public void onLikelyDefaultLayoutChange() {
-        // Don't override custom layouts
-        if (mUsingCustomLayout) return;
 
         // Reevaluate new layout
         final String newValue = getDefaultLayout();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
index 06b7d1a..daefef5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
@@ -16,19 +16,14 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static android.content.Intent.ACTION_OVERLAY_CHANGED;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY;
 
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.om.IOverlayManager;
 import android.content.om.OverlayInfo;
 import android.content.pm.PackageManager;
 import android.content.res.ApkAssets;
-import android.os.PatternMatcher;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -39,6 +34,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 
 import java.io.FileDescriptor;
@@ -69,16 +65,6 @@
 
     private ArrayList<ModeChangedListener> mListeners = new ArrayList<>();
 
-    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (DEBUG) {
-                Log.d(TAG, "ACTION_OVERLAY_CHANGED");
-            }
-            updateCurrentInteractionMode(true /* notify */);
-        }
-    };
-
     private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedCallback =
             new DeviceProvisionedController.DeviceProvisionedListener() {
                 @Override
@@ -97,6 +83,7 @@
     @Inject
     public NavigationModeController(Context context,
             DeviceProvisionedController deviceProvisionedController,
+            ConfigurationController configurationController,
             @UiBackground Executor uiBgExecutor) {
         mContext = context;
         mCurrentUserContext = context;
@@ -105,10 +92,15 @@
         mUiBgExecutor = uiBgExecutor;
         deviceProvisionedController.addCallback(mDeviceProvisionedCallback);
 
-        IntentFilter overlayFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
-        overlayFilter.addDataScheme("package");
-        overlayFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL);
-        mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, overlayFilter, null, null);
+        configurationController.addCallback(new ConfigurationController.ConfigurationListener() {
+            @Override
+            public void onOverlayChanged() {
+                if (DEBUG) {
+                    Log.d(TAG, "onOverlayChanged");
+                }
+                updateCurrentInteractionMode(true /* notify */);
+            }
+        });
 
         updateCurrentInteractionMode(false /* notify */);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 19de191..e0e52001 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -143,7 +143,6 @@
 import com.android.systemui.charging.WirelessChargingAnimation;
 import com.android.systemui.classifier.FalsingLog;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.fragments.ExtensionFragmentListener;
 import com.android.systemui.fragments.FragmentHostManager;
@@ -511,7 +510,6 @@
     private final ScrimController mScrimController;
     protected DozeScrimController mDozeScrimController;
     private final Executor mUiBgExecutor;
-    private final Executor mMainExecutor;
 
     protected boolean mDozing;
 
@@ -670,7 +668,6 @@
             DisplayMetrics displayMetrics,
             MetricsLogger metricsLogger,
             @UiBackground Executor uiBgExecutor,
-            @Main Executor mainExecutor,
             NotificationMediaManager notificationMediaManager,
             NotificationLockscreenUserManager lockScreenUserManager,
             NotificationRemoteInputManager remoteInputManager,
@@ -751,7 +748,6 @@
         mDisplayMetrics = displayMetrics;
         mMetricsLogger = metricsLogger;
         mUiBgExecutor = uiBgExecutor;
-        mMainExecutor = mainExecutor;
         mMediaManager = notificationMediaManager;
         mLockscreenUserManager = lockScreenUserManager;
         mRemoteInputManager = remoteInputManager;
@@ -1279,8 +1275,7 @@
         mActivityLaunchAnimator = new ActivityLaunchAnimator(
                 mNotificationShadeWindowViewController, this, mNotificationPanelViewController,
                 mNotificationShadeDepthControllerLazy.get(),
-                (NotificationListContainer) mStackScroller,
-                mMainExecutor);
+                (NotificationListContainer) mStackScroller);
 
         // TODO: inject this.
         mPresenter = new StatusBarNotificationPresenter(mContext, mNotificationPanelViewController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 669e6a4..72395e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -30,7 +30,6 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewParent;
 
@@ -49,8 +48,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
-import java.util.concurrent.atomic.AtomicReference;
-
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
@@ -58,8 +55,7 @@
  */
 @Singleton
 public class StatusBarRemoteInputCallback implements Callback, Callbacks,
-        StatusBarStateController.StateListener, KeyguardStateController.Callback {
-    private static final String TAG = StatusBarRemoteInputCallback.class.getSimpleName();
+        StatusBarStateController.StateListener {
 
     private final KeyguardStateController mKeyguardStateController;
     private final SysuiStatusBarStateController mStatusBarStateController;
@@ -78,7 +74,6 @@
     private int mDisabled2;
     protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver();
     private Handler mMainHandler = new Handler();
-    private final AtomicReference<Intent> mPendingConfirmCredentialIntent = new AtomicReference();
 
     /**
      */
@@ -107,9 +102,6 @@
         mActionClickLogger = clickLogger;
         mActivityIntentHelper = new ActivityIntentHelper(mContext);
         mGroupManager = groupManager;
-        // Listen to onKeyguardShowingChanged in case a managed profile needs to be unlocked
-        // once the primary profile's keyguard is no longer shown.
-        mKeyguardStateController.addCallback(this);
     }
 
     @Override
@@ -213,39 +205,12 @@
         // Clear pending remote view, as we do not want to trigger pending remote input view when
         // it's called by other code
         mPendingWorkRemoteInputView = null;
-
-        final Intent newIntent = createConfirmDeviceCredentialIntent(
-                userId, intendSender, notificationKey);
-        if (newIntent == null) {
-            Log.w(TAG, String.format("Cannot create intent to unlock user %d", userId));
-            return false;
-        }
-
-        mPendingConfirmCredentialIntent.set(newIntent);
-
-        // If the Keyguard is currently showing, starting the ConfirmDeviceCredentialActivity
-        // would cause it to pause, not letting the user actually unlock the managed profile.
-        // Instead, wait until we receive a callback indicating it is no longer showing and
-        // then start the pending intent.
-        if (mKeyguardStateController.isShowing()) {
-            // Do nothing, since the callback will get the pending intent and start it.
-            Log.w(TAG, String.format("Keyguard is showing, waiting until it's not"));
-        } else {
-            startPendingConfirmDeviceCredentialIntent();
-        }
-
-        return true;
-    }
-
-    private Intent createConfirmDeviceCredentialIntent(
-            int userId, IntentSender intendSender, String notificationKey) {
+        // Begin old BaseStatusBar.startWorkChallengeIfNecessary.
         final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null,
                 null, userId);
-
         if (newIntent == null) {
-            return null;
+            return false;
         }
-
         final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
         callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender);
         callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey);
@@ -261,40 +226,14 @@
         newIntent.putExtra(
                 Intent.EXTRA_INTENT,
                 callBackPendingIntent.getIntentSender());
-
-        return newIntent;
-    }
-
-    private void startPendingConfirmDeviceCredentialIntent() {
-        final Intent pendingIntent = mPendingConfirmCredentialIntent.getAndSet(null);
-        if (pendingIntent == null) {
-            return;
-        }
-
         try {
-            if (mKeyguardStateController.isShowing()) {
-                Log.w(TAG, "Keyguard is showing while starting confirm device credential intent.");
-            }
-            ActivityManager.getService().startConfirmDeviceCredentialIntent(pendingIntent,
+            ActivityManager.getService().startConfirmDeviceCredentialIntent(newIntent,
                     null /*options*/);
         } catch (RemoteException ex) {
             // ignore
         }
-    }
-
-    @Override
-    public void onKeyguardShowingChanged() {
-        if (mKeyguardStateController.isShowing()) {
-            // In order to avoid jarring UX where/ the managed profile challenge is shown and
-            // immediately dismissed, do not attempt to start the confirm device credential
-            // activity if the keyguard is still showing.
-            if (mPendingConfirmCredentialIntent.get() != null) {
-                Log.w(TAG, "There's a pending unlock intent but keyguard is still showing, abort.");
-            }
-            return;
-        }
-
-        startPendingConfirmDeviceCredentialIntent();
+        return true;
+        // End old BaseStatusBar.startWorkChallengeIfNecessary.
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index 62a3cf0..02e0312 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -33,7 +33,6 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -146,7 +145,6 @@
             DisplayMetrics displayMetrics,
             MetricsLogger metricsLogger,
             @UiBackground Executor uiBgExecutor,
-            @Main Executor mainExecutor,
             NotificationMediaManager notificationMediaManager,
             NotificationLockscreenUserManager lockScreenUserManager,
             NotificationRemoteInputManager remoteInputManager,
@@ -226,7 +224,6 @@
                 displayMetrics,
                 metricsLogger,
                 uiBgExecutor,
-                mainExecutor,
                 notificationMediaManager,
                 lockScreenUserManager,
                 remoteInputManager,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index 412962c..db00770 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -409,18 +409,6 @@
         Log.e(TAG, "Couldn't switch to user, id=" + userId);
     }
 
-    public int getSwitchableUserCount() {
-        int count = 0;
-        final int N = mUsers.size();
-        for (int i = 0; i < N; ++i) {
-            UserRecord record = mUsers.get(i);
-            if (record.info != null && record.info.supportsSwitchToByUser()) {
-                count++;
-            }
-        }
-        return count;
-    }
-
     protected void switchToUserId(int id) {
         try {
             pauseRefreshUsers();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 9b377ca..c89f6c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -319,7 +319,7 @@
         verify(mNotificationEntryManager).updateNotifications(any());
 
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
         verify(mNotificationEntryManager, times(2)).updateNotifications(anyString());
 
@@ -331,7 +331,7 @@
         mBubbleController.updateBubble(mRow2.getEntry());
         mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
 
         Bubble b = mBubbleData.getOverflowBubbleWithKey(mRow.getEntry().getKey());
         assertThat(mBubbleData.getOverflowBubbles()).isEqualTo(ImmutableList.of(b));
@@ -352,9 +352,10 @@
         mBubbleController.updateBubble(mRow.getEntry(), /* suppressFlyout */
                 false, /* showInShade */ true);
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
 
-        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleController.removeBubble(
+                mRow.getEntry().getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
         verify(mNotificationEntryManager, times(1)).performRemoveNotification(
                 eq(mRow.getEntry().getSbn()), anyInt());
         assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
@@ -367,7 +368,7 @@
         assertTrue(mBubbleController.hasBubbles());
 
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_CHANGED);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_CHANGED);
         verify(mNotificationEntryManager, never()).performRemoveNotification(
                 eq(mRow.getEntry().getSbn()), anyInt());
         assertFalse(mBubbleController.hasBubbles());
@@ -565,7 +566,8 @@
 
         // Dismiss currently expanded
         mBubbleController.removeBubble(
-                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey()).getEntry(),
+                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey())
+                        .getEntry().getKey(),
                 BubbleController.DISMISS_USER_GESTURE);
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
 
@@ -576,7 +578,8 @@
 
         // Dismiss that one
         mBubbleController.removeBubble(
-                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey()).getEntry(),
+                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey())
+                        .getEntry().getKey(),
                 BubbleController.DISMISS_USER_GESTURE);
 
         // Make sure state changes and collapse happens
@@ -702,7 +705,7 @@
     @Test
     public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
-        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_AGED);
+        mBubbleController.removeBubble(mRow.getEntry().getKey(), BubbleController.DISMISS_AGED);
         verify(mDeleteIntent, never()).send();
     }
 
@@ -710,7 +713,7 @@
     public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         verify(mDeleteIntent, times(1)).send();
     }
 
@@ -813,7 +816,7 @@
 
         // Dismiss the bubble into overflow.
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         assertFalse(mBubbleController.hasBubbles());
 
         boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
@@ -834,7 +837,7 @@
                 mRow.getEntry()));
 
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_NO_LONGER_BUBBLE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_NO_LONGER_BUBBLE);
         assertFalse(mBubbleController.hasBubbles());
 
         boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
@@ -856,12 +859,12 @@
 
         mBubbleData.setMaxOverflowBubbles(1);
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         assertEquals(mBubbleData.getBubbles().size(), 2);
         assertEquals(mBubbleData.getOverflowBubbles().size(), 1);
 
         mBubbleController.removeBubble(
-                mRow2.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow2.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         // Overflow max of 1 is reached; mRow is oldest, so it gets removed
         verify(mNotificationEntryManager, times(1)).performRemoveNotification(
                 mRow.getEntry().getSbn(), REASON_CANCEL);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index eca78ec..8224c88e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -177,7 +177,8 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
 
         // Verify
         verifyUpdateReceived();
@@ -299,12 +300,14 @@
         mBubbleData.setListener(mListener);
 
         mBubbleData.setMaxOverflowBubbles(1);
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
 
         // Overflow max of 1 is reached; A1 is oldest, so it gets removed
-        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA2.getKey(), BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
     }
@@ -325,12 +328,14 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleData.notificationEntryRemoved(mEntryA1.getKey(),
+                BubbleController.DISMISS_NOTIF_CANCEL);
         verifyUpdateReceived();
         assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_GROUP_CANCELLED);
+        mBubbleData.notificationEntryRemoved(mEntryA2.getKey(),
+                BubbleController.DISMISS_GROUP_CANCELLED);
         verifyUpdateReceived();
         assertOverflowChangedTo(ImmutableList.of());
     }
@@ -410,7 +415,8 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA2.getKey(), BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         // TODO: this should fail if things work as I expect them to?
         assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA1);
@@ -430,7 +436,8 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOrderNotChanged();
     }
@@ -449,7 +456,8 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA2.getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
         verifyUpdateReceived();
         assertSelectionChangedTo(mBubbleB2);
     }
@@ -523,7 +531,8 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
 
         // Verify the selection was cleared.
         verifyUpdateReceived();
@@ -623,7 +632,8 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryB2.getKey(), BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleA1);
     }
@@ -647,11 +657,13 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA2.getKey(), BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertSelectionChangedTo(mBubbleB1);
 
-        mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryB1.getKey(), BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertSelectionChangedTo(mBubbleA1);
     }
@@ -765,7 +777,8 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(
+                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertExpandedChangedTo(false);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
index b18d67b..ead95ca1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
@@ -287,7 +287,8 @@
         assertTrue(mBubbleController.hasBubbles());
         verify(mNotifCallback, times(1)).invalidateNotifications(anyString());
 
-        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleController.removeBubble(
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
         verify(mNotifCallback, times(2)).invalidateNotifications(anyString());
     }
@@ -304,7 +305,8 @@
         mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).setSuppressNotification(true);
 
         // Now remove the bubble
-        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleController.removeBubble(
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         assertTrue(mBubbleData.hasOverflowBubbleWithKey(mRow.getEntry().getKey()));
 
         // We don't remove the notification since the bubble is still in overflow.
@@ -324,7 +326,8 @@
         mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).setSuppressNotification(true);
 
         // Now remove the bubble
-        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleController.removeBubble(
+                mRow.getEntry().getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
         assertFalse(mBubbleData.hasOverflowBubbleWithKey(mRow.getEntry().getKey()));
 
         // Since the notif is dismissed and not in overflow, once the bubble is removed,
@@ -504,7 +507,8 @@
 
         // Dismiss currently expanded
         mBubbleController.removeBubble(
-                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey()).getEntry(),
+                mBubbleData.getBubbleInStackWithKey(
+                        stackView.getExpandedBubble().getKey()).getEntry().getKey(),
                 BubbleController.DISMISS_USER_GESTURE);
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
 
@@ -515,7 +519,8 @@
 
         // Dismiss that one
         mBubbleController.removeBubble(
-                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey()).getEntry(),
+                mBubbleData.getBubbleInStackWithKey(
+                        stackView.getExpandedBubble().getKey()).getEntry().getKey(),
                 BubbleController.DISMISS_USER_GESTURE);
 
         // Make sure state changes and collapse happens
@@ -615,7 +620,7 @@
     @Test
     public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
-        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_AGED);
+        mBubbleController.removeBubble(mRow.getEntry().getKey(), BubbleController.DISMISS_AGED);
         verify(mDeleteIntent, never()).send();
     }
 
@@ -623,7 +628,7 @@
     public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         verify(mDeleteIntent, times(1)).send();
     }
 
@@ -691,7 +696,7 @@
 
         // Dismiss the bubble
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
         assertFalse(mBubbleController.hasBubbles());
 
         // Dismiss the notification
@@ -712,7 +717,7 @@
 
         // Dismiss the bubble
         mBubbleController.removeBubble(
-                mRow.getEntry(), BubbleController.DISMISS_NOTIF_CANCEL);
+                mRow.getEntry().getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
         assertFalse(mBubbleController.hasBubbles());
 
         // Dismiss the notification
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
index d49d021..f468192 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
@@ -29,9 +29,9 @@
 class BubblePersistentRepositoryTest : SysuiTestCase() {
 
     private val bubbles = listOf(
-            BubbleEntity(0, "com.example.messenger", "shortcut-1"),
-            BubbleEntity(10, "com.example.chat", "alice and bob"),
-            BubbleEntity(0, "com.example.messenger", "shortcut-2")
+            BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1"),
+            BubbleEntity(10, "com.example.chat", "alice and bob", "key-2"),
+            BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3")
     )
     private lateinit var repository: BubblePersistentRepository
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
index 7acc937..ee48846 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
@@ -28,9 +28,9 @@
 @RunWith(AndroidTestingRunner::class)
 class BubbleVolatileRepositoryTest : SysuiTestCase() {
 
-    private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1")
-    private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob")
-    private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2")
+    private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1")
+    private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob", "k2")
+    private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3")
     private val bubbles = listOf(bubble1, bubble2, bubble3)
 
     private lateinit var repository: BubbleVolatileRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
index ef4580c..79701ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
@@ -31,17 +31,17 @@
 class BubbleXmlHelperTest : SysuiTestCase() {
 
     private val bubbles = listOf(
-        BubbleEntity(0, "com.example.messenger", "shortcut-1"),
-        BubbleEntity(10, "com.example.chat", "alice and bob"),
-        BubbleEntity(0, "com.example.messenger", "shortcut-2")
+        BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1"),
+        BubbleEntity(10, "com.example.chat", "alice and bob", "k2"),
+        BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3")
     )
 
     @Test
     fun testWriteXml() {
         val expectedEntries = """
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" />
-            <bb uid="10" pkg="com.example.chat" sid="alice and bob" />
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" />
+            <bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" />
         """.trimIndent()
         ByteArrayOutputStream().use {
             writeXml(it, bubbles)
@@ -56,9 +56,9 @@
         val src = """
             <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
             <bs>
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" />
-            <bb uid="10" pkg="com.example.chat" sid="alice and bob" />
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" />
+            <bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" />
             </bs>
         """.trimIndent()
         val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt
new file mode 100644
index 0000000..7fe6827
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.controls.dagger
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.ControlsUiController
+import dagger.Lazy
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsComponentTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var controller: ControlsController
+    @Mock
+    private lateinit var uiController: ControlsUiController
+    @Mock
+    private lateinit var listingController: ControlsListingController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testFeatureEnabled() {
+        val component = ControlsComponent(
+                true,
+                Lazy { controller },
+                Lazy { uiController },
+                Lazy { listingController }
+        )
+
+        assertTrue(component.getControlsController().isPresent)
+        assertEquals(controller, component.getControlsController().get())
+        assertTrue(component.getControlsUiController().isPresent)
+        assertEquals(uiController, component.getControlsUiController().get())
+        assertTrue(component.getControlsListingController().isPresent)
+        assertEquals(listingController, component.getControlsListingController().get())
+    }
+
+    @Test
+    fun testFeatureDisabled() {
+        val component = ControlsComponent(
+                false,
+                Lazy { controller },
+                Lazy { uiController },
+                Lazy { listingController }
+        )
+
+        assertFalse(component.getControlsController().isPresent)
+        assertFalse(component.getControlsUiController().isPresent)
+        assertFalse(component.getControlsListingController().isPresent)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt
index 663f011..ee1cc7b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt
@@ -66,6 +66,7 @@
         MockitoAnnotations.initMocks(this)
 
         mContext.setMockPackageManager(packageManager)
+        `when`(packageManager.hasSystemFeature(PackageManager.FEATURE_CONTROLS)).thenReturn(true)
         mContext.addMockSystemService(ActivityManager::class.java, activityManager)
 
         receiver = ControlsRequestReceiver()
@@ -145,6 +146,14 @@
         } ?: run { fail("Null start intent") }
     }
 
+    @Test
+    fun testFeatureDisabled_activityNotStarted() {
+        `when`(packageManager.hasSystemFeature(PackageManager.FEATURE_CONTROLS)).thenReturn(false)
+        receiver.onReceive(wrapper, intent)
+
+        assertNull(wrapper.intent)
+    }
+
     class MyWrapper(context: Context) : ContextWrapper(context) {
         var intent: Intent? = null
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
index 487452b..3254633 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
@@ -59,6 +59,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.controls.controller.ControlsController;
+import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.controls.management.ControlsListingController;
 import com.android.systemui.controls.ui.ControlsUiController;
 import com.android.systemui.model.SysUiState;
@@ -121,6 +122,7 @@
     @Mock GlobalActionsPanelPlugin.PanelViewController mWalletController;
     @Mock private Handler mHandler;
     @Mock private CurrentUserContextTracker mCurrentUserContextTracker;
+    private ControlsComponent mControlsComponent;
 
     private TestableLooper mTestableLooper;
 
@@ -132,6 +134,13 @@
 
         when(mRingerModeTracker.getRingerMode()).thenReturn(mRingerModeLiveData);
         when(mCurrentUserContextTracker.getCurrentUserContext()).thenReturn(mContext);
+        mControlsComponent = new ControlsComponent(
+                true,
+                () -> mControlsController,
+                () -> mControlsUiController,
+                () -> mControlsListingController
+        );
+
         mGlobalActionsDialog = new GlobalActionsDialog(mContext,
                 mWindowManagerFuncs,
                 mAudioManager,
@@ -156,15 +165,13 @@
                 mColorExtractor,
                 mStatusBarService,
                 mNotificationShadeWindowController,
-                mControlsUiController,
                 mWindowManager,
                 mBackgroundExecutor,
-                mControlsListingController,
-                mControlsController,
                 mUiEventLogger,
                 mRingerModeTracker,
                 mSysUiState,
                 mHandler,
+                mControlsComponent,
                 mCurrentUserContextTracker
         );
         mGlobalActionsDialog.setZeroDialogPressDelayForTesting();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
index eb43b81..f38c722 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
@@ -18,10 +18,13 @@
 
 import static android.view.WindowInsets.Type.ime;
 
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.os.PowerManager;
 import android.os.SystemClock;
 import android.view.View;
 import android.view.WindowInsets;
@@ -54,16 +57,15 @@
      * doesn't interfere with the IME, i.e. soft-keyboard state.
      */
     @Test
-    public void testGlobalActions_doesntStealImeControl() {
+    public void testGlobalActions_doesntStealImeControl() throws Exception {
+        turnScreenOn();
         final TestActivity activity = mActivityTestRule.launchActivity(null);
 
-        activity.waitFor(() -> activity.mHasFocus && activity.mControlsIme && activity.mImeVisible);
+        waitUntil("Ime is visible", activity::isImeVisible);
 
-        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
-                "input keyevent --longpress POWER"
-        );
+        executeShellCommand("input keyevent --longpress POWER");
 
-        activity.waitFor(() -> !activity.mHasFocus);
+        waitUntil("activity loses focus", () -> !activity.mHasFocus);
         // Give the dialog time to animate in, and steal IME focus. Unfortunately, there's currently
         // no better way to wait for this.
         SystemClock.sleep(TimeUnit.SECONDS.toMillis(2));
@@ -76,7 +78,39 @@
         });
     }
 
-    /** Like Instrumentation.runOnMainThread(), but forwards AssertionErrors to the caller. */
+    private void turnScreenOn() throws Exception {
+        PowerManager powerManager = mContext.getSystemService(PowerManager.class);
+        assertNotNull(powerManager);
+        if (powerManager.isInteractive()) {
+            return;
+        }
+        executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        waitUntil("Device not interactive", powerManager::isInteractive);
+        executeShellCommand("am wait-for-broadcast-idle");
+    }
+
+    private static void waitUntil(String message, BooleanSupplier predicate)
+            throws Exception {
+        int sleep = 125;
+        final long timeout = SystemClock.uptimeMillis() + 10_000;  // 10 second timeout
+        while (SystemClock.uptimeMillis() < timeout) {
+            if (predicate.getAsBoolean()) {
+                return; // okay
+            }
+            Thread.sleep(sleep);
+            sleep *= 5;
+            sleep = Math.min(2000, sleep);
+        }
+        fail(message);
+    }
+
+    private static void executeShellCommand(String cmd) {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd);
+    }
+
+    /**
+     * Like Instrumentation.runOnMainThread(), but forwards AssertionErrors to the caller.
+     */
     private static void runAssertionOnMainThread(Runnable r) {
         AssertionError[] t = new AssertionError[1];
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
@@ -96,7 +130,6 @@
             WindowInsetsController.OnControllableInsetsChangedListener,
             View.OnApplyWindowInsetsListener {
 
-        private EditText mContent;
         boolean mHasFocus;
         boolean mControlsIme;
         boolean mImeVisible;
@@ -105,13 +138,13 @@
         protected void onCreate(@Nullable Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
 
-            mContent = new EditText(this);
-            mContent.setCursorVisible(false);  // Otherwise, main thread doesn't go idle.
-            setContentView(mContent);
-            mContent.requestFocus();
+            EditText content = new EditText(this);
+            content.setCursorVisible(false);  // Otherwise, main thread doesn't go idle.
+            setContentView(content);
+            content.requestFocus();
 
             getWindow().getDecorView().setOnApplyWindowInsetsListener(this);
-            WindowInsetsController wic = mContent.getWindowInsetsController();
+            WindowInsetsController wic = content.getWindowInsetsController();
             wic.addOnControllableInsetsChangedListener(this);
             wic.show(ime());
         }
@@ -133,16 +166,8 @@
             }
         }
 
-        void waitFor(BooleanSupplier condition) {
-            synchronized (this) {
-                while (!condition.getAsBoolean()) {
-                    try {
-                        wait(TimeUnit.SECONDS.toMillis(5));
-                    } catch (InterruptedException e) {
-                        throw new RuntimeException(e);
-                    }
-                }
-            }
+        boolean isImeVisible() {
+            return mHasFocus && mControlsIme && mImeVisible;
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimatorTest.java
index 1654a5442..cdef49d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimatorTest.java
@@ -19,18 +19,14 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
-import android.os.RemoteException;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.view.IRemoteAnimationFinishedCallback;
 import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
 import android.view.View;
 
 import com.android.systemui.SysuiTestCase;
@@ -40,8 +36,6 @@
 import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -74,11 +68,9 @@
     private NotificationPanelViewController mNotificationPanelViewController;
     @Rule
     public MockitoRule rule = MockitoJUnit.rule();
-    private FakeExecutor mExecutor;
 
     @Before
     public void setUp() throws Exception {
-        mExecutor = new FakeExecutor(new FakeSystemClock());
         when(mNotificationShadeWindowViewController.getView())
                 .thenReturn(mNotificationShadeWindowView);
         when(mNotificationShadeWindowView.getResources()).thenReturn(mContext.getResources());
@@ -88,8 +80,8 @@
                 mCallback,
                 mNotificationPanelViewController,
                 mNotificationShadeDepthController,
-                mNotificationContainer,
-                mExecutor);
+                mNotificationContainer);
+
     }
 
     @Test
@@ -121,29 +113,6 @@
         verify(mCallback).onExpandAnimationTimedOut();
     }
 
-    @Test
-    public void testRowLinkBrokenOnAnimationStartFail() throws RemoteException {
-        ActivityLaunchAnimator.AnimationRunner runner = mLaunchAnimator.new AnimationRunner(mRow,
-                mExecutor);
-        // WHEN onAnimationStart with no valid remote target
-        runner.onAnimationStart(new RemoteAnimationTarget[0], new RemoteAnimationTarget[0],
-                mock(IRemoteAnimationFinishedCallback.class));
-        mExecutor.runAllReady();
-        // THEN the row is nulled out so that it won't be retained
-        Assert.assertTrue("The row should be null", runner.getRow() == null);
-    }
-
-    @Test
-    public void testRowLinkBrokenOnAnimationCancelled() throws RemoteException {
-        ActivityLaunchAnimator.AnimationRunner runner = mLaunchAnimator.new AnimationRunner(mRow,
-                mExecutor);
-        // WHEN onAnimationCancelled
-        runner.onAnimationCancelled();
-        mExecutor.runAllReady();
-        // THEN the row is nulled out so that it won't be retained
-        Assert.assertTrue("The row should be null", runner.getRow() == null);
-    }
-
     private void executePostsImmediately(View view) {
         doAnswer((i) -> {
             Runnable run = i.getArgument(0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
index 4b21ef2..0272028 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
@@ -60,6 +60,7 @@
 import android.content.pm.ShortcutManager;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.os.Handler;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -72,7 +73,6 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.settingslib.notification.ConversationIconFactory;
-import com.android.systemui.Dependency;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
@@ -153,13 +153,14 @@
     private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider = () -> mBuilder;
     @Mock
     private Notification.BubbleMetadata mBubbleMetadata;
+    private Handler mTestHandler;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mTestableLooper = TestableLooper.get(this);
 
-        mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
+        mTestHandler = new Handler(mTestableLooper.getLooper());
         mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
         mDependency.injectTestDependency(BubbleController.class, mBubbleController);
         mDependency.injectTestDependency(ShadeController.class, mShadeController);
@@ -253,7 +254,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon);
         assertEquals(mIconDrawable, view.getDrawable());
     }
@@ -275,7 +278,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
         assertTrue(textView.getText().toString().contains("App Name"));
         assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
@@ -324,7 +329,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
         assertTrue(textView.getText().toString().contains(group.getName()));
         assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
@@ -348,7 +355,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
         assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
         assertEquals(GONE, textView.getVisibility());
@@ -371,7 +380,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
         assertEquals(GONE, nameView.getVisibility());
     }
@@ -404,7 +415,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
         assertEquals(VISIBLE, nameView.getVisibility());
         assertTrue(nameView.getText().toString().contains("Proxied"));
@@ -430,7 +443,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
         settingsButton.performClick();
@@ -454,7 +469,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
         assertTrue(settingsButton.getVisibility() != View.VISIBLE);
     }
@@ -479,7 +496,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                false);
+                false,
+                mTestHandler,
+                mTestHandler);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
         assertTrue(settingsButton.getVisibility() != View.VISIBLE);
     }
@@ -502,7 +521,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         View view = mNotificationInfo.findViewById(R.id.silence);
         assertThat(view.isSelected()).isTrue();
     }
@@ -528,7 +549,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         View view = mNotificationInfo.findViewById(R.id.default_behavior);
         assertThat(view.isSelected()).isTrue();
         assertThat(((TextView) view.findViewById(R.id.default_summary)).getText()).isEqualTo(
@@ -557,7 +580,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
         View view = mNotificationInfo.findViewById(R.id.default_behavior);
         assertThat(view.isSelected()).isTrue();
         assertThat(((TextView) view.findViewById(R.id.default_summary)).getText()).isEqualTo(
@@ -585,7 +610,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         View fave = mNotificationInfo.findViewById(R.id.priority);
         fave.performClick();
@@ -627,7 +654,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         mNotificationInfo.findViewById(R.id.default_behavior).performClick();
         mTestableLooper.processAllMessages();
@@ -668,7 +697,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         View silence = mNotificationInfo.findViewById(R.id.silence);
 
@@ -710,7 +741,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         View fave = mNotificationInfo.findViewById(R.id.priority);
         fave.performClick();
@@ -745,7 +778,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         View fave = mNotificationInfo.findViewById(R.id.priority);
         fave.performClick();
@@ -778,7 +813,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         mNotificationInfo.findViewById(R.id.default_behavior).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -812,7 +849,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         mNotificationInfo.findViewById(R.id.default_behavior).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -846,7 +885,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         mNotificationInfo.findViewById(R.id.default_behavior).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -879,7 +920,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         View silence = mNotificationInfo.findViewById(R.id.silence);
         silence.performClick();
@@ -911,7 +954,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         verify(mMockINotificationManager, times(1)).createConversationNotificationChannelForPackage(
                 anyString(), anyInt(), anyString(), any(), eq(CONVERSATION_ID));
@@ -934,7 +979,9 @@
                 mIconFactory,
                 mContext,
                 mBuilderProvider,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         verify(mMockINotificationManager, never()).createConversationNotificationChannelForPackage(
                 anyString(), anyInt(), anyString(), any(), eq(CONVERSATION_ID));
@@ -967,7 +1014,9 @@
                 mIconFactory,
                 mContext,
                 () -> b,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         // WHEN user clicks "priority"
         mNotificationInfo.setSelectedAction(NotificationConversationInfo.ACTION_FAVORITE);
@@ -1001,7 +1050,9 @@
                 mIconFactory,
                 mContext,
                 () -> b,
-                true);
+                true,
+                mTestHandler,
+                mTestHandler);
 
         // WHEN user clicks "priority"
         mNotificationInfo.setSelectedAction(NotificationConversationInfo.ACTION_FAVORITE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index eeb912e..da7d249 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -144,7 +144,7 @@
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false);
 
         mGutsManager = new NotificationGutsManager(mContext, mVisualStabilityManager,
-                () -> mStatusBar, mHandler, mAccessibilityManager, mHighPriorityProvider,
+                () -> mStatusBar, mHandler, mHandler, mAccessibilityManager, mHighPriorityProvider,
                 mINotificationManager, mLauncherApps, mShortcutManager,
                 mChannelEditorDialogController, mContextTracker, mProvider);
         mGutsManager.setUpWithPresenter(mPresenter, mStackScroller,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 27cbb03..5a08c9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -251,7 +251,6 @@
     @Mock private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
     private ShadeController mShadeController;
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
-    private FakeExecutor mMainExecutor = new FakeExecutor(new FakeSystemClock());
     private InitController mInitController = new InitController();
 
     @Before
@@ -354,7 +353,6 @@
                 new DisplayMetrics(),
                 mMetricsLogger,
                 mUiBgExecutor,
-                mMainExecutor,
                 mNotificationMediaManager,
                 mLockscreenUserManager,
                 mRemoteInputManager,
diff --git a/packages/Tethering/tests/integration/Android.bp b/packages/Tethering/tests/integration/Android.bp
index 3305ed0..ed69b7d 100644
--- a/packages/Tethering/tests/integration/Android.bp
+++ b/packages/Tethering/tests/integration/Android.bp
@@ -63,7 +63,6 @@
 // NetworkStackTests.
 android_test {
     name: "TetheringCoverageTests",
-    certificate: "platform",
     platform_apis: true,
     test_suites: ["device-tests", "mts"],
     test_config: "AndroidTest_Coverage.xml",
diff --git a/packages/Tethering/tests/unit/Android.bp b/packages/Tethering/tests/unit/Android.bp
index 08cfb30..e00435b 100644
--- a/packages/Tethering/tests/unit/Android.bp
+++ b/packages/Tethering/tests/unit/Android.bp
@@ -83,7 +83,7 @@
 
 android_test {
     name: "TetheringTests",
-    certificate: "platform",
+    platform_apis: true,
     test_suites: [
         "device-tests",
         "mts",
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
index 103151d..26e85be 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
@@ -51,7 +51,7 @@
  */
 public class AppPredictionPerUserService extends
         AbstractPerUserSystemService<AppPredictionPerUserService, AppPredictionManagerService>
-             implements RemoteAppPredictionService.RemoteAppPredictionServiceCallbacks {
+        implements RemoteAppPredictionService.RemoteAppPredictionServiceCallbacks {
 
     private static final String TAG = AppPredictionPerUserService.class.getSimpleName();
     private static final String PREDICT_USING_PEOPLE_SERVICE_PREFIX =
@@ -114,8 +114,11 @@
     public void onCreatePredictionSessionLocked(@NonNull AppPredictionContext context,
             @NonNull AppPredictionSessionId sessionId) {
         if (!mSessionInfos.containsKey(sessionId)) {
+            // TODO(b/157500121): remove below forceUsingPeopleService logic after testing
+            //  PeopleService for 2 weeks on Droidfood.
+            final boolean forceUsingPeopleService = context.getUiSurface().equals("share");
             mSessionInfos.put(sessionId, new AppPredictionSessionInfo(sessionId, context,
-                    DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
+                    forceUsingPeopleService || DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
                             PREDICT_USING_PEOPLE_SERVICE_PREFIX + context.getUiSurface(), false),
                     this::removeAppPredictionSessionInfo));
         }
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 7c61ba2..1e9a9d8 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -3102,30 +3102,24 @@
     }
 
     private void notifyDataStallSuspected(DataStallReportParcelable p, int netId) {
+        log("Data stall detected with methods: " + p.detectionMethod);
+
         final PersistableBundle extras = new PersistableBundle();
-        switch (p.detectionMethod) {
-            case DETECTION_METHOD_DNS_EVENTS:
-                extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, p.dnsConsecutiveTimeouts);
-                break;
-            case DETECTION_METHOD_TCP_METRICS:
-                extras.putInt(KEY_TCP_PACKET_FAIL_RATE, p.tcpPacketFailRate);
-                extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS,
-                        p.tcpMetricsCollectionPeriodMillis);
-                break;
-            default:
-                // TODO(b/156294356): update for new data stall detection methods
-                log("Unknown data stall detection method, ignoring: " + p.detectionMethod);
-                return;
+        int detectionMethod = 0;
+        if (hasDataStallDetectionMethod(p, DETECTION_METHOD_DNS_EVENTS)) {
+            extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, p.dnsConsecutiveTimeouts);
+            detectionMethod |= DETECTION_METHOD_DNS_EVENTS;
+        }
+        if (hasDataStallDetectionMethod(p, DETECTION_METHOD_TCP_METRICS)) {
+            extras.putInt(KEY_TCP_PACKET_FAIL_RATE, p.tcpPacketFailRate);
+            extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS,
+                    p.tcpMetricsCollectionPeriodMillis);
+            detectionMethod |= DETECTION_METHOD_TCP_METRICS;
         }
 
-        notifyDataStallSuspected(p.detectionMethod, netId, p.timestampMillis, extras);
-    }
-
-    private void notifyDataStallSuspected(int detectionMethod, int netId, long timestampMillis,
-            @NonNull PersistableBundle extras) {
         final Message msg = mConnectivityDiagnosticsHandler.obtainMessage(
                 ConnectivityDiagnosticsHandler.EVENT_DATA_STALL_SUSPECTED, detectionMethod, netId,
-                timestampMillis);
+                p.timestampMillis);
         msg.setData(new Bundle(extras));
 
         // NetworkStateTrackerHandler currently doesn't take any actions based on data
@@ -3134,6 +3128,10 @@
         mConnectivityDiagnosticsHandler.sendMessage(msg);
     }
 
+    private boolean hasDataStallDetectionMethod(DataStallReportParcelable p, int detectionMethod) {
+        return (p.detectionMethod & detectionMethod) != 0;
+    }
+
     private boolean networkRequiresPrivateDnsValidation(NetworkAgentInfo nai) {
         return isPrivateDnsValidationRequired(nai.networkCapabilities);
     }
@@ -8189,6 +8187,19 @@
                 + "creators");
         }
 
-        notifyDataStallSuspected(detectionMethod, network.netId, timestampMillis, extras);
+        final DataStallReportParcelable p = new DataStallReportParcelable();
+        p.timestampMillis = timestampMillis;
+        p.detectionMethod = detectionMethod;
+
+        if (hasDataStallDetectionMethod(p, DETECTION_METHOD_DNS_EVENTS)) {
+            p.dnsConsecutiveTimeouts = extras.getInt(KEY_DNS_CONSECUTIVE_TIMEOUTS);
+        }
+        if (hasDataStallDetectionMethod(p, DETECTION_METHOD_TCP_METRICS)) {
+            p.tcpPacketFailRate = extras.getInt(KEY_TCP_PACKET_FAIL_RATE);
+            p.tcpMetricsCollectionPeriodMillis = extras.getInt(
+                    KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS);
+        }
+
+        notifyDataStallSuspected(p, network.netId);
     }
 }
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index e77458c..e303f0d 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -177,9 +177,6 @@
     // 0 if no prune is scheduled.
     @GuardedBy("mLock")
     private long mUptimeAtLastStateSync;
-    // If true, sync explicit health check packages with the ExplicitHealthCheckController.
-    @GuardedBy("mLock")
-    private boolean mSyncRequired = false;
 
     @FunctionalInterface
     @VisibleForTesting
@@ -255,7 +252,6 @@
      */
     public void registerHealthObserver(PackageHealthObserver observer) {
         synchronized (mLock) {
-            mSyncRequired = true;
             ObserverInternal internalObserver = mAllObservers.get(observer.getName());
             if (internalObserver != null) {
                 internalObserver.registeredObserver = observer;
@@ -642,7 +638,7 @@
         synchronized (mLock) {
             if (mIsPackagesReady) {
                 Set<String> packages = getPackagesPendingHealthChecksLocked();
-                if (!packages.equals(mRequestedHealthCheckPackages) || mSyncRequired) {
+                if (!packages.equals(mRequestedHealthCheckPackages) || packages.isEmpty()) {
                     syncRequired = true;
                     mRequestedHealthCheckPackages = packages;
                 }
@@ -654,7 +650,6 @@
             Slog.i(TAG, "Syncing health check requests for packages: "
                     + mRequestedHealthCheckPackages);
             mHealthCheckController.syncRequests(mRequestedHealthCheckPackages);
-            mSyncRequired = false;
         }
     }
 
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index f3c7874..a33817c 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -65,6 +65,7 @@
 import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkProvider;
+import android.net.NetworkRequest;
 import android.net.RouteInfo;
 import android.net.UidRange;
 import android.net.VpnManager;
@@ -2225,12 +2226,27 @@
 
         @Override
         public void run() {
-            // Explicitly use only the network that ConnectivityService thinks is the "best." In
-            // other words, only ever use the currently selected default network. This does mean
-            // that in both onLost() and onConnected(), any old sessions MUST be torn down. This
-            // does NOT include VPNs.
+            // Unless the profile is restricted to test networks, explicitly use only the network
+            // that ConnectivityService thinks is the "best." In other words, only ever use the
+            // currently selected default network. This does mean that in both onLost() and
+            // onConnected(), any old sessions MUST be torn down. This does NOT include VPNs.
+            //
+            // When restricted to test networks, select any network with TRANSPORT_TEST. Since the
+            // creator of the profile and the test network creator both have MANAGE_TEST_NETWORKS,
+            // this is considered safe.
             final ConnectivityManager cm = ConnectivityManager.from(mContext);
-            cm.requestNetwork(cm.getDefaultRequest(), mNetworkCallback);
+            final NetworkRequest req;
+
+            if (mProfile.isRestrictedToTestNetworks()) {
+                req = new NetworkRequest.Builder()
+                        .clearCapabilities()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                        .build();
+            } else {
+                req = cm.getDefaultRequest();
+            }
+
+            cm.requestNetwork(req, mNetworkCallback);
         }
 
         private boolean isActiveNetwork(@Nullable Network network) {
@@ -2868,6 +2884,11 @@
         verifyCallingUidAndPackage(packageName);
         enforceNotRestrictedUser();
 
+        if (profile.isRestrictedToTestNetworks) {
+            mContext.enforceCallingPermission(Manifest.permission.MANAGE_TEST_NETWORKS,
+                    "Test-mode profiles require the MANAGE_TEST_NETWORKS permission");
+        }
+
         final byte[] encodedProfile = profile.encode();
         if (encodedProfile.length > MAX_VPN_PROFILE_SIZE_BYTES) {
             throw new IllegalArgumentException("Profile too big");
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
index b9cd43d..787f7f3 100644
--- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -28,7 +28,6 @@
 import android.util.Slog;
 import android.util.Spline;
 
-import com.android.internal.BrightnessSynchronizer;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 import com.android.server.display.utils.Plog;
@@ -347,10 +346,11 @@
         }
     }
 
+    // Normalize entire brightness range to 0 - 1.
     protected static float normalizeAbsoluteBrightness(int brightness) {
-        return BrightnessSynchronizer.brightnessIntToFloat(brightness,
-                PowerManager.BRIGHTNESS_OFF + 1, PowerManager.BRIGHTNESS_ON,
-                PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX);
+        brightness = MathUtils.constrain(brightness,
+                PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON);
+        return (float) brightness / PowerManager.BRIGHTNESS_ON;
     }
 
     private Pair<float[], float[]> insertControlPoint(
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 9d56d81..77b030f 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -85,7 +85,8 @@
     private static final int XML_VERSION = 2;
     /** What version to check to do the upgrade for bubbles. */
     private static final int XML_VERSION_BUBBLES_UPGRADE = 1;
-    private static final int UNKNOWN_UID = UserHandle.USER_NULL;
+    @VisibleForTesting
+    static final int UNKNOWN_UID = UserHandle.USER_NULL;
     private static final String NON_BLOCKABLE_CHANNEL_DELIM = ":";
 
     @VisibleForTesting
@@ -224,7 +225,7 @@
                             }
                             boolean skipWarningLogged = false;
                             boolean hasSAWPermission = false;
-                            if (upgradeForBubbles) {
+                            if (upgradeForBubbles && uid != UNKNOWN_UID) {
                                 hasSAWPermission = mAppOps.noteOpNoThrow(
                                         OP_SYSTEM_ALERT_WINDOW, uid, name, null,
                                         "check-notif-bubble") == AppOpsManager.MODE_ALLOWED;
diff --git a/services/core/java/com/android/server/notification/ShortcutHelper.java b/services/core/java/com/android/server/notification/ShortcutHelper.java
index 0fa522c..ee02e3f 100644
--- a/services/core/java/com/android/server/notification/ShortcutHelper.java
+++ b/services/core/java/com/android/server/notification/ShortcutHelper.java
@@ -208,7 +208,7 @@
         if (shortcutInfo.isLongLived() && !shortcutInfo.isCached()) {
             mShortcutServiceInternal.cacheShortcuts(user.getIdentifier(), "android",
                     shortcutInfo.getPackage(), Collections.singletonList(shortcutInfo.getId()),
-                    shortcutInfo.getUserId());
+                    shortcutInfo.getUserId(), ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index c6d08c3..5bbe490 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -18,6 +18,8 @@
 
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+import static android.content.pm.LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS;
+import static android.content.pm.LauncherApps.FLAG_CACHE_NOTIFICATION_SHORTCUTS;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -78,6 +80,7 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
+import com.android.internal.util.Preconditions;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.pm.parsing.pkg.AndroidPackage;
@@ -780,26 +783,28 @@
 
         @Override
         public void cacheShortcuts(String callingPackage, String packageName, List<String> ids,
-                UserHandle targetUser) {
+                UserHandle targetUser, int cacheFlags) {
             ensureStrictAccessShortcutsPermission(callingPackage);
             if (!canAccessProfile(targetUser.getIdentifier(), "Cannot cache shortcuts")) {
                 return;
             }
 
-            mShortcutServiceInternal.cacheShortcuts(getCallingUserId(),
-                    callingPackage, packageName, ids, targetUser.getIdentifier());
+            mShortcutServiceInternal.cacheShortcuts(
+                    getCallingUserId(), callingPackage, packageName, ids,
+                    targetUser.getIdentifier(), toShortcutsCacheFlags(cacheFlags));
         }
 
         @Override
         public void uncacheShortcuts(String callingPackage, String packageName, List<String> ids,
-                UserHandle targetUser) {
+                UserHandle targetUser, int cacheFlags) {
             ensureStrictAccessShortcutsPermission(callingPackage);
             if (!canAccessProfile(targetUser.getIdentifier(), "Cannot uncache shortcuts")) {
                 return;
             }
 
-            mShortcutServiceInternal.uncacheShortcuts(getCallingUserId(),
-                    callingPackage, packageName, ids, targetUser.getIdentifier());
+            mShortcutServiceInternal.uncacheShortcuts(
+                    getCallingUserId(), callingPackage, packageName, ids,
+                    targetUser.getIdentifier(), toShortcutsCacheFlags(cacheFlags));
         }
 
         @Override
@@ -1058,6 +1063,18 @@
                     user.getIdentifier(), debugMsg, false);
         }
 
+        private int toShortcutsCacheFlags(int cacheFlags) {
+            int ret = 0;
+            if (cacheFlags == FLAG_CACHE_NOTIFICATION_SHORTCUTS) {
+                ret = ShortcutInfo.FLAG_CACHED_NOTIFICATIONS;
+            } else if (cacheFlags == FLAG_CACHE_BUBBLE_SHORTCUTS) {
+                ret = ShortcutInfo.FLAG_CACHED_BUBBLES;
+            }
+            Preconditions.checkArgumentPositive(ret, "Invalid cache owner");
+
+            return ret;
+        }
+
         @VisibleForTesting
         void postToPackageMonitorHandler(Runnable r) {
             mCallbackHandler.post(r);
@@ -1154,7 +1171,7 @@
                 final int shortcutFlags = (matchDynamic ? ShortcutInfo.FLAG_DYNAMIC : 0)
                         | (matchPinned ? ShortcutInfo.FLAG_PINNED : 0)
                         | (matchManifest ? ShortcutInfo.FLAG_MANIFEST : 0)
-                        | (matchCached ? ShortcutInfo.FLAG_CACHED : 0);
+                        | (matchCached ? ShortcutInfo.FLAG_CACHED_ALL : 0);
 
                 for (int i = 0; i < shortcuts.size(); i++) {
                     final ShortcutInfo si = shortcuts.get(i);
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 1642607..9e27f65 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -287,7 +287,7 @@
         if (shortcut != null) {
             mShortcutUser.mService.removeIconLocked(shortcut);
             shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
-                    | ShortcutInfo.FLAG_MANIFEST | ShortcutInfo.FLAG_CACHED);
+                    | ShortcutInfo.FLAG_MANIFEST | ShortcutInfo.FLAG_CACHED_ALL);
         }
         return shortcut;
     }
@@ -323,36 +323,18 @@
         newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
 
         final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
-
-        final boolean replaced;
-
-        final boolean wasPinned;
-        final boolean wasCached;
-
-        if (oldShortcut == null) {
-            replaced = false;
-            wasPinned = false;
-            wasCached = false;
-        } else {
+        if (oldShortcut != null) {
             // It's an update case.
             // Make sure the target is updatable. (i.e. should be mutable.)
             oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false);
-            replaced = true;
 
-            wasPinned = oldShortcut.isPinned();
-            wasCached = oldShortcut.isCached();
-        }
-
-        // If it was originally pinned, the new one should be pinned too.
-        if (wasPinned) {
-            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
-        }
-        if (wasCached) {
-            newShortcut.addFlags(ShortcutInfo.FLAG_CACHED);
+            // If it was originally pinned or cached, the new one should be pinned or cached too.
+            newShortcut.addFlags(oldShortcut.getFlags()
+                    & (ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_CACHED_ALL));
         }
 
         forceReplaceShortcutInner(newShortcut);
-        return replaced;
+        return oldShortcut != null;
     }
 
     /**
@@ -373,9 +355,6 @@
 
         changedShortcuts.clear();
         final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
-        boolean wasPinned = false;
-        boolean wasCached = false;
-
         boolean deleted = false;
 
         if (oldShortcut == null) {
@@ -408,16 +387,9 @@
             // Make sure the target is updatable. (i.e. should be mutable.)
             oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false);
 
-            wasPinned = oldShortcut.isPinned();
-            wasCached = oldShortcut.isCached();
-        }
-
-        // If it was originally pinned or cached, the new one should be pinned or cached too.
-        if (wasPinned) {
-            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
-        }
-        if (wasCached) {
-            newShortcut.addFlags(ShortcutInfo.FLAG_CACHED);
+            // If it was originally pinned or cached, the new one should be pinned or cached too.
+            newShortcut.addFlags(oldShortcut.getFlags()
+                    & (ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_CACHED_ALL));
         }
 
         forceReplaceShortcutInner(newShortcut);
@@ -511,7 +483,7 @@
     public ShortcutInfo deleteLongLivedWithId(@NonNull String shortcutId, boolean ignoreInvisible) {
         final ShortcutInfo shortcut = mShortcuts.get(shortcutId);
         if (shortcut != null) {
-            shortcut.clearFlags(ShortcutInfo.FLAG_CACHED);
+            shortcut.clearFlags(ShortcutInfo.FLAG_CACHED_ALL);
         }
         return deleteOrDisableWithId(
                 shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible,
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 3732b47..3ec1397 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -2407,7 +2407,7 @@
             final int shortcutFlags = (matchDynamic ? ShortcutInfo.FLAG_DYNAMIC : 0)
                     | (matchPinned ? ShortcutInfo.FLAG_PINNED : 0)
                     | (matchManifest ? ShortcutInfo.FLAG_MANIFEST : 0)
-                    | (matchCached ? ShortcutInfo.FLAG_CACHED : 0);
+                    | (matchCached ? ShortcutInfo.FLAG_CACHED_ALL : 0);
 
             return getShortcutsWithQueryLocked(
                     packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
@@ -3045,17 +3045,17 @@
         @Override
         public void cacheShortcuts(int launcherUserId,
                 @NonNull String callingPackage, @NonNull String packageName,
-                @NonNull List<String> shortcutIds, int userId) {
+                @NonNull List<String> shortcutIds, int userId, int cacheFlags) {
             updateCachedShortcutsInternal(launcherUserId, callingPackage, packageName, shortcutIds,
-                    userId, /* doCache= */ true);
+                    userId, cacheFlags, /* doCache= */ true);
         }
 
         @Override
         public void uncacheShortcuts(int launcherUserId,
                 @NonNull String callingPackage, @NonNull String packageName,
-                @NonNull List<String> shortcutIds, int userId) {
+                @NonNull List<String> shortcutIds, int userId, int cacheFlags) {
             updateCachedShortcutsInternal(launcherUserId, callingPackage, packageName, shortcutIds,
-                    userId, /* doCache= */ false);
+                    userId, cacheFlags, /* doCache= */ false);
         }
 
         @Override
@@ -3079,10 +3079,12 @@
 
         private void updateCachedShortcutsInternal(int launcherUserId,
                 @NonNull String callingPackage, @NonNull String packageName,
-                @NonNull List<String> shortcutIds, int userId, boolean doCache) {
+                @NonNull List<String> shortcutIds, int userId, int cacheFlags, boolean doCache) {
             // Calling permission must be checked by LauncherAppsImpl.
             Preconditions.checkStringNotEmpty(packageName, "packageName");
             Objects.requireNonNull(shortcutIds, "shortcutIds");
+            Preconditions.checkState(
+                    (cacheFlags & ShortcutInfo.FLAG_CACHED_ALL) != 0, "invalid cacheFlags");
 
             List<ShortcutInfo> changedShortcuts = null;
             List<ShortcutInfo> removedShortcuts = null;
@@ -3101,13 +3103,13 @@
                 for (int i = 0; i < idSize; i++) {
                     final String id = Preconditions.checkStringNotEmpty(shortcutIds.get(i));
                     final ShortcutInfo si = sp.findShortcutById(id);
-                    if (si == null || doCache == si.isCached()) {
+                    if (si == null || doCache == si.hasFlags(cacheFlags)) {
                         continue;
                     }
 
                     if (doCache) {
                         if (si.isLongLived()) {
-                            si.addFlags(ShortcutInfo.FLAG_CACHED);
+                            si.addFlags(cacheFlags);
                             if (changedShortcuts == null) {
                                 changedShortcuts = new ArrayList<>(1);
                             }
@@ -3118,9 +3120,8 @@
                         }
                     } else {
                         ShortcutInfo removed = null;
-                        if (si.isDynamic()) {
-                            si.clearFlags(ShortcutInfo.FLAG_CACHED);
-                        } else {
+                        si.clearFlags(cacheFlags);
+                        if (!si.isDynamic() && !si.isCached()) {
                             removed = sp.deleteLongLivedWithId(id, /*ignoreInvisible=*/ true);
                         }
                         if (removed != null) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 29428a2..b0e3ecb 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -3507,7 +3507,7 @@
                     Slog.w(LOG_TAG, "could not start pre-created user " + userId, e);
                 }
             } else {
-                dispatchUserAddedIntent(userInfo);
+                dispatchUserAdded(userInfo);
             }
 
         } finally {
@@ -3568,7 +3568,7 @@
             // Could not read the existing permissions, re-grant them.
             mPm.onNewUserCreated(preCreatedUser.id);
         }
-        dispatchUserAddedIntent(preCreatedUser);
+        dispatchUserAdded(preCreatedUser);
         return preCreatedUser;
     }
 
@@ -3600,7 +3600,7 @@
         return (now > EPOCH_PLUS_30_YEARS) ? now : 0;
     }
 
-    private void dispatchUserAddedIntent(@NonNull UserInfo userInfo) {
+    private void dispatchUserAdded(@NonNull UserInfo userInfo) {
         Intent addedIntent = new Intent(Intent.ACTION_USER_ADDED);
         addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userInfo.id);
         // Also, add the UserHandle for mainline modules which can't use the @hide
@@ -3610,6 +3610,15 @@
                 android.Manifest.permission.MANAGE_USERS);
         MetricsLogger.count(mContext, userInfo.isGuest() ? TRON_GUEST_CREATED
                 : (userInfo.isDemo() ? TRON_DEMO_CREATED : TRON_USER_CREATED), 1);
+
+        if (!userInfo.isProfile()) {
+            // If the user switch hasn't been explicitly toggled on or off by the user, turn it on.
+            if (android.provider.Settings.Global.getString(mContext.getContentResolver(),
+                    android.provider.Settings.Global.USER_SWITCHER_ENABLED) == null) {
+                android.provider.Settings.Global.putInt(mContext.getContentResolver(),
+                        android.provider.Settings.Global.USER_SWITCHER_ENABLED, 1);
+            }
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 1fec8aa..14d043c 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -206,21 +206,9 @@
      */
     private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS =
             Sets.newArraySet(
-                    UserManager.DISALLOW_CONFIG_DATE_TIME,
-                    UserManager.DISALLOW_CAMERA,
-                    UserManager.DISALLOW_BLUETOOTH,
-                    UserManager.DISALLOW_BLUETOOTH_SHARING,
-                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
-                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
-                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
-                    UserManager.DISALLOW_CONFIG_TETHERING,
-                    UserManager.DISALLOW_DATA_ROAMING,
-                    UserManager.DISALLOW_SAFE_BOOT,
-                    UserManager.DISALLOW_SMS,
-                    UserManager.DISALLOW_USB_FILE_TRANSFER,
                     UserManager.DISALLOW_AIRPLANE_MODE,
-                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
-                    UserManager.DISALLOW_UNMUTE_MICROPHONE
+                    UserManager.DISALLOW_CONFIG_DATE_TIME,
+                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS
     );
 
     /**
@@ -236,7 +224,19 @@
                     UserManager.DISALLOW_CONTENT_SUGGESTIONS,
                     UserManager.DISALLOW_DEBUGGING_FEATURES,
                     UserManager.DISALLOW_SHARE_LOCATION,
-                    UserManager.DISALLOW_OUTGOING_CALLS
+                    UserManager.DISALLOW_OUTGOING_CALLS,
+                    UserManager.DISALLOW_CAMERA,
+                    UserManager.DISALLOW_BLUETOOTH,
+                    UserManager.DISALLOW_BLUETOOTH_SHARING,
+                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
+                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
+                    UserManager.DISALLOW_CONFIG_TETHERING,
+                    UserManager.DISALLOW_DATA_ROAMING,
+                    UserManager.DISALLOW_SAFE_BOOT,
+                    UserManager.DISALLOW_SMS,
+                    UserManager.DISALLOW_USB_FILE_TRANSFER,
+                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
+                    UserManager.DISALLOW_UNMUTE_MICROPHONE
     );
 
     /**
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 29c1243..0b3254f 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -179,6 +179,7 @@
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
 
 /**
  * SystemService containing PullAtomCallbacks that are registered with statsd.
@@ -325,6 +326,7 @@
                     case FrameworkStatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG:
                     case FrameworkStatsLog.MOBILE_BYTES_TRANSFER:
                     case FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG:
+                    case FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED:
                         return pullDataBytesTransfer(atomTag, data);
                     case FrameworkStatsLog.BLUETOOTH_BYTES_TRANSFER:
                         return pullBluetoothBytesTransfer(atomTag, data);
@@ -641,11 +643,14 @@
                 collectNetworkStatsSnapshotForAtom(FrameworkStatsLog.MOBILE_BYTES_TRANSFER));
         mNetworkStatsBaselines.addAll(collectNetworkStatsSnapshotForAtom(
                 FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG));
+        mNetworkStatsBaselines.addAll(collectNetworkStatsSnapshotForAtom(
+                FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED));
 
         registerWifiBytesTransfer();
         registerWifiBytesTransferBackground();
         registerMobileBytesTransfer();
         registerMobileBytesTransferBackground();
+        registerBytesTransferByTagAndMetered();
     }
 
     /**
@@ -787,50 +792,94 @@
     private static class NetworkStatsExt {
         @NonNull
         public final NetworkStats stats;
-        public final int transport;
-        public final boolean withFgbg;
+        public final int[] transports;
+        public final boolean slicedByFgbg;
+        public final boolean slicedByTag;
+        public final boolean slicedByMetered;
 
-        NetworkStatsExt(@NonNull NetworkStats stats, int transport, boolean withFgbg) {
+        NetworkStatsExt(@NonNull NetworkStats stats, int[] transports, boolean slicedByFgbg) {
+            this(stats, transports, slicedByFgbg, /*slicedByTag=*/false, /*slicedByMetered=*/false);
+        }
+
+        NetworkStatsExt(@NonNull NetworkStats stats, int[] transports, boolean slicedByFgbg,
+                boolean slicedByTag, boolean slicedByMetered) {
             this.stats = stats;
-            this.transport = transport;
-            this.withFgbg = withFgbg;
+
+            // Sort transports array so that we can test for equality without considering order.
+            this.transports = Arrays.copyOf(transports, transports.length);
+            Arrays.sort(this.transports);
+
+            this.slicedByFgbg = slicedByFgbg;
+            this.slicedByTag = slicedByTag;
+            this.slicedByMetered = slicedByMetered;
+        }
+
+        public boolean hasSameSlicing(@NonNull NetworkStatsExt other) {
+            return Arrays.equals(transports, other.transports) && slicedByFgbg == other.slicedByFgbg
+                    && slicedByTag == other.slicedByTag && slicedByMetered == other.slicedByMetered;
         }
     }
 
     @NonNull
     private List<NetworkStatsExt> collectNetworkStatsSnapshotForAtom(int atomTag) {
+        List<NetworkStatsExt> ret = new ArrayList<>();
         switch(atomTag) {
-            case FrameworkStatsLog.WIFI_BYTES_TRANSFER:
-                return collectUidNetworkStatsSnapshot(TRANSPORT_WIFI, /*withFgbg=*/false);
-            case  FrameworkStatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG:
-                return collectUidNetworkStatsSnapshot(TRANSPORT_WIFI, /*withFgbg=*/true);
-            case FrameworkStatsLog.MOBILE_BYTES_TRANSFER:
-                return collectUidNetworkStatsSnapshot(TRANSPORT_CELLULAR, /*withFgbg=*/false);
-            case FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG:
-                return collectUidNetworkStatsSnapshot(TRANSPORT_CELLULAR, /*withFgbg=*/true);
+            case FrameworkStatsLog.WIFI_BYTES_TRANSFER: {
+                final NetworkStats stats = getUidNetworkStatsSnapshot(TRANSPORT_WIFI,
+                        /*includeTags=*/false);
+                if (stats != null) {
+                    ret.add(new NetworkStatsExt(stats.groupedByUid(), new int[] {TRANSPORT_WIFI},
+                                    /*slicedByFgbg=*/false));
+                }
+                break;
+            }
+            case FrameworkStatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG: {
+                final NetworkStats stats = getUidNetworkStatsSnapshot(TRANSPORT_WIFI,
+                        /*includeTags=*/false);
+                if (stats != null) {
+                    ret.add(new NetworkStatsExt(sliceNetworkStatsByUidAndFgbg(stats),
+                                    new int[] {TRANSPORT_WIFI}, /*slicedByFgbg=*/true));
+                }
+                break;
+            }
+            case FrameworkStatsLog.MOBILE_BYTES_TRANSFER: {
+                final NetworkStats stats = getUidNetworkStatsSnapshot(TRANSPORT_CELLULAR,
+                        /*includeTags=*/false);
+                if (stats != null) {
+                    ret.add(new NetworkStatsExt(stats.groupedByUid(),
+                                    new int[] {TRANSPORT_CELLULAR}, /*slicedByFgbg=*/false));
+                }
+                break;
+            }
+            case FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG: {
+                final NetworkStats stats = getUidNetworkStatsSnapshot(TRANSPORT_CELLULAR,
+                        /*includeTags=*/false);
+                if (stats != null) {
+                    ret.add(new NetworkStatsExt(sliceNetworkStatsByUidAndFgbg(stats),
+                                    new int[] {TRANSPORT_CELLULAR}, /*slicedByFgbg=*/true));
+                }
+                break;
+            }
+            case FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED: {
+                final NetworkStats wifiStats = getUidNetworkStatsSnapshot(TRANSPORT_WIFI,
+                        /*includeTags=*/true);
+                final NetworkStats cellularStats = getUidNetworkStatsSnapshot(TRANSPORT_CELLULAR,
+                        /*includeTags=*/true);
+                if (wifiStats != null && cellularStats != null) {
+                    final NetworkStats stats = wifiStats.add(cellularStats);
+                    ret.add(new NetworkStatsExt(sliceNetworkStatsByUidTagAndMetered(stats),
+                                    new int[] {TRANSPORT_WIFI, TRANSPORT_CELLULAR},
+                                    /*slicedByFgbg=*/false, /*slicedByTag=*/true,
+                                    /*slicedByMetered=*/true));
+                }
+                break;
+            }
             default:
                 throw new IllegalArgumentException("Unknown atomTag " + atomTag);
         }
-    }
-
-    // Get a snapshot of Uid NetworkStats. The snapshot contains NetworkStats with its associated
-    // information, and wrapped by a list since multiple NetworkStatsExt objects might be collected.
-    @NonNull
-    private List<NetworkStatsExt> collectUidNetworkStatsSnapshot(int transport, boolean withFgbg) {
-        final List<NetworkStatsExt> ret = new ArrayList<>();
-        final NetworkTemplate template = (transport == TRANSPORT_CELLULAR
-                ? NetworkTemplate.buildTemplateMobileWithRatType(
-                        /*subscriptionId=*/null, NETWORK_TYPE_ALL)
-                : NetworkTemplate.buildTemplateWifiWildcard());
-
-        final NetworkStats stats = getUidNetworkStatsSnapshot(template, withFgbg);
-        if (stats != null) {
-            ret.add(new NetworkStatsExt(stats, transport, withFgbg));
-        }
         return ret;
     }
 
-
     private int pullDataBytesTransfer(
             int atomTag, @NonNull List<StatsEvent> pulledData) {
         final List<NetworkStatsExt> current = collectNetworkStatsSnapshotForAtom(atomTag);
@@ -842,22 +891,28 @@
 
         for (final NetworkStatsExt item : current) {
             final NetworkStatsExt baseline = CollectionUtils.find(mNetworkStatsBaselines,
-                    it -> it.withFgbg == item.withFgbg && it.transport == item.transport);
+                    it -> it.hasSameSlicing(item));
 
             // No matched baseline indicates error has occurred during initialization stage,
             // skip reporting anything since the snapshot is invalid.
             if (baseline == null) {
-                Slog.e(TAG, "baseline is null for " + atomTag + ", transport="
-                        + item.transport + " , withFgbg=" + item.withFgbg + ", return.");
+                Slog.e(TAG, "baseline is null for " + atomTag + ", return.");
                 return StatsManager.PULL_SKIP;
             }
-            final NetworkStatsExt diff = new NetworkStatsExt(item.stats.subtract(
-                    baseline.stats).removeEmptyEntries(), item.transport, item.withFgbg);
+            final NetworkStatsExt diff = new NetworkStatsExt(
+                    item.stats.subtract(baseline.stats).removeEmptyEntries(), item.transports,
+                    item.slicedByFgbg, item.slicedByTag, item.slicedByMetered);
 
             // If no diff, skip.
             if (diff.stats.size() == 0) continue;
 
-            addNetworkStats(atomTag, pulledData, diff);
+            switch (atomTag) {
+                case FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED:
+                    addBytesTransferByTagAndMeteredAtoms(diff, pulledData);
+                    break;
+                default:
+                    addNetworkStats(atomTag, pulledData, diff);
+            }
         }
         return StatsManager.PULL_SUCCESS;
     }
@@ -879,7 +934,7 @@
             }
             e.writeInt(entry.uid);
             e.addBooleanAnnotation(ANNOTATION_ID_IS_UID, true);
-            if (statsExt.withFgbg) {
+            if (statsExt.slicedByFgbg) {
                 e.writeInt(entry.set);
             }
             e.writeLong(entry.rxBytes);
@@ -890,14 +945,38 @@
         }
     }
 
+    private void addBytesTransferByTagAndMeteredAtoms(@NonNull NetworkStatsExt statsExt,
+            @NonNull List<StatsEvent> pulledData) {
+        final NetworkStats.Entry entry = new NetworkStats.Entry(); // for recycling
+        for (int i = 0; i < statsExt.stats.size(); i++) {
+            statsExt.stats.getValues(i, entry);
+            StatsEvent e = StatsEvent.newBuilder()
+                    .setAtomId(FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED)
+                    .addBooleanAnnotation(ANNOTATION_ID_TRUNCATE_TIMESTAMP, true)
+                    .writeInt(entry.uid)
+                    .addBooleanAnnotation(ANNOTATION_ID_IS_UID, true)
+                    .writeBoolean(entry.metered == NetworkStats.METERED_YES)
+                    .writeInt(entry.tag)
+                    .writeLong(entry.rxBytes)
+                    .writeLong(entry.rxPackets)
+                    .writeLong(entry.txBytes)
+                    .writeLong(entry.txPackets)
+                    .build();
+            pulledData.add(e);
+        }
+    }
+
     /**
      * Create a snapshot of NetworkStats since boot, but add 1 bucket duration before boot as a
      * buffer to ensure at least one full bucket will be included.
      * Note that this should be only used to calculate diff since the snapshot might contains
      * some traffic before boot.
      */
-    @Nullable private NetworkStats getUidNetworkStatsSnapshot(
-            @NonNull NetworkTemplate template, boolean withFgbg) {
+    @Nullable private NetworkStats getUidNetworkStatsSnapshot(int transport, boolean includeTags) {
+        final NetworkTemplate template = (transport == TRANSPORT_CELLULAR)
+                ? NetworkTemplate.buildTemplateMobileWithRatType(
+                        /*subscriptionId=*/null, NETWORK_TYPE_ALL)
+                : NetworkTemplate.buildTemplateWifiWildcard();
 
         final long elapsedMillisSinceBoot = SystemClock.elapsedRealtime();
         final long currentTimeInMillis = MICROSECONDS.toMillis(SystemClock.currentTimeMicro());
@@ -906,38 +985,72 @@
         try {
             final NetworkStats stats = getNetworkStatsSession().getSummaryForAllUid(template,
                     currentTimeInMillis - elapsedMillisSinceBoot - bucketDuration,
-                    currentTimeInMillis, /*includeTags=*/false);
-            return withFgbg ? rollupNetworkStatsByFgbg(stats) : stats.groupedByUid();
+                    currentTimeInMillis, includeTags);
+            return stats;
         } catch (RemoteException | NullPointerException e) {
-            Slog.e(TAG, "Pulling netstats for " + template
-                    + " fgbg= " + withFgbg + " bytes has error", e);
+            Slog.e(TAG, "Pulling netstats for template=" + template + " and includeTags="
+                    + includeTags  + " causes error", e);
         }
         return null;
     }
 
+    @NonNull private NetworkStats sliceNetworkStatsByUidAndFgbg(@NonNull NetworkStats stats) {
+        return sliceNetworkStats(stats,
+                (newEntry, oldEntry) -> {
+                    newEntry.uid = oldEntry.uid;
+                    newEntry.set = oldEntry.set;
+                });
+    }
+
+    @NonNull private NetworkStats sliceNetworkStatsByUidTagAndMetered(@NonNull NetworkStats stats) {
+        return sliceNetworkStats(stats,
+                (newEntry, oldEntry) -> {
+                    newEntry.uid = oldEntry.uid;
+                    newEntry.tag = oldEntry.tag;
+                    newEntry.metered = oldEntry.metered;
+                });
+    }
+
     /**
-     * Allows rollups per UID but keeping the set (foreground/background) slicing.
-     * Adapted from groupedByUid in frameworks/base/core/java/android/net/NetworkStats.java
+     * Slices NetworkStats along the dimensions specified in the slicer lambda and aggregates over
+     * non-sliced dimensions.
+     *
+     * This function iterates through each NetworkStats.Entry, sets its dimensions equal to the
+     * default state (with the presumption that we don't want to slice on anything), and then
+     * applies the slicer lambda to allow users to control which dimensions to slice on. This is
+     * adapted from groupedByUid within NetworkStats.java
+     *
+     * @param slicer An operation taking into two parameters, new NetworkStats.Entry and old
+     *               NetworkStats.Entry, that should be used to copy state from the old to the new.
+     *               This is useful for slicing by particular dimensions. For example, if we wished
+     *               to slice by uid and tag, we could write the following lambda:
+     *                  (new, old) -> {
+     *                          new.uid = old.uid;
+     *                          new.tag = old.tag;
+     *                  }
+     *               If no slicer is provided, the data is not sliced by any dimensions.
+     * @return new NeworkStats object appropriately sliced
      */
-    @NonNull private NetworkStats rollupNetworkStatsByFgbg(@NonNull NetworkStats stats) {
+    @NonNull private NetworkStats sliceNetworkStats(@NonNull NetworkStats stats,
+            @Nullable BiConsumer<NetworkStats.Entry, NetworkStats.Entry> slicer) {
         final NetworkStats ret = new NetworkStats(stats.getElapsedRealtime(), 1);
 
         final NetworkStats.Entry entry = new NetworkStats.Entry();
+        entry.uid = NetworkStats.UID_ALL;
         entry.iface = NetworkStats.IFACE_ALL;
+        entry.set = NetworkStats.SET_ALL;
         entry.tag = NetworkStats.TAG_NONE;
         entry.metered = NetworkStats.METERED_ALL;
         entry.roaming = NetworkStats.ROAMING_ALL;
+        entry.defaultNetwork = NetworkStats.DEFAULT_NETWORK_ALL;
 
-        int size = stats.size();
-        final NetworkStats.Entry recycle = new NetworkStats.Entry(); // Used for retrieving values
-        for (int i = 0; i < size; i++) {
+        final NetworkStats.Entry recycle = new NetworkStats.Entry(); // used for retrieving values
+        for (int i = 0; i < stats.size(); i++) {
             stats.getValues(i, recycle);
+            if (slicer != null) {
+                slicer.accept(entry, recycle);
+            }
 
-            // Skip specific tags, since already counted in TAG_NONE
-            if (recycle.tag != NetworkStats.TAG_NONE) continue;
-
-            entry.set = recycle.set; // Allows slicing by background/foreground
-            entry.uid = recycle.uid;
             entry.rxBytes = recycle.rxBytes;
             entry.rxPackets = recycle.rxPackets;
             entry.txBytes = recycle.txBytes;
@@ -987,6 +1100,19 @@
         );
     }
 
+    private void registerBytesTransferByTagAndMetered() {
+        int tagId = FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED;
+        PullAtomMetadata metadata = new PullAtomMetadata.Builder()
+                .setAdditiveFields(new int[] {4, 5, 6, 7})
+                .build();
+        mStatsManager.setPullAtomCallback(
+                tagId,
+                metadata,
+                BackgroundThread.getExecutor(),
+                mStatsCallbackImpl
+        );
+    }
+
     private void registerBluetoothBytesTransfer() {
         int tagId = FrameworkStatsLog.BLUETOOTH_BYTES_TRANSFER;
         PullAtomMetadata metadata = new PullAtomMetadata.Builder()
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index e3b1152c..323ac7b 100755
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.tv;
 
+import static android.media.AudioManager.DEVICE_NONE;
 import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED;
 import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED_STANDBY;
 
@@ -2047,6 +2048,36 @@
             return clientPid;
         }
 
+        /**
+         * Add a hardware device in the TvInputHardwareManager for CTS testing
+         * purpose.
+         *
+         * @param device id of the adding hardware device.
+         */
+        @Override
+        public void addHardwareDevice(int deviceId) {
+            TvInputHardwareInfo info = new TvInputHardwareInfo.Builder()
+                        .deviceId(deviceId)
+                        .type(TvInputHardwareInfo.TV_INPUT_TYPE_HDMI)
+                        .audioType(DEVICE_NONE)
+                        .audioAddress("0")
+                        .hdmiPortId(0)
+                        .build();
+            mTvInputHardwareManager.onDeviceAvailable(info, null);
+            return;
+        }
+
+        /**
+         * Remove a hardware device in the TvInputHardwareManager for CTS testing
+         * purpose.
+         *
+         * @param device id of the removing hardware device.
+         */
+        @Override
+        public void removeHardwareDevice(int deviceId) {
+            mTvInputHardwareManager.onDeviceUnavailable(deviceId);
+        }
+
         private int getClientPidLocked(String sessionId)
                 throws IllegalStateException {
             if (mSessionIdToSessionStateMap.get(sessionId) == null) {
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 9b9b613..fc69ef5 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -2638,6 +2638,9 @@
             mRootWindowContainer.ensureVisibilityAndConfig(null /* starting */,
                     getDisplay().mDisplayId, false /* markFrozenIfConfigChanged */,
                     false /* deferResume */);
+            // Usually resuming a top activity triggers the next app transition, but nothing's got
+            // resumed in this case, so we need to execute it explicitly.
+            getDisplay().mDisplayContent.executeAppTransition();
         } else {
             mRootWindowContainer.resumeFocusedStacksTopActivities();
         }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index dfae702..3272a5d 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -99,6 +99,7 @@
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_FOCUS;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT;
+import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_IME;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_SCREEN_ON;
 import static com.android.server.wm.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
@@ -498,6 +499,8 @@
      */
     private ActivityRecord mFixedRotationLaunchingApp;
 
+    private FixedRotationAnimationController mFixedRotationAnimationController;
+
     final FixedRotationTransitionListener mFixedRotationTransitionListener =
             new FixedRotationTransitionListener();
 
@@ -1487,6 +1490,11 @@
         return mFixedRotationLaunchingApp;
     }
 
+    @VisibleForTesting
+    @Nullable FixedRotationAnimationController getFixedRotationAnimationController() {
+        return mFixedRotationAnimationController;
+    }
+
     void setFixedRotationLaunchingAppUnchecked(@Nullable ActivityRecord r) {
         setFixedRotationLaunchingAppUnchecked(r, ROTATION_UNDEFINED);
     }
@@ -1494,8 +1502,13 @@
     void setFixedRotationLaunchingAppUnchecked(@Nullable ActivityRecord r, int rotation) {
         if (mFixedRotationLaunchingApp == null && r != null) {
             mWmService.mDisplayNotificationController.dispatchFixedRotationStarted(this, rotation);
+            if (mFixedRotationAnimationController == null) {
+                mFixedRotationAnimationController = new FixedRotationAnimationController(this);
+                mFixedRotationAnimationController.hide();
+            }
         } else if (mFixedRotationLaunchingApp != null && r == null) {
             mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this);
+            finishFixedRotationAnimationIfPossible();
         }
         mFixedRotationLaunchingApp = r;
     }
@@ -1584,6 +1597,15 @@
         }
     }
 
+    /** Re-show the previously hidden windows if all seamless rotated windows are done. */
+    void finishFixedRotationAnimationIfPossible() {
+        final FixedRotationAnimationController controller = mFixedRotationAnimationController;
+        if (controller != null && !mDisplayRotation.hasSeamlessRotatingWindow()) {
+            controller.show();
+            mFixedRotationAnimationController = null;
+        }
+    }
+
     /**
      * Update rotation of the display.
      *
@@ -3496,7 +3518,7 @@
         if (target == mInputMethodTarget && mInputMethodTargetWaitingAnim == targetWaitingAnim) {
             return;
         }
-
+        ProtoLog.i(WM_DEBUG_IME, "setInputMethodTarget %s", target);
         mInputMethodTarget = target;
         mInputMethodTargetWaitingAnim = targetWaitingAnim;
         assignWindowLayers(true /* setLayoutNeeded */);
@@ -3510,6 +3532,7 @@
      */
     void setInputMethodInputTarget(WindowState target) {
         if (mInputMethodInputTarget != target) {
+            ProtoLog.i(WM_DEBUG_IME, "setInputMethodInputTarget %s", target);
             mInputMethodInputTarget = target;
             updateImeControlTarget();
         }
@@ -3517,6 +3540,8 @@
 
     private void updateImeControlTarget() {
         mInputMethodControlTarget = computeImeControlTarget();
+        ProtoLog.i(WM_DEBUG_IME, "updateImeControlTarget %s",
+                mInputMethodControlTarget.getWindow());
         mInsetsStateController.onImeControlTargetChanged(mInputMethodControlTarget);
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 702df2a..96f2363 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -560,6 +560,7 @@
         }, true /* traverseTopToBottom */);
         mSeamlessRotationCount = 0;
         mRotatingSeamlessly = false;
+        mDisplayContent.finishFixedRotationAnimationIfPossible();
     }
 
     private void prepareSeamlessRotation() {
@@ -573,6 +574,10 @@
         return mRotatingSeamlessly;
     }
 
+    boolean hasSeamlessRotatingWindow() {
+        return mSeamlessRotationCount > 0;
+    }
+
     @VisibleForTesting
     boolean shouldRotateSeamlessly(int oldRotation, int newRotation, boolean forceUpdate) {
         // Display doesn't need to be frozen because application has been started in correct
@@ -646,6 +651,7 @@
                     "Performing post-rotate rotation after seamless rotation");
             // Finish seamless rotation.
             mRotatingSeamlessly = false;
+            mDisplayContent.finishFixedRotationAnimationIfPossible();
 
             updateRotationAndSendNewConfigIfChanged();
         }
diff --git a/services/core/java/com/android/server/wm/FixedRotationAnimationController.java b/services/core/java/com/android/server/wm/FixedRotationAnimationController.java
new file mode 100644
index 0000000..cc02e99
--- /dev/null
+++ b/services/core/java/com/android/server/wm/FixedRotationAnimationController.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.AnimationSpecProto.WINDOW;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_FIXED_TRANSFORM;
+import static com.android.server.wm.WindowAnimationSpecProto.ANIMATION;
+
+import android.content.Context;
+import android.util.ArrayMap;
+import android.util.proto.ProtoOutputStream;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Transformation;
+
+import com.android.internal.R;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Controller to fade out and in system ui when applying a fixed rotation transform to a window
+ * token.
+ *
+ * The system bars will be fade out when the fixed rotation transform starts and will be fade in
+ * once all surfaces have been rotated.
+ */
+public class FixedRotationAnimationController {
+
+    private final Context mContext;
+    private final WindowState mStatusBar;
+    private final WindowState mNavigationBar;
+    private final ArrayList<WindowToken> mAnimatedWindowToken = new ArrayList<>(2);
+    private final ArrayMap<WindowToken, Runnable> mDeferredFinishCallbacks = new ArrayMap<>();
+
+    public FixedRotationAnimationController(DisplayContent displayContent) {
+        mContext = displayContent.mWmService.mContext;
+        final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
+        mStatusBar = displayPolicy.getStatusBar();
+        // Do not animate movable navigation bar (e.g. non-gesture mode).
+        mNavigationBar = !displayPolicy.navigationBarCanMove()
+                ? displayPolicy.getNavigationBar()
+                : null;
+    }
+
+    /** Applies show animation on the previously hidden window tokens. */
+    void show() {
+        for (int i = mAnimatedWindowToken.size() - 1; i >= 0; i--) {
+            final WindowToken windowToken = mAnimatedWindowToken.get(i);
+            fadeWindowToken(true /* show */, windowToken);
+        }
+    }
+
+    /** Applies hide animation on the window tokens which may be seamlessly rotated later. */
+    void hide() {
+        if (mNavigationBar != null) {
+            fadeWindowToken(false /* show */, mNavigationBar.mToken);
+        }
+        if (mStatusBar != null) {
+            fadeWindowToken(false /* show */, mStatusBar.mToken);
+        }
+    }
+
+    private void fadeWindowToken(boolean show, WindowToken windowToken) {
+        if (windowToken == null || windowToken.getParent() == null) {
+            return;
+        }
+
+        final Animation animation = AnimationUtils.loadAnimation(mContext,
+                show ? R.anim.fade_in : R.anim.fade_out);
+        final LocalAnimationAdapter.AnimationSpec windowAnimationSpec =
+                createAnimationSpec(animation);
+
+        final FixedRotationAnimationAdapter animationAdapter = new FixedRotationAnimationAdapter(
+                windowAnimationSpec, windowToken.getSurfaceAnimationRunner(), show, windowToken);
+
+        // We deferred the end of the animation when hiding the token, so we need to end it now that
+        // it's shown again.
+        final SurfaceAnimator.OnAnimationFinishedCallback finishedCallback = show ? (t, r) -> {
+            final Runnable runnable = mDeferredFinishCallbacks.remove(windowToken);
+            if (runnable != null) {
+                runnable.run();
+            }
+        } : null;
+        windowToken.startAnimation(windowToken.getPendingTransaction(), animationAdapter,
+                show /* hidden */, ANIMATION_TYPE_FIXED_TRANSFORM, finishedCallback);
+        mAnimatedWindowToken.add(windowToken);
+    }
+
+    private LocalAnimationAdapter.AnimationSpec createAnimationSpec(Animation animation) {
+        return new LocalAnimationAdapter.AnimationSpec() {
+
+            final Transformation mTransformation = new Transformation();
+
+            @Override
+            public boolean getShowWallpaper() {
+                return true;
+            }
+
+            @Override
+            public long getDuration() {
+                return animation.getDuration();
+            }
+
+            @Override
+            public void apply(SurfaceControl.Transaction t, SurfaceControl leash,
+                    long currentPlayTime) {
+                mTransformation.clear();
+                animation.getTransformation(currentPlayTime, mTransformation);
+                t.setAlpha(leash, mTransformation.getAlpha());
+            }
+
+            @Override
+            public void dump(PrintWriter pw, String prefix) {
+                pw.print(prefix);
+                pw.println(animation);
+            }
+
+            @Override
+            public void dumpDebugInner(ProtoOutputStream proto) {
+                final long token = proto.start(WINDOW);
+                proto.write(ANIMATION, animation.toString());
+                proto.end(token);
+            }
+        };
+    }
+
+    private class FixedRotationAnimationAdapter extends LocalAnimationAdapter {
+        private final boolean mShow;
+        private final WindowToken mToken;
+
+        FixedRotationAnimationAdapter(AnimationSpec windowAnimationSpec,
+                SurfaceAnimationRunner surfaceAnimationRunner, boolean show,
+                WindowToken token) {
+            super(windowAnimationSpec, surfaceAnimationRunner);
+            mShow = show;
+            mToken = token;
+        }
+
+        @Override
+        public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) {
+            // We defer the end of the hide animation to ensure the tokens stay hidden until
+            // we show them again.
+            if (!mShow) {
+                mDeferredFinishCallbacks.put(mToken, endDeferFinishCallback);
+                return true;
+            }
+            return false;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 7491376..a0985fc4 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -65,9 +65,16 @@
             // Target should still be the same.
             if (isImeTargetFromDisplayContentAndImeSame()) {
                 final InsetsControlTarget target = mDisplayContent.mInputMethodControlTarget;
-                ProtoLog.d(WM_DEBUG_IME, "call showInsets(ime) on %s",
+
+                ProtoLog.i(WM_DEBUG_IME, "call showInsets(ime) on %s",
                         target.getWindow() != null ? target.getWindow().getName() : "");
                 target.showInsets(WindowInsets.Type.ime(), true /* fromIme */);
+                if (target != mImeTargetFromIme && mImeTargetFromIme != null) {
+                    ProtoLog.w(WM_DEBUG_IME,
+                            "showInsets(ime) was requested by different window: %s ",
+                            (mImeTargetFromIme.getWindow() != null
+                                    ? mImeTargetFromIme.getWindow().getName() : ""));
+                }
             }
             abortShowImePostLayout();
         };
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index a6a21fc..6a49759 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -24,6 +24,7 @@
 import static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE;
 import static android.view.ViewRootImpl.sNewInsetsMode;
 
+import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_IME;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_INSETS_CONTROL;
 import static com.android.server.wm.WindowManagerService.H.LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED;
 
@@ -40,6 +41,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.function.TriConsumer;
+import com.android.server.protolog.common.ProtoLog;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
 import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
 
@@ -134,6 +136,7 @@
             // animate-out as new one animates-in.
             mWin.cancelAnimation();
         }
+        ProtoLog.d(WM_DEBUG_IME, "InsetsSource setWin %s", win);
         mWin = win;
         mFrameProvider = frameProvider;
         mImeFrameProvider = imeFrameProvider;
@@ -299,6 +302,8 @@
         updateVisibility();
         mControl = new InsetsSourceControl(mSource.getType(), leash,
                 new Point(mWin.getWindowFrames().mFrame.left, mWin.getWindowFrames().mFrame.top));
+        ProtoLog.d(WM_DEBUG_IME,
+                "InsetsSource Control %s for target %s", mControl, mControlTarget);
     }
 
     void startSeamlessRotation() {
@@ -349,6 +354,9 @@
         final boolean isClientControlled = mControlTarget != null
                 && mControlTarget.isClientControlled();
         mSource.setVisible(mServerVisible && (!isClientControlled || mClientVisible));
+        ProtoLog.d(WM_DEBUG_IME,
+                "InsetsSource updateVisibility serverVisible: %s clientVisible: %s",
+                mServerVisible, mClientVisible);
     }
 
     InsetsSourceControl getControl(InsetsControlTarget target) {
@@ -391,6 +399,44 @@
         return mImeOverrideFrame;
     }
 
+    public void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "InsetsSourceProvider");
+        pw.print(prefix + " mSource="); mSource.dump(prefix + "  ", pw);
+        if (mControl != null) {
+            pw.print(prefix + " mControl=");
+            mControl.dump(prefix + "  ", pw);
+        }
+        pw.print(prefix + " mFakeControl="); mFakeControl.dump(prefix + "  ", pw);
+        pw.print(" mIsLeashReadyForDispatching="); pw.print(mIsLeashReadyForDispatching);
+        pw.print(" mImeOverrideFrame="); pw.print(mImeOverrideFrame.toString());
+        if (mWin != null) {
+            pw.print(prefix + " mWin=");
+            mWin.dump(pw, prefix + "  ", false /* dumpAll */);
+        }
+        if (mAdapter != null) {
+            pw.print(prefix + " mAdapter=");
+            mAdapter.dump(pw, prefix + "  ");
+        }
+        if (mControlTarget != null) {
+            pw.print(prefix + " mControlTarget=");
+            if (mControlTarget.getWindow() != null) {
+                mControlTarget.getWindow().dump(pw, prefix + "  ", false /* dumpAll */);
+            }
+        }
+        if (mPendingControlTarget != null) {
+            pw.print(prefix + " mPendingControlTarget=");
+            if (mPendingControlTarget.getWindow() != null) {
+                mPendingControlTarget.getWindow().dump(pw, prefix + "  ", false /* dumpAll */);
+            }
+        }
+        if (mFakeControlTarget != null) {
+            pw.print(prefix + " mFakeControlTarget=");
+            if (mFakeControlTarget.getWindow() != null) {
+                mFakeControlTarget.getWindow().dump(pw, prefix + "  ", false /* dumpAll */);
+            }
+        }
+    }
+
     private class ControlAdapter implements AnimationAdapter {
 
         private SurfaceControl mCapturedLeash;
@@ -410,6 +456,9 @@
                 t.setAlpha(animationLeash, 1 /* alpha */);
                 t.hide(animationLeash);
             }
+            ProtoLog.i(WM_DEBUG_IME,
+                    "ControlAdapter startAnimation mSource: %s controlTarget: %s", mSource,
+                    mControlTarget);
 
             mCapturedLeash = animationLeash;
             final Rect frame = mWin.getWindowFrames().mFrame;
@@ -424,6 +473,9 @@
                 mControlTarget = null;
                 mAdapter = null;
                 setClientVisible(InsetsState.getDefaultVisibility(mSource.getType()));
+                ProtoLog.i(WM_DEBUG_IME,
+                        "ControlAdapter onAnimationCancelled mSource: %s mControlTarget: %s",
+                        mSource, mControlTarget);
             }
         }
 
@@ -439,6 +491,8 @@
 
         @Override
         public void dump(PrintWriter pw, String prefix) {
+            pw.println(prefix + "ControlAdapter");
+            pw.print(prefix + " mCapturedLeash="); pw.print(mCapturedLeash);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 9798d77..77bc37f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -29,6 +29,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 
+import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_IME;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.WindowConfiguration;
@@ -42,6 +44,8 @@
 import android.view.InsetsState.InternalInsetsType;
 import android.view.WindowManager;
 
+import com.android.server.protolog.common.ProtoLog;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.function.Consumer;
@@ -289,7 +293,10 @@
 
         // Make sure that we always have a control target for the IME, even if the IME target is
         // null. Otherwise there is no leash that will hide it and IME becomes "randomly" visible.
-        onControlChanged(ITYPE_IME, imeTarget != null ? imeTarget : mEmptyImeControlTarget);
+        InsetsControlTarget target = imeTarget != null ? imeTarget : mEmptyImeControlTarget;
+        onControlChanged(ITYPE_IME, target);
+        ProtoLog.d(WM_DEBUG_IME, "onImeControlTargetChanged %s",
+                target != null ? target.getWindow() : "null");
         notifyPendingInsetsControlChanged();
     }
 
@@ -440,5 +447,11 @@
             pw.println(InsetsState.typeToString(mTypeControlTargetMap.keyAt(i)) + " -> "
                     + mTypeControlTargetMap.valueAt(i));
         }
+        pw.println(prefix + "  " + "InsetsSourceProviders map:");
+        for (int i = mProviders.size() - 1; i >= 0; i--) {
+            pw.print(prefix + "  ");
+            pw.println(InsetsState.typeToString(mProviders.keyAt(i)) + " -> ");
+            mProviders.valueAt(i).dump(pw, prefix);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 42342a6..0143eb1 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -489,6 +489,12 @@
     static final int ANIMATION_TYPE_INSETS_CONTROL = 1 << 5;
 
     /**
+     * Animation when a fixed rotation transform is applied to a window token.
+     * @hide
+     */
+    static final int ANIMATION_TYPE_FIXED_TRANSFORM = 1 << 6;
+
+    /**
      * Bitmask to include all animation types. This is NOT an {@link AnimationType}
      * @hide
      */
@@ -505,7 +511,8 @@
             ANIMATION_TYPE_DIMMER,
             ANIMATION_TYPE_RECENTS,
             ANIMATION_TYPE_WINDOW_ANIMATION,
-            ANIMATION_TYPE_INSETS_CONTROL
+            ANIMATION_TYPE_INSETS_CONTROL,
+            ANIMATION_TYPE_FIXED_TRANSFORM
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface AnimationType {}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 159c59b..eee4e77 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -6191,6 +6191,7 @@
             final int displayId = dc.getDisplayId();
             final WindowState inputMethodTarget = dc.mInputMethodTarget;
             final WindowState inputMethodInputTarget = dc.mInputMethodInputTarget;
+            final InsetsControlTarget inputMethodControlTarget = dc.mInputMethodControlTarget;
             if (inputMethodTarget != null) {
                 pw.print("  mInputMethodTarget in display# "); pw.print(displayId);
                 pw.print(' '); pw.println(inputMethodTarget);
@@ -6199,6 +6200,10 @@
                 pw.print("  mInputMethodInputTarget in display# "); pw.print(displayId);
                 pw.print(' '); pw.println(inputMethodInputTarget);
             }
+            if (inputMethodControlTarget != null) {
+                pw.print("  inputMethodControlTarget in display# "); pw.print(displayId);
+                pw.print(' '); pw.println(inputMethodControlTarget.getWindow());
+            }
         });
         pw.print("  mInTouchMode="); pw.println(mInTouchMode);
         pw.print("  mLastDisplayFreezeDuration=");
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 619d87b..bdecb8d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -351,6 +351,11 @@
     }
 
     private int runDumpVisibleWindowViews(PrintWriter pw) {
+        if (!mInternal.checkCallingPermission(android.Manifest.permission.DUMP,
+                "runDumpVisibleWindowViews()")) {
+            throw new SecurityException("Requires DUMP permission");
+        }
+
         try (ZipOutputStream out = new ZipOutputStream(getRawOutputStream())) {
             ArrayList<Pair<String, ByteTransferPipe>> requestList = new ArrayList<>();
             synchronized (mInternal.mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 5fa4afd..4f1893e 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -113,6 +113,7 @@
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_FOCUS;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT;
+import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_IME;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_RESIZE;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW;
@@ -3559,6 +3560,7 @@
      * Called when the insets state changed.
      */
     void notifyInsetsChanged() {
+        ProtoLog.d(WM_DEBUG_IME, "notifyInsetsChanged for %s ", this);
         try {
             mClient.insetsChanged(getInsetsState());
         } catch (RemoteException e) {
@@ -3568,6 +3570,7 @@
 
     @Override
     public void notifyInsetsControlChanged() {
+        ProtoLog.d(WM_DEBUG_IME, "notifyInsetsControlChanged for %s ", this);
         final InsetsStateController stateController =
                 getDisplayContent().getInsetsStateController();
         try {
diff --git a/services/incremental/BinderIncrementalService.cpp b/services/incremental/BinderIncrementalService.cpp
index 6018b9e..e790a19 100644
--- a/services/incremental/BinderIncrementalService.cpp
+++ b/services/incremental/BinderIncrementalService.cpp
@@ -280,8 +280,9 @@
 
 binder::Status BinderIncrementalService::configureNativeBinaries(
         int32_t storageId, const std::string& apkFullPath, const std::string& libDirRelativePath,
-        const std::string& abi, bool* _aidl_return) {
-    *_aidl_return = mImpl.configureNativeBinaries(storageId, apkFullPath, libDirRelativePath, abi);
+        const std::string& abi, bool extractNativeLibs, bool* _aidl_return) {
+    *_aidl_return = mImpl.configureNativeBinaries(storageId, apkFullPath, libDirRelativePath, abi,
+                                                  extractNativeLibs);
     return ok();
 }
 
diff --git a/services/incremental/BinderIncrementalService.h b/services/incremental/BinderIncrementalService.h
index af11363..68549f5 100644
--- a/services/incremental/BinderIncrementalService.h
+++ b/services/incremental/BinderIncrementalService.h
@@ -77,7 +77,8 @@
 
     binder::Status configureNativeBinaries(int32_t storageId, const std::string& apkFullPath,
                                            const std::string& libDirRelativePath,
-                                           const std::string& abi, bool* _aidl_return) final;
+                                           const std::string& abi, bool extractNativeLibs,
+                                           bool* _aidl_return) final;
     binder::Status waitForNativeBinariesExtraction(int storageId, bool* _aidl_return) final;
 
 private:
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index b03d1ea..66c7717 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -1379,7 +1379,7 @@
 // Extract lib files from zip, create new files in incfs and write data to them
 bool IncrementalService::configureNativeBinaries(StorageId storage, std::string_view apkFullPath,
                                                  std::string_view libDirRelativePath,
-                                                 std::string_view abi) {
+                                                 std::string_view abi, bool extractNativeLibs) {
     auto start = Clock::now();
 
     const auto ifs = getIfs(storage);
@@ -1423,6 +1423,21 @@
             continue;
         }
 
+        if (!extractNativeLibs) {
+            // ensure the file is properly aligned and unpacked
+            if (entry.method != kCompressStored) {
+                LOG(WARNING) << "Library " << fileName << " must be uncompressed to mmap it";
+                return false;
+            }
+            if ((entry.offset & (constants().blockSize - 1)) != 0) {
+                LOG(WARNING) << "Library " << fileName
+                             << " must be page-aligned to mmap it, offset = 0x" << std::hex
+                             << entry.offset;
+                return false;
+            }
+            continue;
+        }
+
         auto startFileTs = Clock::now();
 
         const auto libName = path::basename(fileName);
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index bde4ef6..05f62b9 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -138,7 +138,8 @@
     bool startLoading(StorageId storage) const;
 
     bool configureNativeBinaries(StorageId storage, std::string_view apkFullPath,
-                                 std::string_view libDirRelativePath, std::string_view abi);
+                                 std::string_view libDirRelativePath, std::string_view abi,
+                                 bool extractNativeLibs);
     bool waitForNativeBinariesExtraction(StorageId storage);
 
     class AppOpsListener : public android::BnAppOpsCallback {
diff --git a/services/people/java/com/android/server/people/data/ConversationInfo.java b/services/people/java/com/android/server/people/data/ConversationInfo.java
index dc3fa2a..1737828 100644
--- a/services/people/java/com/android/server/people/data/ConversationInfo.java
+++ b/services/people/java/com/android/server/people/data/ConversationInfo.java
@@ -142,9 +142,12 @@
         return hasShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED);
     }
 
-    /** Whether the shortcut for this conversation is cached in Shortcut Service. */
-    public boolean isShortcutCached() {
-        return hasShortcutFlags(ShortcutInfo.FLAG_CACHED);
+    /**
+     * Whether the shortcut for this conversation is cached in Shortcut Service, with cache owner
+     * set as notifications.
+     */
+    public boolean isShortcutCachedForNotification() {
+        return hasShortcutFlags(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
     }
 
     /** Whether this conversation is marked as important by the user. */
@@ -223,7 +226,7 @@
         if (isShortcutLongLived()) {
             sb.append("Liv");
         }
-        if (isShortcutCached()) {
+        if (isShortcutCachedForNotification()) {
             sb.append("Cac");
         }
         sb.append("]");
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index bbb0215..63b7162 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -294,14 +294,14 @@
             if (notificationListener != null) {
                 String packageName = packageData.getPackageName();
                 packageData.forAllConversations(conversationInfo -> {
-                    if (conversationInfo.isShortcutCached()
+                    if (conversationInfo.isShortcutCachedForNotification()
                             && conversationInfo.getNotificationChannelId() == null
                             && !notificationListener.hasActiveNotifications(
                                     packageName, conversationInfo.getShortcutId())) {
                         mShortcutServiceInternal.uncacheShortcuts(userId,
                                 mContext.getPackageName(), packageName,
                                 Collections.singletonList(conversationInfo.getShortcutId()),
-                                userId);
+                                userId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
                     }
                 });
             }
@@ -821,12 +821,12 @@
                         // The shortcut was cached by Notification Manager synchronously when the
                         // associated notification was posted. Uncache it here when all the
                         // associated notifications are removed.
-                        if (conversationInfo.isShortcutCached()
+                        if (conversationInfo.isShortcutCachedForNotification()
                                 && conversationInfo.getNotificationChannelId() == null) {
                             mShortcutServiceInternal.uncacheShortcuts(mUserId,
                                     mContext.getPackageName(), sbn.getPackageName(),
                                     Collections.singletonList(conversationInfo.getShortcutId()),
-                                    mUserId);
+                                    mUserId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
                         }
                     } else {
                         mActiveNotifCounts.put(conversationKey, count);
@@ -891,12 +891,12 @@
                 ConversationInfo conversationInfo =
                         packageData != null ? packageData.getConversationInfo(shortcutId) : null;
                 if (conversationInfo != null
-                        && conversationInfo.isShortcutCached()
+                        && conversationInfo.isShortcutCachedForNotification()
                         && conversationInfo.getNotificationChannelId() == null) {
                     mShortcutServiceInternal.uncacheShortcuts(mUserId,
                             mContext.getPackageName(), packageName,
                             Collections.singletonList(shortcutId),
-                            mUserId);
+                            mUserId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
                 }
             }
         }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 724048b..4a77489 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -1997,19 +1997,9 @@
 
     private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS =
             Sets.newSet(
-                    UserManager.DISALLOW_CONFIG_DATE_TIME,
-                    UserManager.DISALLOW_BLUETOOTH_SHARING,
-                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
-                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
-                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
-                    UserManager.DISALLOW_CONFIG_TETHERING,
-                    UserManager.DISALLOW_DATA_ROAMING,
-                    UserManager.DISALLOW_SAFE_BOOT,
-                    UserManager.DISALLOW_SMS,
-                    UserManager.DISALLOW_USB_FILE_TRANSFER,
                     UserManager.DISALLOW_AIRPLANE_MODE,
-                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
-                    UserManager.DISALLOW_UNMUTE_MICROPHONE
+                    UserManager.DISALLOW_CONFIG_DATE_TIME,
+                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS
             );
 
     private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_LOCAL_RESTRICTIONS =
@@ -2021,7 +2011,17 @@
                     UserManager.DISALLOW_CONTENT_SUGGESTIONS,
                     UserManager.DISALLOW_DEBUGGING_FEATURES,
                     UserManager.DISALLOW_SHARE_LOCATION,
-                    UserManager.DISALLOW_OUTGOING_CALLS
+                    UserManager.DISALLOW_OUTGOING_CALLS,
+                    UserManager.DISALLOW_BLUETOOTH_SHARING,
+                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
+                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
+                    UserManager.DISALLOW_CONFIG_TETHERING,
+                    UserManager.DISALLOW_DATA_ROAMING,
+                    UserManager.DISALLOW_SAFE_BOOT,
+                    UserManager.DISALLOW_SMS,
+                    UserManager.DISALLOW_USB_FILE_TRANSFER,
+                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
+                    UserManager.DISALLOW_UNMUTE_MICROPHONE
             );
 
     public void testSetUserRestriction_asPoOfOrgOwnedDevice() throws Exception {
@@ -2045,8 +2045,9 @@
         parentDpm.setCameraDisabled(admin1, true);
         verify(getServices().userManagerInternal).setDevicePolicyUserRestrictions(
                 eq(CALLER_USER_HANDLE),
-                MockUtils.checkUserRestrictions(UserManager.DISALLOW_CAMERA),
-                MockUtils.checkUserRestrictions(CALLER_USER_HANDLE),
+                MockUtils.checkUserRestrictions(),
+                MockUtils.checkUserRestrictions(UserHandle.USER_SYSTEM,
+                        UserManager.DISALLOW_CAMERA),
                 eq(false));
         DpmTestUtils.assertRestrictions(
                 DpmTestUtils.newRestrictions(UserManager.DISALLOW_CAMERA),
diff --git a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
index 70d6cf8..c5d9487 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
@@ -46,7 +46,8 @@
                 .setContactUri(CONTACT_URI)
                 .setContactPhoneNumber(PHONE_NUMBER)
                 .setNotificationChannelId(NOTIFICATION_CHANNEL_ID)
-                .setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED | ShortcutInfo.FLAG_CACHED)
+                .setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED
+                        | ShortcutInfo.FLAG_CACHED_NOTIFICATIONS)
                 .setImportant(true)
                 .setNotificationSilenced(true)
                 .setBubbled(true)
@@ -62,7 +63,7 @@
         assertEquals(PHONE_NUMBER, conversationInfo.getContactPhoneNumber());
         assertEquals(NOTIFICATION_CHANNEL_ID, conversationInfo.getNotificationChannelId());
         assertTrue(conversationInfo.isShortcutLongLived());
-        assertTrue(conversationInfo.isShortcutCached());
+        assertTrue(conversationInfo.isShortcutCachedForNotification());
         assertTrue(conversationInfo.isImportant());
         assertTrue(conversationInfo.isNotificationSilenced());
         assertTrue(conversationInfo.isBubbled());
@@ -84,7 +85,7 @@
         assertNull(conversationInfo.getContactPhoneNumber());
         assertNull(conversationInfo.getNotificationChannelId());
         assertFalse(conversationInfo.isShortcutLongLived());
-        assertFalse(conversationInfo.isShortcutCached());
+        assertFalse(conversationInfo.isShortcutCachedForNotification());
         assertFalse(conversationInfo.isImportant());
         assertFalse(conversationInfo.isNotificationSilenced());
         assertFalse(conversationInfo.isBubbled());
diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
index 1a2032a..b2f7abb 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
@@ -405,7 +405,7 @@
 
         ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
                 buildPerson());
-        shortcut.setCached();
+        shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
         mDataManager.addOrUpdateConversationInfo(shortcut);
 
         NotificationListenerService listenerService =
@@ -419,7 +419,8 @@
         assertEquals(1, activeNotificationOpenTimeSlots.size());
         verify(mShortcutServiceInternal).uncacheShortcuts(
                 anyInt(), any(), eq(TEST_PKG_NAME),
-                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY));
+                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
+                eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
     }
 
     @Test
@@ -434,7 +435,7 @@
                 mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
 
         // Post one notification.
-        shortcut.setCached();
+        shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
         mDataManager.addOrUpdateConversationInfo(shortcut);
         listenerService.onNotificationPosted(mStatusBarNotification);
 
@@ -445,14 +446,15 @@
         listenerService.onNotificationRemoved(mStatusBarNotification, null,
                 NotificationListenerService.REASON_CANCEL);
         verify(mShortcutServiceInternal, never()).uncacheShortcuts(
-                anyInt(), any(), anyString(), any(), anyInt());
+                anyInt(), any(), anyString(), any(), anyInt(), anyInt());
 
         // Removing the second notification un-caches the shortcut.
         listenerService.onNotificationRemoved(mStatusBarNotification, null,
                 NotificationListenerService.REASON_CANCEL_ALL);
         verify(mShortcutServiceInternal).uncacheShortcuts(
                 anyInt(), any(), eq(TEST_PKG_NAME),
-                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY));
+                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
+                eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
     }
 
     @Test
@@ -467,7 +469,7 @@
                 mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
 
         listenerService.onNotificationPosted(mStatusBarNotification);
-        shortcut.setCached();
+        shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
         mDataManager.addOrUpdateConversationInfo(shortcut);
 
         listenerService.onNotificationChannelModified(TEST_PKG_NAME, UserHandle.of(USER_ID_PRIMARY),
@@ -477,7 +479,8 @@
                 NotificationListenerService.REASON_CANCEL_ALL);
         verify(mShortcutServiceInternal, never()).uncacheShortcuts(
                 anyInt(), any(), eq(TEST_PKG_NAME),
-                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY));
+                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
+                eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
     }
 
     @Test
@@ -569,13 +572,14 @@
                 mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
 
         listenerService.onNotificationPosted(mStatusBarNotification);
-        shortcut.setCached();
+        shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
         mDataManager.addOrUpdateConversationInfo(shortcut);
 
         mShutdownBroadcastReceiver.onReceive(mContext, new Intent());
         verify(mShortcutServiceInternal).uncacheShortcuts(
                 anyInt(), any(), eq(TEST_PKG_NAME),
-                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY));
+                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
+                eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
     }
 
     @Test
@@ -590,7 +594,7 @@
                 mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
 
         listenerService.onNotificationPosted(mStatusBarNotification);
-        shortcut.setCached();
+        shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
         mDataManager.addOrUpdateConversationInfo(shortcut);
 
         listenerService.onNotificationChannelModified(TEST_PKG_NAME, UserHandle.of(USER_ID_PRIMARY),
@@ -599,7 +603,8 @@
         mShutdownBroadcastReceiver.onReceive(mContext, new Intent());
         verify(mShortcutServiceInternal, never()).uncacheShortcuts(
                 anyInt(), any(), eq(TEST_PKG_NAME),
-                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY));
+                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
+                eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
     }
 
     @Test
@@ -767,14 +772,15 @@
 
         ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
                 buildPerson());
-        shortcut.setCached();
+        shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
         mDataManager.addOrUpdateConversationInfo(shortcut);
 
         mDataManager.pruneDataForUser(USER_ID_PRIMARY, mCancellationSignal);
 
         verify(mShortcutServiceInternal).uncacheShortcuts(
                 anyInt(), any(), eq(TEST_PKG_NAME),
-                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY));
+                eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
+                eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index db02524..90989b9 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -137,6 +137,9 @@
 @SmallTest
 public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
 
+    private static final int CACHE_OWNER_0 = LauncherApps.FLAG_CACHE_NOTIFICATION_SHORTCUTS;
+    private static final int CACHE_OWNER_1 = LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS;
+
     @Override
     protected void tearDown() throws Exception {
         deleteUriFile("file32x32.jpg");
@@ -487,7 +490,8 @@
         mManager.pushDynamicShortcut(s8);
         assertEquals(4, getCallerShortcut("s8").getRank());
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s8"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s8"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
         });
 
         mManager.pushDynamicShortcut(s9);
@@ -1452,8 +1456,10 @@
 
         // Cache 1 and 2
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2"),
-                    HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1"),
+                    HANDLE_USER_0, CACHE_OWNER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"),
+                    HANDLE_USER_0, CACHE_OWNER_1);
         });
 
         setCaller(CALLING_PACKAGE_1);
@@ -1532,8 +1538,10 @@
 
         // Cache some, but non long lived shortcuts will be ignored.
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2", "s4"),
-                    HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2"),
+                    HANDLE_USER_0, CACHE_OWNER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2", "s4"),
+                    HANDLE_USER_0, CACHE_OWNER_1);
         });
 
         setCaller(CALLING_PACKAGE_1);
@@ -1555,10 +1563,18 @@
         assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED),
                 "s2", "s4");
 
+        runWithCaller(LAUNCHER_1, USER_0, () -> {
+            mLauncherApps.uncacheShortcuts(CALLING_PACKAGE_1, list("s2", "s4"),
+                    HANDLE_USER_0, CACHE_OWNER_0);
+        });
+        // s2 still cached by owner1. s4 wasn't cached by owner0 so didn't get removed.
+        assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED),
+                "s2", "s4");
+
         // uncache a non-dynamic shortcut. Should be removed.
         runWithCaller(LAUNCHER_1, USER_0, () -> {
             mLauncherApps.uncacheShortcuts(CALLING_PACKAGE_1, list("s4"),
-                    HANDLE_USER_0);
+                    HANDLE_USER_0, CACHE_OWNER_1);
         });
         assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED),
                 "s2");
@@ -1566,7 +1582,7 @@
         // Cache another shortcut
         runWithCaller(LAUNCHER_1, USER_0, () -> {
             mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s3"),
-                    HANDLE_USER_0);
+                    HANDLE_USER_0, CACHE_OWNER_0);
         });
         assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED),
                 "s2", "s3");
@@ -1594,7 +1610,7 @@
         // Cache All
         runWithCaller(LAUNCHER_1, USER_0, () -> {
             mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2", "s3", "s4"),
-                    HANDLE_USER_0);
+                    HANDLE_USER_0, CACHE_OWNER_0);
         });
 
         setCaller(CALLING_PACKAGE_1);
@@ -1792,8 +1808,10 @@
         setCaller(LAUNCHER_1);
 
         // Cache some shortcuts. Only long lived shortcuts can get cached.
-        mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1"), getCallingUser());
-        mLauncherApps.cacheShortcuts(CALLING_PACKAGE_3, list("s3"), getCallingUser());
+        mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1"), getCallingUser(),
+                CACHE_OWNER_0);
+        mLauncherApps.cacheShortcuts(CALLING_PACKAGE_3, list("s3"), getCallingUser(),
+                CACHE_OWNER_0);
 
         // Cached ones only
         assertShortcutIds(assertAllNotKeyFieldsOnly(
@@ -8732,7 +8750,8 @@
         assertTrue(mInternal.isSharingShortcut(USER_0, LAUNCHER_1, CALLING_PACKAGE_1, "s3", USER_0,
                 filter_any));
 
-        mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2"), HANDLE_USER_0);
+        mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2"), HANDLE_USER_0,
+                CACHE_OWNER_0);
         mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0);
 
         runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest11.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest11.java
index 6219665..6a2b8e0 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest11.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest11.java
@@ -25,6 +25,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.content.ComponentName;
+import android.content.pm.LauncherApps;
 import android.content.pm.LauncherApps.ShortcutChangeCallback;
 import android.content.pm.LauncherApps.ShortcutQuery;
 import android.content.pm.ShortcutInfo;
@@ -46,6 +47,9 @@
     private static final ShortcutQuery QUERY_MATCH_ALL = createShortcutQuery(
             ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED);
 
+    private static final int CACHE_OWNER_0 = LauncherApps.FLAG_CACHE_NOTIFICATION_SHORTCUTS;
+    private static final int CACHE_OWNER_1 = LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS;
+
     private final TestLooper mTestLooper = new TestLooper();
 
     public void testShortcutChangeCallback_setDynamicShortcuts() {
@@ -113,7 +117,8 @@
 
         runWithCaller(LAUNCHER_1, USER_0, () -> {
             mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1"), HANDLE_USER_0);
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
         });
 
         ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class);
@@ -211,7 +216,42 @@
         runWithCaller(LAUNCHER_1, USER_0, () -> {
             mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
                     mTestLooper.getNewExecutor());
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s3"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s3"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s3"), HANDLE_USER_0,
+                    CACHE_OWNER_1);
+        });
+
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<List> shortcuts = ArgumentCaptor.forClass(List.class);
+        verify(callback, times(2)).onShortcutsAddedOrUpdated(
+                eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0));
+        verify(callback, times(0)).onShortcutsRemoved(any(), any(), any());
+
+        assertWith(shortcuts.getValue())
+                .areAllWithKeyFieldsOnly()
+                .haveIds("s1", "s3");
+    }
+
+    public void testShortcutChangeCallback_cacheShortcuts_alreadyCached() {
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(makeLongLivedShortcut("s1"),
+                    makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3"))));
+        });
+
+        ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class);
+        runWithCaller(LAUNCHER_1, USER_0, () -> {
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s3"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
+            mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
+                    mTestLooper.getNewExecutor());
+            // Should not cause any callback events
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s3"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
+            // Should cause a change event
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s3"), HANDLE_USER_0,
+                    CACHE_OWNER_1);
         });
 
         mTestLooper.dispatchAll();
@@ -234,10 +274,12 @@
 
         ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class);
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
             mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
                     mTestLooper.getNewExecutor());
-            mLauncherApps.uncacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+            mLauncherApps.uncacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
         });
 
         mTestLooper.dispatchAll();
@@ -259,8 +301,11 @@
         });
 
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2", "s3"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2", "s3"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
             mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1"), HANDLE_USER_0,
+                    CACHE_OWNER_1);
         });
 
         runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
@@ -271,7 +316,8 @@
         runWithCaller(LAUNCHER_1, USER_0, () -> {
             mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
                     mTestLooper.getNewExecutor());
-            mLauncherApps.uncacheShortcuts(CALLING_PACKAGE_1, list("s2", "s3"), HANDLE_USER_0);
+            mLauncherApps.uncacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2", "s3"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
         });
 
         mTestLooper.dispatchAll();
@@ -284,9 +330,10 @@
         verify(callback, times(1)).onShortcutsRemoved(
                 eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0));
 
+        // s1 is still cached for owner1, s2 is pinned.
         assertWith(changedShortcuts.getValue())
                 .areAllWithKeyFieldsOnly()
-                .haveIds("s2");
+                .haveIds("s1", "s2");
 
         assertWith(removedShortcuts.getValue())
                 .areAllWithKeyFieldsOnly()
@@ -453,7 +500,8 @@
 
         ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class);
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
             mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
                     mTestLooper.getNewExecutor());
         });
@@ -511,7 +559,8 @@
 
         ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class);
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
             mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0);
             mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
                     mTestLooper.getNewExecutor());
@@ -547,7 +596,8 @@
         });
 
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
             mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0);
         });
 
@@ -614,7 +664,8 @@
 
         ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class);
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
             mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0);
             mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
                     mTestLooper.getNewExecutor());
@@ -680,7 +731,8 @@
 
         ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class);
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
             mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0);
             mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
                     mTestLooper.getNewExecutor());
@@ -747,7 +799,8 @@
 
         ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class);
         runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+            mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0,
+                    CACHE_OWNER_0);
             mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0);
             mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL,
                     mTestLooper.getNewExecutor());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 2bea491..cf63682 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -6152,7 +6152,7 @@
         // Make sure the shortcut is cached.
         verify(mShortcutServiceInternal).cacheShortcuts(
                 anyInt(), any(), eq(PKG), eq(Collections.singletonList(VALID_CONVO_SHORTCUT_ID)),
-                eq(USER_SYSTEM));
+                eq(USER_SYSTEM), eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
 
         // Test: Remove the shortcut
         when(mLauncherApps.getShortcuts(any(), any())).thenReturn(null);
@@ -6225,7 +6225,7 @@
         // Make sure the shortcut is cached.
         verify(mShortcutServiceInternal).cacheShortcuts(
                 anyInt(), any(), eq(PKG), eq(Collections.singletonList(shortcutId)),
-                eq(USER_SYSTEM));
+                eq(USER_SYSTEM), eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
 
         // Test: Remove the notification
         mBinderService.cancelNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 078c21e..1d6f823 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -31,6 +31,7 @@
 
 import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE;
 import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_COUNT_LIMIT;
+import static com.android.server.notification.PreferencesHelper.UNKNOWN_UID;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -2511,6 +2512,26 @@
     }
 
     @Test
+    public void testBubblePrefence_noSAWCheckForUnknownUid() throws Exception {
+        final String xml = "<ranking version=\"1\">\n"
+                + "<package name=\"" + PKG_O + "\" uid=\"" + UNKNOWN_UID + "\">\n"
+                + "<channel id=\"someId\" name=\"hi\""
+                + " importance=\"3\"/>"
+                + "</package>"
+                + "</ranking>";
+        XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())),
+                null);
+        parser.nextTag();
+        mHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+        assertEquals(DEFAULT_BUBBLE_PREFERENCE, mHelper.getBubblePreference(PKG_O, UID_O));
+        assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O));
+        verify(mAppOpsManager, never()).noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(),
+                anyString(), eq(null), anyString());
+    }
+
+    @Test
     public void testBubblePreference_xml() throws Exception {
         mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE);
         assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_NONE);
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 4e82ceb..d063f10 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -57,6 +57,7 @@
 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.SurfaceAnimator.ANIMATION_TYPE_FIXED_TRANSFORM;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
 
@@ -1060,6 +1061,11 @@
     @Test
     public void testApplyTopFixedRotationTransform() {
         mWm.mIsFixedRotationTransformEnabled = true;
+        final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
+        // Only non-movable (gesture) navigation bar will be animated by fixed rotation animation.
+        doReturn(false).when(displayPolicy).navigationBarCanMove();
+        displayPolicy.addWindowLw(mStatusBarWindow, mStatusBarWindow.mAttrs);
+        displayPolicy.addWindowLw(mNavBarWindow, mNavBarWindow.mAttrs);
         final Configuration config90 = new Configuration();
         mDisplayContent.computeScreenConfiguration(config90, ROTATION_90);
 
@@ -1080,6 +1086,12 @@
                 ROTATION_0 /* oldRotation */, ROTATION_90 /* newRotation */,
                 false /* forceUpdate */));
 
+        assertNotNull(mDisplayContent.getFixedRotationAnimationController());
+        assertTrue(mStatusBarWindow.getParent().isAnimating(WindowContainer.AnimationFlags.PARENTS,
+                ANIMATION_TYPE_FIXED_TRANSFORM));
+        assertTrue(mNavBarWindow.getParent().isAnimating(WindowContainer.AnimationFlags.PARENTS,
+                ANIMATION_TYPE_FIXED_TRANSFORM));
+
         final Rect outFrame = new Rect();
         final Rect outInsets = new Rect();
         final Rect outStableInsets = new Rect();
@@ -1132,6 +1144,7 @@
         assertFalse(app.hasFixedRotationTransform());
         assertFalse(app2.hasFixedRotationTransform());
         assertEquals(config90.orientation, mDisplayContent.getConfiguration().orientation);
+        assertNull(mDisplayContent.getFixedRotationAnimationController());
     }
 
     @Test
@@ -1310,7 +1323,7 @@
     }
 
     private static int getRotatedOrientation(DisplayContent dc) {
-        return dc.getLastOrientation() == SCREEN_ORIENTATION_LANDSCAPE
+        return dc.mBaseDisplayWidth > dc.mBaseDisplayHeight
                 ? SCREEN_ORIENTATION_PORTRAIT
                 : SCREEN_ORIENTATION_LANDSCAPE;
     }
diff --git a/telephony/java/android/telephony/ims/ImsMmTelManager.java b/telephony/java/android/telephony/ims/ImsMmTelManager.java
index 43db1d9ce..f6c14e6 100644
--- a/telephony/java/android/telephony/ims/ImsMmTelManager.java
+++ b/telephony/java/android/telephony/ims/ImsMmTelManager.java
@@ -518,9 +518,6 @@
      * @param executor The executor the callback events should be run on.
      * @param c The MmTel {@link CapabilityCallback} to be registered.
      * @see #unregisterMmTelCapabilityCallback(CapabilityCallback)
-     * @throws IllegalArgumentException if the subscription associated with this callback is not
-     * active (SIM is not inserted, ESIM inactive) or invalid, or a null {@link Executor} or
-     * {@link CapabilityCallback} callback.
      * @throws ImsException if the subscription associated with this callback is valid, but
      * the {@link ImsService} associated with the subscription is not available. This can happen if
      * the service crashed, for example. See {@link ImsException#getCode()} for a more detailed
@@ -543,18 +540,13 @@
         ITelephony iTelephony = getITelephony();
         if (iTelephony == null) {
             throw new ImsException("Could not find Telephony Service.",
-                    ImsException.CODE_ERROR_INVALID_SUBSCRIPTION);
+                    ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
         }
 
         try {
             iTelephony.registerMmTelCapabilityCallback(mSubId, c.getBinder());
         } catch (ServiceSpecificException e) {
-            if (e.errorCode == ImsException.CODE_ERROR_INVALID_SUBSCRIPTION) {
-                // Rethrow as runtime error to keep API compatible.
-                throw new IllegalArgumentException(e.getMessage());
-            } else {
-                throw new ImsException(e.getMessage(), e.errorCode);
-            }
+            throw new ImsException(e.getMessage(), e.errorCode);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }  catch (IllegalStateException e) {
diff --git a/tests/BlobStoreTestUtils/Android.bp b/tests/BlobStoreTestUtils/Android.bp
index 5c7c68b..53d3638 100644
--- a/tests/BlobStoreTestUtils/Android.bp
+++ b/tests/BlobStoreTestUtils/Android.bp
@@ -18,6 +18,7 @@
   static_libs: [
     "truth-prebuilt",
     "androidx.test.uiautomator_uiautomator",
+    "androidx.test.ext.junit",
   ],
   sdk_version: "test_current",
 }
\ No newline at end of file
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
index 371375c..4a0ca66 100644
--- a/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
@@ -42,6 +42,7 @@
     private final File mFile;
     private final long mFileSize;
     private final CharSequence mLabel;
+    private final long mExpiryDurationMs;
 
     byte[] mFileDigest;
     long mExpiryTimeMs;
@@ -51,6 +52,7 @@
         mFile = new File(builder.getContext().getFilesDir(), builder.getFileName());
         mFileSize = builder.getFileSize();
         mLabel = builder.getLabel();
+        mExpiryDurationMs = builder.getExpiryDurationMs();
     }
 
     public static class Builder {
@@ -59,6 +61,7 @@
         private long mFileSize = DEFAULT_SIZE_BYTES;
         private CharSequence mLabel = "Test label";
         private String mFileName = "blob_" + System.nanoTime();
+        private long mExpiryDurationMs = TimeUnit.DAYS.toMillis(1);
 
         public Builder(Context context) {
             mContext = context;
@@ -104,6 +107,15 @@
             return mFileName;
         }
 
+        public Builder setExpiryDurationMs(long durationMs) {
+            mExpiryDurationMs = durationMs;
+            return this;
+        }
+
+        public long getExpiryDurationMs() {
+            return mExpiryDurationMs;
+        }
+
         public DummyBlobData build() {
             return new DummyBlobData(this);
         }
@@ -114,7 +126,7 @@
             writeRandomData(file, mFileSize);
         }
         mFileDigest = FileUtils.digest(mFile, "SHA-256");
-        mExpiryTimeMs = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
+        mExpiryTimeMs = System.currentTimeMillis() + mExpiryDurationMs;
     }
 
     public BlobHandle getBlobHandle() throws Exception {
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
index 7cf58e1..b9bd661 100644
--- a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
@@ -18,7 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.Instrumentation;
 import android.app.blob.BlobHandle;
 import android.app.blob.BlobStoreManager;
 import android.app.blob.LeaseInfo;
@@ -27,6 +26,7 @@
 import android.os.ParcelFileDescriptor;
 import android.util.Log;
 
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.uiautomator.UiDevice;
 
 import java.io.FileInputStream;
@@ -149,14 +149,14 @@
         assertThat(leaseInfo.getDescription()).isEqualTo(description);
     }
 
-    public static void triggerIdleMaintenance(Instrumentation instrumentation) throws IOException {
-        runShellCmd(instrumentation, "cmd blob_store idle-maintenance");
+    public static void triggerIdleMaintenance() throws IOException {
+        runShellCmd("cmd blob_store idle-maintenance");
     }
 
-    private static String runShellCmd(Instrumentation instrumentation,
-            String cmd) throws IOException {
-        final UiDevice uiDevice = UiDevice.getInstance(instrumentation);
-        final String result = uiDevice.executeShellCommand(cmd);
+    public static String runShellCmd(String cmd) throws IOException {
+        final UiDevice uiDevice = UiDevice.getInstance(
+                InstrumentationRegistry.getInstrumentation());
+        final String result = uiDevice.executeShellCommand(cmd).trim();
         Log.i(TAG, "Output of '" + cmd + "': '" + result + "'");
         return result;
     }
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index d011dbb..ae93a81 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -1113,6 +1113,7 @@
         mTestLooper.dispatchAll();
 
         List<Set> expectedSyncRequests = List.of(
+                Set.of(),
                 Set.of(APP_A),
                 Set.of(APP_A, APP_B),
                 Set.of(APP_A, APP_B, APP_C),
diff --git a/tests/net/java/com/android/internal/net/VpnProfileTest.java b/tests/net/java/com/android/internal/net/VpnProfileTest.java
index ceca6f0..e5daa71 100644
--- a/tests/net/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/net/java/com/android/internal/net/VpnProfileTest.java
@@ -33,7 +33,9 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 /** Unit tests for {@link VpnProfile}. */
 @SmallTest
@@ -41,6 +43,9 @@
 public class VpnProfileTest {
     private static final String DUMMY_PROFILE_KEY = "Test";
 
+    private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23;
+    private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24;
+
     @Test
     public void testDefaults() throws Exception {
         final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);
@@ -67,10 +72,11 @@
         assertFalse(p.isMetered);
         assertEquals(1360, p.maxMtu);
         assertFalse(p.areAuthParamsInline);
+        assertFalse(p.isRestrictedToTestNetworks);
     }
 
     private VpnProfile getSampleIkev2Profile(String key) {
-        final VpnProfile p = new VpnProfile(key);
+        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */);
 
         p.name = "foo";
         p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
@@ -116,7 +122,7 @@
 
     @Test
     public void testParcelUnparcel() {
-        assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 22);
+        assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
     }
 
     @Test
@@ -159,14 +165,41 @@
         assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
     }
 
+    private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) {
+        // Sort to ensure when we remove, we can do it from greatest first.
+        Arrays.sort(missingIndices);
+
+        final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode());
+        final List<String> parts =
+                new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER)));
+
+        // Remove from back first to ensure indexing is consistent.
+        for (int i = missingIndices.length - 1; i >= 0; i--) {
+            parts.remove(missingIndices[i]);
+        }
+
+        return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0]));
+    }
+
     @Test
     public void testEncodeDecodeInvalidNumberOfValues() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        final String encoded = new String(profile.encode());
-        final byte[] tooFewValues =
-                encoded.substring(0, encoded.lastIndexOf(VpnProfile.VALUE_DELIMITER)).getBytes();
+        final String tooFewValues =
+                getEncodedDecodedIkev2ProfileMissingValues(
+                        ENCODED_INDEX_AUTH_PARAMS_INLINE,
+                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */);
 
-        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues));
+        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
+    }
+
+    @Test
+    public void testEncodeDecodeMissingIsRestrictedToTestNetworks() {
+        final String tooFewValues =
+                getEncodedDecodedIkev2ProfileMissingValues(
+                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */);
+
+        // Verify decoding without isRestrictedToTestNetworks defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.isRestrictedToTestNetworks);
     }
 
     @Test