Merge "Hide the mobile slot when QSBH expands in legacy model" into sc-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 452be30..96cbed7 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -2629,8 +2629,7 @@
                 }
             } catch (NameNotFoundException e) {
                 throw new IllegalArgumentException(
-                        "Tried to schedule job for non-existent package: "
-                                + service.getPackageName());
+                        "Tried to schedule job for non-existent component: " + service);
             }
         }
 
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index c6847aa..d519b3f 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6048,11 +6048,13 @@
                     .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED)
                     .highlightExpander(false)
                     .fillTextsFrom(this);
-            if (!useRegularSubtext || TextUtils.isEmpty(mParams.summaryText)) {
+            if (!useRegularSubtext || TextUtils.isEmpty(p.summaryText)) {
                 p.summaryText(createSummaryText());
             }
             RemoteViews header = makeNotificationHeader(p);
             header.setBoolean(R.id.notification_header, "setAcceptAllTouches", true);
+            // The low priority header has no app name and shows the text
+            header.setBoolean(R.id.notification_header, "styleTextAsTitle", true);
             return header;
         }
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index a1d419e..edf0e57 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -4775,8 +4775,7 @@
      * @param flags Additional option flags to modify the data returned.
      * @return A {@link ServiceInfo} object containing information about the
      *         service.
-     * @throws NameNotFoundException if a package with the given name cannot be
-     *             found on the system.
+     * @throws NameNotFoundException if the component cannot be found on the system.
      */
     @NonNull
     public abstract ServiceInfo getServiceInfo(@NonNull ComponentName component,
diff --git a/core/java/android/net/vcn/VcnConfig.java b/core/java/android/net/vcn/VcnConfig.java
index d41c0b4..caab152 100644
--- a/core/java/android/net/vcn/VcnConfig.java
+++ b/core/java/android/net/vcn/VcnConfig.java
@@ -52,12 +52,17 @@
     private static final String GATEWAY_CONNECTION_CONFIGS_KEY = "mGatewayConnectionConfigs";
     @NonNull private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs;
 
+    private static final String IS_TEST_MODE_PROFILE_KEY = "mIsTestModeProfile";
+    private final boolean mIsTestModeProfile;
+
     private VcnConfig(
             @NonNull String packageName,
-            @NonNull Set<VcnGatewayConnectionConfig> gatewayConnectionConfigs) {
+            @NonNull Set<VcnGatewayConnectionConfig> gatewayConnectionConfigs,
+            boolean isTestModeProfile) {
         mPackageName = packageName;
         mGatewayConnectionConfigs =
                 Collections.unmodifiableSet(new ArraySet<>(gatewayConnectionConfigs));
+        mIsTestModeProfile = isTestModeProfile;
 
         validate();
     }
@@ -77,6 +82,7 @@
                 new ArraySet<>(
                         PersistableBundleUtils.toList(
                                 gatewayConnectionConfigsBundle, VcnGatewayConnectionConfig::new));
+        mIsTestModeProfile = in.getBoolean(IS_TEST_MODE_PROFILE_KEY);
 
         validate();
     }
@@ -104,6 +110,15 @@
     }
 
     /**
+     * Returns whether or not this VcnConfig is restricted to test networks.
+     *
+     * @hide
+     */
+    public boolean isTestModeProfile() {
+        return mIsTestModeProfile;
+    }
+
+    /**
      * Serializes this object to a PersistableBundle.
      *
      * @hide
@@ -119,13 +134,14 @@
                         new ArrayList<>(mGatewayConnectionConfigs),
                         VcnGatewayConnectionConfig::toPersistableBundle);
         result.putPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY, gatewayConnectionConfigsBundle);
+        result.putBoolean(IS_TEST_MODE_PROFILE_KEY, mIsTestModeProfile);
 
         return result;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mPackageName, mGatewayConnectionConfigs);
+        return Objects.hash(mPackageName, mGatewayConnectionConfigs, mIsTestModeProfile);
     }
 
     @Override
@@ -136,7 +152,8 @@
 
         final VcnConfig rhs = (VcnConfig) other;
         return mPackageName.equals(rhs.mPackageName)
-                && mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs);
+                && mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs)
+                && mIsTestModeProfile == rhs.mIsTestModeProfile;
     }
 
     // Parcelable methods
@@ -172,6 +189,8 @@
         @NonNull
         private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs = new ArraySet<>();
 
+        private boolean mIsTestModeProfile = false;
+
         public Builder(@NonNull Context context) {
             Objects.requireNonNull(context, "context was null");
 
@@ -207,13 +226,29 @@
         }
 
         /**
+         * Restricts this VcnConfig to matching with test networks (only).
+         *
+         * <p>This method is for testing only, and must not be used by apps. Calling {@link
+         * VcnManager#setVcnConfig(ParcelUuid, VcnConfig)} with a VcnConfig where test-network usage
+         * is enabled will require the MANAGE_TEST_NETWORKS permission.
+         *
+         * @return this {@link Builder} instance, for chaining
+         * @hide
+         */
+        @NonNull
+        public Builder setIsTestModeProfile() {
+            mIsTestModeProfile = true;
+            return this;
+        }
+
+        /**
          * Builds and validates the VcnConfig.
          *
          * @return an immutable VcnConfig instance
          */
         @NonNull
         public VcnConfig build() {
-            return new VcnConfig(mPackageName, mGatewayConnectionConfigs);
+            return new VcnConfig(mPackageName, mGatewayConnectionConfigs, mIsTestModeProfile);
         }
     }
 }
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index f02346b..7eea0b1 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -15,6 +15,8 @@
  */
 package android.net.vcn;
 
+import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_MOBIKE;
+
 import static com.android.internal.annotations.VisibleForTesting.Visibility;
 
 import android.annotation.IntDef;
@@ -438,6 +440,8 @@
          *     distinguish between VcnGatewayConnectionConfigs configured on a single {@link
          *     VcnConfig}. This will be used as the identifier in VcnStatusCallback invocations.
          * @param tunnelConnectionParams the IKE tunnel connection configuration
+         * @throws IllegalArgumentException if the provided IkeTunnelConnectionParams is not
+         *     configured to support MOBIKE
          * @see IkeTunnelConnectionParams
          * @see VcnManager.VcnStatusCallback#onGatewayConnectionError
          */
@@ -446,6 +450,10 @@
                 @NonNull IkeTunnelConnectionParams tunnelConnectionParams) {
             Objects.requireNonNull(gatewayConnectionName, "gatewayConnectionName was null");
             Objects.requireNonNull(tunnelConnectionParams, "tunnelConnectionParams was null");
+            if (!tunnelConnectionParams.getIkeSessionParams().hasIkeOption(IKE_OPTION_MOBIKE)) {
+                throw new IllegalArgumentException(
+                        "MOBIKE must be configured for the provided IkeSessionParams");
+            }
 
             mGatewayConnectionName = gatewayConnectionName;
             mTunnelConnectionParams = tunnelConnectionParams;
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index d7d1902..1092adf 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -580,6 +580,13 @@
      */
     public static final String NAMESPACE_GAME_OVERLAY = "game_overlay";
 
+    /**
+     * Namespace for Constrain Display APIs related features.
+     *
+     * @hide
+     */
+    public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
+
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
     private static ArrayMap<OnPropertiesChangedListener, Pair<String, Executor>> sListeners =
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b879082..e410e50 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8636,7 +8636,6 @@
          */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
         @TestApi
-        @Readable
         public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component";
 
         /**
diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java
index 6c8753b..000c685 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -29,6 +29,7 @@
 import android.util.AttributeSet;
 import android.widget.RelativeLayout;
 import android.widget.RemoteViews;
+import android.widget.TextView;
 
 import com.android.internal.R;
 import com.android.internal.widget.CachingIconView;
@@ -175,6 +176,28 @@
     }
 
     /**
+     * This is used to make the low-priority header show the bolded text of a title.
+     *
+     * @param styleTextAsTitle true if this header's text is to have the style of a title
+     */
+    @RemotableViewMethod
+    public void styleTextAsTitle(boolean styleTextAsTitle) {
+        int styleResId = styleTextAsTitle
+                ? R.style.TextAppearance_DeviceDefault_Notification_Title
+                : R.style.TextAppearance_DeviceDefault_Notification_Info;
+        // Most of the time, we're showing text in the minimized state
+        View headerText = findViewById(R.id.header_text);
+        if (headerText instanceof TextView) {
+            ((TextView) headerText).setTextAppearance(styleResId);
+        }
+        // If there's no summary or text, we show the app name instead of nothing
+        View appNameText = findViewById(R.id.app_name_text);
+        if (appNameText instanceof TextView) {
+            ((TextView) appNameText).setTextAppearance(styleResId);
+        }
+    }
+
+    /**
      * Get the current margin end value for the header text.
      * Add this to {@link #getTopLineBaseMarginEnd()} to get the total margin of the top line.
      *
diff --git a/core/java/com/android/internal/os/KernelCpuUidTimeReader.java b/core/java/com/android/internal/os/KernelCpuUidTimeReader.java
index e670178..50df166 100644
--- a/core/java/com/android/internal/os/KernelCpuUidTimeReader.java
+++ b/core/java/com/android/internal/os/KernelCpuUidTimeReader.java
@@ -477,18 +477,17 @@
             }
             copyToCurTimes();
             boolean notify = false;
-            boolean valid = true;
             for (int i = 0; i < mFreqCount; i++) {
                 // Unit is 10ms.
                 mDeltaTimes[i] = mCurTimes[i] - lastTimes[i];
                 if (mDeltaTimes[i] < 0) {
                     Slog.e(mTag, "Negative delta from freq time for uid: " + uid
                             + ", delta: " + mDeltaTimes[i]);
-                    valid = false;
+                    return;
                 }
                 notify |= mDeltaTimes[i] > 0;
             }
-            if (notify && valid) {
+            if (notify) {
                 System.arraycopy(mCurTimes, 0, lastTimes, 0, mFreqCount);
                 if (cb != null) {
                     cb.onUidCpuTime(uid, mDeltaTimes);
@@ -826,11 +825,11 @@
                 if (mDeltaTime[i] < 0) {
                     Slog.e(mTag, "Negative delta from cluster time for uid: " + uid
                             + ", delta: " + mDeltaTime[i]);
-                    valid = false;
+                    return;
                 }
                 notify |= mDeltaTime[i] > 0;
             }
-            if (notify && valid) {
+            if (notify) {
                 System.arraycopy(mCurTime, 0, lastTimes, 0, mNumClusters);
                 if (cb != null) {
                     cb.onUidCpuTime(uid, mDeltaTime);
diff --git a/packages/SettingsLib/FooterPreference/Android.bp b/packages/SettingsLib/FooterPreference/Android.bp
index 11f39e7..0929706 100644
--- a/packages/SettingsLib/FooterPreference/Android.bp
+++ b/packages/SettingsLib/FooterPreference/Android.bp
@@ -20,4 +20,8 @@
     ],
     sdk_version: "system_current",
     min_sdk_version: "21",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.permission",
+    ],
 }
diff --git a/packages/SettingsLib/RadioButtonPreference/Android.bp b/packages/SettingsLib/RadioButtonPreference/Android.bp
index b309c01..28ff71f 100644
--- a/packages/SettingsLib/RadioButtonPreference/Android.bp
+++ b/packages/SettingsLib/RadioButtonPreference/Android.bp
@@ -20,4 +20,8 @@
 
     sdk_version: "system_current",
     min_sdk_version: "21",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.permission",
+    ],
 }
diff --git a/packages/SettingsLib/TwoTargetPreference/Android.bp b/packages/SettingsLib/TwoTargetPreference/Android.bp
index 078e8c3..b32d5b4 100644
--- a/packages/SettingsLib/TwoTargetPreference/Android.bp
+++ b/packages/SettingsLib/TwoTargetPreference/Android.bp
@@ -19,4 +19,8 @@
     ],
     sdk_version: "system_current",
     min_sdk_version: "21",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.permission",
+    ],
 }
diff --git a/packages/SystemUI/res/layout/qs_detail.xml b/packages/SystemUI/res/layout/qs_detail.xml
index f056402..59e1a75 100644
--- a/packages/SystemUI/res/layout/qs_detail.xml
+++ b/packages/SystemUI/res/layout/qs_detail.xml
@@ -23,7 +23,6 @@
     android:clickable="true"
     android:orientation="vertical"
     android:layout_marginTop="@*android:dimen/quick_qs_offset_height"
-    android:layout_marginBottom="@dimen/qs_container_bottom_padding"
     android:paddingBottom="8dp"
     android:visibility="invisible"
     android:elevation="4dp"
diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml
index 4607e5f..4c6418a 100644
--- a/packages/SystemUI/res/layout/qs_panel.xml
+++ b/packages/SystemUI/res/layout/qs_panel.xml
@@ -17,7 +17,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/quick_settings_container"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     android:clipToPadding="false"
     android:clipChildren="false" >
 
@@ -25,7 +25,6 @@
         android:id="@+id/expanded_qs_scroll_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingBottom="@dimen/qs_container_bottom_padding"
         android:elevation="4dp"
         android:importantForAccessibility="no"
         android:scrollbars="none"
diff --git a/packages/SystemUI/res/layout/qs_tile_side_icon.xml b/packages/SystemUI/res/layout/qs_tile_side_icon.xml
index 9f9af9d..1ae0a1c 100644
--- a/packages/SystemUI/res/layout/qs_tile_side_icon.xml
+++ b/packages/SystemUI/res/layout/qs_tile_side_icon.xml
@@ -35,6 +35,7 @@
         android:layout_width="@dimen/qs_icon_size"
         android:layout_height="@dimen/qs_icon_size"
         android:src="@*android:drawable/ic_chevron_end"
+        android:autoMirrored="true"
         android:visibility="gone"
         android:importantForAccessibility="no"
     />
diff --git a/packages/SystemUI/res/layout/quick_settings_security_footer.xml b/packages/SystemUI/res/layout/quick_settings_security_footer.xml
index ce7f827..08bd71c 100644
--- a/packages/SystemUI/res/layout/quick_settings_security_footer.xml
+++ b/packages/SystemUI/res/layout/quick_settings_security_footer.xml
@@ -55,6 +55,7 @@
         android:layout_marginStart="8dp"
         android:contentDescription="@null"
         android:src="@*android:drawable/ic_chevron_end"
+        android:autoMirrored="true"
         android:tint="?android:attr/textColorSecondary" />
 
 </com.android.systemui.util.DualHeightHorizontalLinearLayout>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 57e6cc3..fa4771d 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -623,8 +623,6 @@
 
     <dimen name="qs_notif_collapsed_space">64dp</dimen>
 
-    <dimen name="qs_container_bottom_padding">24dp</dimen>
-
     <!-- Desired qs icon overlay size. -->
     <dimen name="qs_detail_icon_overlay_size">24dp</dimen>
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 4b71a3a..baf3458 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -19,42 +19,22 @@
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
-import android.app.PendingIntent;
 import android.app.WallpaperManager;
-import android.app.smartspace.SmartspaceConfig;
-import android.app.smartspace.SmartspaceManager;
-import android.app.smartspace.SmartspaceSession;
-import android.app.smartspace.SmartspaceTarget;
-import android.content.Intent;
-import android.content.pm.UserInfo;
 import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.provider.Settings;
 import android.text.TextUtils;
 import android.text.format.DateFormat;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.keyguard.clock.ClockManager;
-import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.BcSmartspaceDataPlugin;
-import com.android.systemui.plugins.BcSmartspaceDataPlugin.IntentStarter;
 import com.android.systemui.plugins.ClockPlugin;
-import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
@@ -62,14 +42,10 @@
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
 import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.ViewController;
-import com.android.systemui.util.settings.SecureSettings;
 
 import java.util.Locale;
-import java.util.Optional;
 import java.util.TimeZone;
-import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -85,9 +61,8 @@
     private final KeyguardSliceViewController mKeyguardSliceViewController;
     private final NotificationIconAreaController mNotificationIconAreaController;
     private final BroadcastDispatcher mBroadcastDispatcher;
-    private final Executor mUiExecutor;
     private final BatteryController mBatteryController;
-    private final FeatureFlags mFeatureFlags;
+    private final LockscreenSmartspaceController mSmartspaceController;
 
     /**
      * Clock for both small and large sizes
@@ -97,20 +72,8 @@
     private AnimatableClockController mLargeClockViewController;
     private FrameLayout mLargeClockFrame;
 
-    private SmartspaceSession mSmartspaceSession;
-    private SmartspaceSession.OnTargetsAvailableListener mSmartspaceCallback;
-    private ConfigurationController mConfigurationController;
-    private ActivityStarter mActivityStarter;
-    private FalsingManager mFalsingManager;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final KeyguardBypassController mBypassController;
-    private Handler mHandler;
-    private UserTracker mUserTracker;
-    private SecureSettings mSecureSettings;
-    private ContentObserver mSettingsObserver;
-    private boolean mShowSensitiveContentForCurrentUser;
-    private boolean mShowSensitiveContentForManagedUser;
-    private UserHandle mManagedUserHandle;
 
     /**
      * Listener for changes to the color palette.
@@ -118,59 +81,30 @@
      * The color palette changes when the wallpaper is changed.
      */
     private final ColorExtractor.OnColorsChangedListener mColorsListener =
-            new ColorExtractor.OnColorsChangedListener() {
-        @Override
-        public void onColorsChanged(ColorExtractor extractor, int which) {
-            if ((which & WallpaperManager.FLAG_LOCK) != 0) {
-                mView.updateColors(getGradientColors());
-            }
-        }
-    };
-
-    private final ConfigurationController.ConfigurationListener mConfigurationListener =
-            new ConfigurationController.ConfigurationListener() {
-        @Override
-        public void onThemeChanged() {
-            updateWallpaperColor();
-        }
-    };
-
-    private ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin;
-
-    private final StatusBarStateController.StateListener mStatusBarStateListener =
-            new StatusBarStateController.StateListener() {
-                @Override
-                public void onDozeAmountChanged(float linear, float eased) {
-                    if (mSmartspaceView != null) {
-                        mSmartspaceView.setDozeAmount(eased);
-                    }
+            (extractor, which) -> {
+                if ((which & WallpaperManager.FLAG_LOCK) != 0) {
+                    mView.updateColors(getGradientColors());
                 }
             };
 
+    private final ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin;
+
     // If set, will replace keyguard_status_area
-    private BcSmartspaceDataPlugin.SmartspaceView mSmartspaceView;
-    private Optional<BcSmartspaceDataPlugin> mSmartspacePlugin;
+    private View mSmartspaceView;
 
     @Inject
     public KeyguardClockSwitchController(
             KeyguardClockSwitch keyguardClockSwitch,
             StatusBarStateController statusBarStateController,
-            SysuiColorExtractor colorExtractor, ClockManager clockManager,
+            SysuiColorExtractor colorExtractor,
+            ClockManager clockManager,
             KeyguardSliceViewController keyguardSliceViewController,
             NotificationIconAreaController notificationIconAreaController,
             BroadcastDispatcher broadcastDispatcher,
-            FeatureFlags featureFlags,
-            @Main Executor uiExecutor,
             BatteryController batteryController,
-            ConfigurationController configurationController,
-            ActivityStarter activityStarter,
-            FalsingManager falsingManager,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             KeyguardBypassController bypassController,
-            @Main Handler handler,
-            UserTracker userTracker,
-            SecureSettings secureSettings,
-            Optional<BcSmartspaceDataPlugin> smartspacePlugin) {
+            LockscreenSmartspaceController smartspaceController) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
         mColorExtractor = colorExtractor;
@@ -178,18 +112,10 @@
         mKeyguardSliceViewController = keyguardSliceViewController;
         mNotificationIconAreaController = notificationIconAreaController;
         mBroadcastDispatcher = broadcastDispatcher;
-        mFeatureFlags = featureFlags;
-        mUiExecutor = uiExecutor;
         mBatteryController = batteryController;
-        mConfigurationController = configurationController;
-        mActivityStarter = activityStarter;
-        mFalsingManager = falsingManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mBypassController = bypassController;
-        mHandler = handler;
-        mUserTracker = userTracker;
-        mSecureSettings = secureSettings;
-        mSmartspacePlugin = smartspacePlugin;
+        mSmartspaceController = smartspaceController;
     }
 
     /**
@@ -232,119 +158,33 @@
                         mBypassController);
         mLargeClockViewController.init();
 
-        mStatusBarStateController.addCallback(mStatusBarStateListener);
-        mConfigurationController.addCallback(mConfigurationListener);
+        if (mSmartspaceController.isEnabled()) {
+            mSmartspaceView = mSmartspaceController.buildAndConnectView(mView);
 
-        if (mFeatureFlags.isSmartspaceEnabled() && mSmartspacePlugin.isPresent()) {
-            BcSmartspaceDataPlugin smartspaceDataPlugin = mSmartspacePlugin.get();
             View ksa = mView.findViewById(R.id.keyguard_status_area);
             int ksaIndex = mView.indexOfChild(ksa);
             ksa.setVisibility(View.GONE);
 
-            mSmartspaceView = smartspaceDataPlugin.getView(mView);
-            mSmartspaceView.registerDataProvider(smartspaceDataPlugin);
-            mSmartspaceView.setIntentStarter(new IntentStarter() {
-                public void startIntent(View v, Intent i) {
-                    mActivityStarter.startActivity(i, true /* dismissShade */);
-                }
-
-                public void startPendingIntent(PendingIntent pi) {
-                    mActivityStarter.startPendingIntentDismissingKeyguard(pi);
-                }
-            });
-            mSmartspaceView.setFalsingManager(mFalsingManager);
-            updateWallpaperColor();
-            View asView = (View) mSmartspaceView;
-
             // Place smartspace view below normal clock...
             RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
                     MATCH_PARENT, WRAP_CONTENT);
             lp.addRule(RelativeLayout.BELOW, R.id.lockscreen_clock_view);
 
-            mView.addView(asView, ksaIndex, lp);
+            mView.addView(mSmartspaceView, ksaIndex, lp);
             int padding = getContext().getResources()
                     .getDimensionPixelSize(R.dimen.below_clock_padding_start);
-            asView.setPadding(padding, 0, padding, 0);
+            mSmartspaceView.setPadding(padding, 0, padding, 0);
 
             // ... but above the large clock
             lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
-            lp.addRule(RelativeLayout.BELOW, asView.getId());
+            lp.addRule(RelativeLayout.BELOW, mSmartspaceView.getId());
             mLargeClockFrame.setLayoutParams(lp);
 
             View nic = mView.findViewById(
                     R.id.left_aligned_notification_icon_container);
             lp = (RelativeLayout.LayoutParams) nic.getLayoutParams();
-            lp.addRule(RelativeLayout.BELOW, asView.getId());
+            lp.addRule(RelativeLayout.BELOW, mSmartspaceView.getId());
             nic.setLayoutParams(lp);
-
-            mSmartspaceSession = getContext().getSystemService(SmartspaceManager.class)
-                    .createSmartspaceSession(
-                            new SmartspaceConfig.Builder(getContext(), "lockscreen").build());
-            mSmartspaceCallback = targets -> {
-                targets.removeIf(this::filterSmartspaceTarget);
-                smartspaceDataPlugin.onTargetsAvailable(targets);
-            };
-            mSmartspaceSession.addOnTargetsAvailableListener(mUiExecutor, mSmartspaceCallback);
-            mSettingsObserver = new ContentObserver(mHandler) {
-                @Override
-                public void onChange(boolean selfChange, Uri uri) {
-                    reloadSmartspace();
-                }
-            };
-
-            getContext().getContentResolver().registerContentObserver(
-                    Settings.Secure.getUriFor(
-                            Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
-                    true, mSettingsObserver, UserHandle.USER_ALL);
-            reloadSmartspace();
-        }
-
-        float dozeAmount = mStatusBarStateController.getDozeAmount();
-        mStatusBarStateListener.onDozeAmountChanged(dozeAmount, dozeAmount);
-    }
-
-    @VisibleForTesting
-    boolean filterSmartspaceTarget(SmartspaceTarget t) {
-        if (!t.isSensitive()) return false;
-
-        if (t.getUserHandle().equals(mUserTracker.getUserHandle())) {
-            return !mShowSensitiveContentForCurrentUser;
-        }
-        if (t.getUserHandle().equals(mManagedUserHandle)) {
-            return !mShowSensitiveContentForManagedUser;
-        }
-
-        return false;
-    }
-
-    private void reloadSmartspace() {
-        mManagedUserHandle = getWorkProfileUser();
-        final String setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
-
-        mShowSensitiveContentForCurrentUser =
-                mSecureSettings.getIntForUser(setting, 0, mUserTracker.getUserId()) == 1;
-        if (mManagedUserHandle != null) {
-            int id = mManagedUserHandle.getIdentifier();
-            mShowSensitiveContentForManagedUser =
-                    mSecureSettings.getIntForUser(setting, 0, id) == 1;
-        }
-
-        mSmartspaceSession.requestSmartspaceUpdate();
-    }
-
-    private UserHandle getWorkProfileUser() {
-        for (UserInfo userInfo : mUserTracker.getUserProfiles()) {
-            if (userInfo.isManagedProfile()) {
-                return userInfo.getUserHandle();
-            }
-        }
-        return null;
-    }
-
-    private void updateWallpaperColor() {
-        if (mSmartspaceView != null) {
-            int color = Utils.getColorAttrDefaultColor(getContext(), R.attr.wallpaperTextColor);
-            mSmartspaceView.setPrimaryTextColor(color);
         }
     }
 
@@ -356,16 +196,16 @@
         mColorExtractor.removeOnColorsChangedListener(mColorsListener);
         mView.setClockPlugin(null, mStatusBarStateController.getState());
 
-        if (mSmartspaceSession != null) {
-            mSmartspaceSession.removeOnTargetsAvailableListener(mSmartspaceCallback);
-            mSmartspaceSession.close();
-            mSmartspaceSession = null;
-        }
-        mStatusBarStateController.removeCallback(mStatusBarStateListener);
-        mConfigurationController.removeCallback(mConfigurationListener);
+        mSmartspaceController.disconnect();
 
-        if (mSettingsObserver != null) {
-            getContext().getContentResolver().unregisterContentObserver(mSettingsObserver);
+        // TODO: This is an unfortunate necessity since smartspace plugin retains a single instance
+        // of the smartspace view -- if we don't remove the view, it can't be reused by a later
+        // instance of this class. In order to fix this, we need to modify the plugin so that
+        // (a) we get a new view each time and (b) we can properly clean up an old view by making
+        // it unregister itself as a plugin listener.
+        if (mSmartspaceView != null) {
+            mView.removeView(mSmartspaceView);
+            mSmartspaceView = null;
         }
     }
 
@@ -436,7 +276,7 @@
                 scale, props, animate);
 
         if (mSmartspaceView != null) {
-            PropertyAnimator.setProperty((View) mSmartspaceView, AnimatableProperty.TRANSLATION_X,
+            PropertyAnimator.setProperty(mSmartspaceView, AnimatableProperty.TRANSLATION_X,
                     x, props, animate);
         }
 
@@ -510,14 +350,4 @@
     private int getCurrentLayoutDirection() {
         return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
     }
-
-    @VisibleForTesting
-    ConfigurationController.ConfigurationListener getConfigurationListener() {
-        return mConfigurationListener;
-    }
-
-    @VisibleForTesting
-    ContentObserver getSettingsObserver() {
-        return mSettingsObserver;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index fd80d50..26db33d 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -29,6 +29,7 @@
 import android.app.WallpaperManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.role.RoleManager;
+import android.app.smartspace.SmartspaceManager;
 import android.app.trust.TrustManager;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -400,4 +401,10 @@
     static PermissionManager providePermissionManager(Context context) {
         return context.getSystemService(PermissionManager.class);
     }
+
+    @Provides
+    @Singleton
+    static SmartspaceManager provideSmartspaceManager(Context context) {
+        return context.getSystemService(SmartspaceManager.class);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index c459963..3a3f3f1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -129,6 +129,12 @@
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         mNavBarInset = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
+        mQSPanelContainer.setPaddingRelative(
+                mQSPanelContainer.getPaddingStart(),
+                mQSPanelContainer.getPaddingTop(),
+                mQSPanelContainer.getPaddingEnd(),
+                mNavBarInset
+        );
         return super.onApplyWindowInsets(insets);
     }
 
@@ -138,8 +144,7 @@
         // bottom and footer are inside the screen.
         MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
 
-        int availableScreenHeight = getDisplayHeight() - mNavBarInset;
-        int maxQs = availableScreenHeight - layoutParams.topMargin - layoutParams.bottomMargin
+        int maxQs = getDisplayHeight() - layoutParams.topMargin - layoutParams.bottomMargin
                 - getPaddingBottom();
         int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
                 + layoutParams.rightMargin;
@@ -148,10 +153,8 @@
         mQSPanelContainer.measure(qsPanelWidthSpec,
                 MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
         int width = mQSPanelContainer.getMeasuredWidth() + padding;
-        int height = layoutParams.topMargin + layoutParams.bottomMargin
-                + mQSPanelContainer.getMeasuredHeight() + getPaddingBottom();
         super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(availableScreenHeight, MeasureSpec.EXACTLY));
+                MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
         // QSCustomizer will always be the height of the screen, but do this after
         // other measuring to avoid changing the height of the QS.
         mQSCustomizer.measure(widthMeasureSpec,
@@ -196,13 +199,10 @@
 
     void updateResources(QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController) {
-        mQSPanelContainer.setPaddingRelative(
-                mQSPanelContainer.getPaddingStart(),
-                mContext.getResources().getDimensionPixelSize(
-                        com.android.internal.R.dimen.quick_qs_offset_height),
-                mQSPanelContainer.getPaddingEnd(),
-                mContext.getResources().getDimensionPixelSize(R.dimen.qs_container_bottom_padding)
-        );
+        LayoutParams layoutParams = (LayoutParams) mQSPanelContainer.getLayoutParams();
+        layoutParams.topMargin = mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.quick_qs_offset_height);
+        mQSPanelContainer.setLayoutParams(layoutParams);
 
         int sideMargins = getResources().getDimensionPixelSize(R.dimen.notification_side_paddings);
         int padding = getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
index 05197e4..0335319 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
@@ -30,6 +30,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
+import android.view.WindowInsets;
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -153,11 +154,18 @@
         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
         lp.topMargin = mContext.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.quick_qs_offset_height);
-        lp.bottomMargin = mContext.getResources().getDimensionPixelSize(
-                R.dimen.qs_container_bottom_padding);
         setLayoutParams(lp);
     }
 
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        int bottomNavBar = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
+        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
+        lp.bottomMargin = bottomNavBar;
+        setLayoutParams(lp);
+        return super.onApplyWindowInsets(insets);
+    }
+
     public boolean isClosingDetail() {
         return mClosingDetail;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index eb7854e..4919593 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -61,6 +61,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
 import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
+import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger;
 import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.systemui.util.DeviceConfigProxy;
@@ -243,11 +244,12 @@
             SystemClock systemClock,
             ActivityStarter activityStarter,
             @Main Executor mainExecutor,
-            IActivityManager iActivityManager) {
+            IActivityManager iActivityManager,
+            OngoingCallLogger logger) {
         OngoingCallController ongoingCallController =
                 new OngoingCallController(
                         notifCollection, featureFlags, systemClock, activityStarter, mainExecutor,
-                        iActivityManager);
+                        iActivityManager, logger);
         ongoingCallController.init();
         return ongoingCallController;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
new file mode 100644
index 0000000..ce60c85
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.lockscreen
+
+import android.app.PendingIntent
+import android.app.smartspace.SmartspaceConfig
+import android.app.smartspace.SmartspaceManager
+import android.app.smartspace.SmartspaceSession
+import android.app.smartspace.SmartspaceTarget
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import android.view.View
+import android.view.ViewGroup
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.Execution
+import com.android.systemui.util.settings.SecureSettings
+import java.lang.RuntimeException
+import java.util.Optional
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Controller for managing the smartspace view on the lockscreen
+ */
+@SysUISingleton
+class LockscreenSmartspaceController @Inject constructor(
+    private val context: Context,
+    private val featureFlags: FeatureFlags,
+    private val smartspaceManager: SmartspaceManager,
+    private val activityStarter: ActivityStarter,
+    private val falsingManager: FalsingManager,
+    private val secureSettings: SecureSettings,
+    private val userTracker: UserTracker,
+    private val contentResolver: ContentResolver,
+    private val configurationController: ConfigurationController,
+    private val statusBarStateController: StatusBarStateController,
+    private val execution: Execution,
+    @Main private val uiExecutor: Executor,
+    @Main private val handler: Handler,
+    optionalPlugin: Optional<BcSmartspaceDataPlugin>
+) {
+    private var session: SmartspaceSession? = null
+    private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
+    private lateinit var smartspaceView: SmartspaceView
+
+    lateinit var view: View
+        private set
+
+    private var showSensitiveContentForCurrentUser = false
+    private var showSensitiveContentForManagedUser = false
+    private var managedUserHandle: UserHandle? = null
+
+    fun isEnabled(): Boolean {
+        execution.assertIsMainThread()
+
+        return featureFlags.isSmartspaceEnabled && plugin != null
+    }
+
+    /**
+     * Constructs the smartspace view and connects it to the smartspace service. Subsequent calls
+     * are idempotent until [disconnect] is called.
+     */
+    fun buildAndConnectView(parent: ViewGroup): View {
+        execution.assertIsMainThread()
+
+        if (!isEnabled()) {
+            throw RuntimeException("Cannot build view when not enabled")
+        }
+
+        buildView(parent)
+        connectSession()
+
+        return view
+    }
+
+    private fun buildView(parent: ViewGroup) {
+        if (plugin == null || this::view.isInitialized) {
+            return
+        }
+
+        val ssView = plugin.getView(parent)
+        ssView.registerDataProvider(plugin)
+        ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter {
+            override fun startIntent(v: View?, i: Intent?) {
+                activityStarter.startActivity(i, true /* dismissShade */)
+            }
+
+            override fun startPendingIntent(pi: PendingIntent?) {
+                activityStarter.startPendingIntentDismissingKeyguard(pi)
+            }
+        })
+        ssView.setFalsingManager(falsingManager)
+
+        this.smartspaceView = ssView
+        this.view = ssView as View
+
+        updateTextColorFromWallpaper()
+        statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount)
+    }
+
+    private fun connectSession() {
+        if (plugin == null || session != null) {
+            return
+        }
+        val session = smartspaceManager.createSmartspaceSession(
+                SmartspaceConfig.Builder(context, "lockscreen").build())
+        session.addOnTargetsAvailableListener(uiExecutor, sessionListener)
+
+        userTracker.addCallback(userTrackerCallback, uiExecutor)
+        contentResolver.registerContentObserver(
+                secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
+                true,
+                settingsObserver,
+                UserHandle.USER_ALL
+        )
+        configurationController.addCallback(configChangeListener)
+        statusBarStateController.addCallback(statusBarStateListener)
+
+        this.session = session
+
+        reloadSmartspace()
+    }
+
+    /**
+     * Disconnects the smartspace view from the smartspace service and cleans up any resources.
+     * Calling [buildAndConnectView] again will cause the same view to be reconnected to the
+     * service.
+     */
+    fun disconnect() {
+        execution.assertIsMainThread()
+
+        if (session == null) {
+            return
+        }
+
+        session?.let {
+            it.removeOnTargetsAvailableListener(sessionListener)
+            it.close()
+        }
+        userTracker.removeCallback(userTrackerCallback)
+        contentResolver.unregisterContentObserver(settingsObserver)
+        configurationController.removeCallback(configChangeListener)
+        statusBarStateController.removeCallback(statusBarStateListener)
+        session = null
+
+        plugin?.onTargetsAvailable(emptyList())
+    }
+
+    fun addListener(listener: SmartspaceTargetListener) {
+        execution.assertIsMainThread()
+        plugin?.registerListener(listener)
+    }
+
+    fun removeListener(listener: SmartspaceTargetListener) {
+        execution.assertIsMainThread()
+        plugin?.unregisterListener(listener)
+    }
+
+    private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets ->
+        execution.assertIsMainThread()
+        val filteredTargets = targets.filter(::filterSmartspaceTarget)
+        plugin?.onTargetsAvailable(filteredTargets)
+    }
+
+    private val userTrackerCallback = object : UserTracker.Callback {
+        override fun onUserChanged(newUser: Int, userContext: Context) {
+            execution.assertIsMainThread()
+            reloadSmartspace()
+        }
+
+        override fun onProfilesChanged(profiles: List<UserInfo>) {
+        }
+    }
+
+    private val settingsObserver = object : ContentObserver(handler) {
+        override fun onChange(selfChange: Boolean, uri: Uri?) {
+            execution.assertIsMainThread()
+            reloadSmartspace()
+        }
+    }
+
+    private val configChangeListener = object : ConfigurationController.ConfigurationListener {
+        override fun onThemeChanged() {
+            execution.assertIsMainThread()
+            updateTextColorFromWallpaper()
+        }
+    }
+
+    private val statusBarStateListener = object : StatusBarStateController.StateListener {
+        override fun onDozeAmountChanged(linear: Float, eased: Float) {
+            execution.assertIsMainThread()
+            smartspaceView.setDozeAmount(eased)
+        }
+    }
+
+    private fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean {
+        return when (t.userHandle) {
+            userTracker.userHandle -> {
+                !t.isSensitive || showSensitiveContentForCurrentUser
+            }
+            managedUserHandle -> {
+                // Really, this should be "if this managed profile is associated with the current
+                // active user", but we don't have a good way to check that, so instead we cheat:
+                // Only the primary user can have an associated managed profile, so only show
+                // content for the managed profile if the primary user is active
+                userTracker.userHandle.identifier == UserHandle.USER_SYSTEM &&
+                        (!t.isSensitive || showSensitiveContentForManagedUser)
+            }
+            else -> {
+                false
+            }
+        }
+    }
+
+    private fun updateTextColorFromWallpaper() {
+        val wallpaperTextColor = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)
+        smartspaceView.setPrimaryTextColor(wallpaperTextColor)
+    }
+
+    private fun reloadSmartspace() {
+        val setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
+
+        showSensitiveContentForCurrentUser =
+                secureSettings.getIntForUser(setting, 0, userTracker.userId) == 1
+
+        managedUserHandle = getWorkProfileUser()
+        val managedId = managedUserHandle?.identifier
+        if (managedId != null) {
+            showSensitiveContentForManagedUser =
+                    secureSettings.getIntForUser(setting, 0, managedId) == 1
+        }
+
+        session?.requestSmartspaceUpdate()
+    }
+
+    private fun getWorkProfileUser(): UserHandle? {
+        for (userInfo in userTracker.userProfiles) {
+            if (userInfo.isManagedProfile) {
+                return userInfo.userHandle
+            }
+        }
+        return null
+    }
+}
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 120f973..3b64d48f 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
@@ -664,7 +664,7 @@
             mDebugPaint.setColor(Color.CYAN);
             canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
 
-            y = (int) (mAmbientState.getStackY() + mAmbientState.getStackHeight());
+            y = (int) (mAmbientState.getStackY() + mSidePaddings + mAmbientState.getStackHeight());
             mDebugPaint.setColor(Color.BLUE);
             canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
         }
@@ -1148,12 +1148,14 @@
         if (mOnStackYChanged != null) {
             mOnStackYChanged.run();
         }
-
-        final float stackEndHeight = getHeight() - getEmptyBottomMargin() - mTopPadding;
-        mAmbientState.setStackEndHeight(stackEndHeight);
-        mAmbientState.setStackHeight(
-                MathUtils.lerp(stackEndHeight * StackScrollAlgorithm.START_FRACTION,
-                        stackEndHeight, fraction));
+        if (mQsExpansionFraction <= 0) {
+            final float stackEndHeight = Math.max(0f,
+                    getHeight() - getEmptyBottomMargin() - stackY - mSidePaddings);
+            mAmbientState.setStackEndHeight(stackEndHeight);
+            mAmbientState.setStackHeight(
+                    MathUtils.lerp(stackEndHeight * StackScrollAlgorithm.START_FRACTION,
+                            stackEndHeight, fraction));
+        }
     }
 
     void setOnStackYChanged(Runnable onStackYChanged) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index d94d030..b2d39a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -58,6 +58,7 @@
     private int mStatusBarHeight;
     private float mHeadsUpInset;
     private int mPinnedZTranslationExtra;
+    private float mNotificationScrimPadding;
 
     public StackScrollAlgorithm(
             Context context,
@@ -82,6 +83,7 @@
         mPinnedZTranslationExtra = res.getDimensionPixelSize(
                 R.dimen.heads_up_pinned_elevation);
         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
+        mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
     }
 
     /**
@@ -258,6 +260,9 @@
         // expanded. Consider updating these states in updateContentView instead so that we don't
         // have to recalculate in every frame.
         float currentY = -scrollY;
+        if (!ambientState.isOnKeyguard()) {
+            currentY += mNotificationScrimPadding;
+        }
         float previousY = 0;
         state.firstViewInShelf = null;
         state.viewHeightBeforeShelf = -1;
@@ -318,6 +323,9 @@
             AmbientState ambientState) {
         // The y coordinate of the current child.
         float currentYPosition = -algorithmState.scrollY;
+        if (!ambientState.isOnKeyguard()) {
+            currentYPosition += mNotificationScrimPadding;
+        }
         int childCount = algorithmState.visibleChildren.size();
         for (int i = 0; i < childCount; i++) {
             currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
index b9fe9c4a..c5a155e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -326,11 +326,13 @@
         // Show the ongoing call chip only if there is an ongoing call *and* notification icons
         // are allowed. (The ongoing call chip occupies the same area as the notification icons,
         // so if the icons are disabled then the call chip should be, too.)
-        if (hasOngoingCall && !disableNotifications) {
+        boolean showOngoingCallChip = hasOngoingCall && !disableNotifications;
+        if (showOngoingCallChip) {
             showOngoingCallChip(animate);
         } else {
             hideOngoingCallChip(animate);
         }
+        mOngoingCallController.notifyChipVisibilityChanged(showOngoingCallChip);
     }
 
     private boolean shouldHideNotificationIcons() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 16abd12..075a0c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -2072,14 +2072,13 @@
         final int qsPanelBottomY = calculateQsBottomPosition(getQsExpansionFraction());
         final boolean visible = (getQsExpansionFraction() > 0 || qsPanelBottomY > 0)
                 && !mShouldUseSplitNotificationShade;
-        final float notificationTop = mAmbientState.getStackY()
-                - mNotificationScrimPadding
-                - mAmbientState.getScrollY();
+        final float notificationTop = mAmbientState.getStackY() - mAmbientState.getScrollY();
         setQsExpansionEnabled(mAmbientState.getScrollY() == 0);
 
         int radius = mScrimCornerRadius;
         if (!mShouldUseSplitNotificationShade) {
-            top = (int) Math.min(qsPanelBottomY, notificationTop);
+            top = (int) (isOnKeyguard() ? Math.min(qsPanelBottomY, notificationTop)
+                    : notificationTop);
             bottom = getView().getBottom();
             left = getView().getLeft();
             right = getView().getRight();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 6d1df5b..e9d256c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -49,7 +49,8 @@
     private val systemClock: SystemClock,
     private val activityStarter: ActivityStarter,
     @Main private val mainExecutor: Executor,
-    private val iActivityManager: IActivityManager
+    private val iActivityManager: IActivityManager,
+    private val logger: OngoingCallLogger
 ) : CallbackController<OngoingCallListener> {
 
     /** Null if there's no ongoing call. */
@@ -104,7 +105,7 @@
     /**
      * Sets the chip view that will contain ongoing call information.
      *
-     * Should only be called from [CollapedStatusBarFragment].
+     * Should only be called from [CollapsedStatusBarFragment].
      */
     fun setChipView(chipView: ViewGroup) {
         this.chipView = chipView
@@ -113,6 +114,16 @@
         }
     }
 
+
+    /**
+     * Called when the chip's visibility may have changed.
+     *
+     * Should only be called from [CollapsedStatusBarFragment].
+     */
+    fun notifyChipVisibilityChanged(chipIsVisible: Boolean) {
+        logger.logChipVisibilityChanged(chipIsVisible)
+    }
+
     /**
      * Returns true if there's an active ongoing call that should be displayed in a status bar chip.
      */
@@ -150,6 +161,7 @@
             timeView.start()
 
             currentChipView.setOnClickListener {
+                logger.logChipClicked()
                 activityStarter.postStartActivityDismissingKeyguard(
                         currentOngoingCallInfo.intent, 0,
                         ActivityLaunchAnimator.Controller.fromView(it))
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt
new file mode 100644
index 0000000..177f215
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.ongoingcall
+
+import androidx.annotation.VisibleForTesting
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/** A class to log events for the ongoing call chip. */
+@SysUISingleton
+class OngoingCallLogger @Inject constructor(private val logger: UiEventLogger) {
+
+    private var chipIsVisible: Boolean = false
+
+    /** Logs that the ongoing call chip was clicked. */
+    fun logChipClicked() {
+        logger.log(OngoingCallEvents.ONGOING_CALL_CLICKED)
+    }
+
+    /**
+     * If needed, logs that the ongoing call chip's visibility has changed.
+     *
+     * For now, only logs when the chip changes from not visible to visible.
+     */
+    fun logChipVisibilityChanged(chipIsVisible: Boolean) {
+        if (chipIsVisible && chipIsVisible != this.chipIsVisible) {
+            logger.log(OngoingCallEvents.ONGOING_CALL_VISIBLE)
+        }
+        this.chipIsVisible = chipIsVisible
+    }
+
+    @VisibleForTesting
+    enum class OngoingCallEvents(val metricId: Int) : UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "The ongoing call chip became visible")
+        ONGOING_CALL_VISIBLE(813),
+
+        @UiEvent(doc = "The ongoing call chip was clicked")
+        ONGOING_CALL_CLICKED(814);
+
+        override fun getId() = metricId
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 8c5f74d..98467d4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -19,30 +19,20 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.smartspace.SmartspaceTarget;
-import android.content.Context;
-import android.content.pm.UserInfo;
 import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
-import android.util.AttributeSet;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
-import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.keyguard.clock.ClockManager;
@@ -50,21 +40,14 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.BcSmartspaceDataPlugin;
-import com.android.systemui.plugins.BcSmartspaceDataPlugin.IntentStarter;
 import com.android.systemui.plugins.ClockPlugin;
-import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
 import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -74,78 +57,54 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.verification.VerificationMode;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.Executor;
-
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class KeyguardClockSwitchControllerTest extends SysuiTestCase {
 
     @Mock
+    private KeyguardClockSwitch mView;
+    @Mock
     private StatusBarStateController mStatusBarStateController;
     @Mock
     private SysuiColorExtractor mColorExtractor;
     @Mock
     private ClockManager mClockManager;
     @Mock
-    private KeyguardClockSwitch mView;
-    @Mock
-    private NotificationIconContainer mNotificationIcons;
-    @Mock
-    private ClockPlugin mClockPlugin;
-    @Mock
-    ColorExtractor.GradientColors mGradientColors;
-    @Mock
     KeyguardSliceViewController mKeyguardSliceViewController;
     @Mock
-    Resources mResources;
-    @Mock
     NotificationIconAreaController mNotificationIconAreaController;
     @Mock
     BroadcastDispatcher mBroadcastDispatcher;
     @Mock
-    private FeatureFlags mFeatureFlags;
+    BatteryController mBatteryController;
     @Mock
-    private Executor mExecutor;
+    KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock
+    KeyguardBypassController mBypassController;
+    @Mock
+    LockscreenSmartspaceController mSmartspaceController;
+
+    @Mock
+    Resources mResources;
+    @Mock
+    private ClockPlugin mClockPlugin;
+    @Mock
+    ColorExtractor.GradientColors mGradientColors;
+
+    @Mock
+    private NotificationIconContainer mNotificationIcons;
     @Mock
     private AnimatableClockView mClockView;
     @Mock
     private AnimatableClockView mLargeClockView;
     @Mock
     private FrameLayout mLargeClockFrame;
-    @Mock
-    BatteryController mBatteryController;
-    @Mock
-    ConfigurationController mConfigurationController;
-    @Mock
-    Optional<BcSmartspaceDataPlugin> mOptionalSmartspaceDataProvider;
-    @Mock
-    BcSmartspaceDataPlugin mSmartspaceDataProvider;
-    @Mock
-    SmartspaceView mSmartspaceView;
-    @Mock
-    ActivityStarter mActivityStarter;
-    @Mock
-    FalsingManager mFalsingManager;
-    @Mock
-    KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @Mock
-    KeyguardBypassController mBypassController;
-    @Mock
-    Handler mHandler;
-    @Mock
-    UserTracker mUserTracker;
-    @Mock
-    SecureSettings mSecureSettings;
+
+    private final View mFakeSmartspaceView = new View(mContext);
 
     private KeyguardClockSwitchController mController;
     private View mStatusArea;
 
-    private static final int USER_ID = 5;
-    private static final int MANAGED_USER_ID = 15;
-
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
@@ -162,9 +121,9 @@
         when(mClockView.getContext()).thenReturn(getContext());
         when(mLargeClockView.getContext()).thenReturn(getContext());
 
-        when(mFeatureFlags.isSmartspaceEnabled()).thenReturn(true);
         when(mView.isAttachedToWindow()).thenReturn(true);
         when(mResources.getString(anyInt())).thenReturn("h:mm");
+        when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
         mController = new KeyguardClockSwitchController(
                 mView,
                 mStatusBarStateController,
@@ -173,28 +132,16 @@
                 mKeyguardSliceViewController,
                 mNotificationIconAreaController,
                 mBroadcastDispatcher,
-                mFeatureFlags,
-                mExecutor,
                 mBatteryController,
-                mConfigurationController,
-                mActivityStarter,
-                mFalsingManager,
                 mKeyguardUpdateMonitor,
                 mBypassController,
-                mHandler,
-                mUserTracker,
-                mSecureSettings,
-                mOptionalSmartspaceDataProvider
-        );
+                mSmartspaceController);
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
         when(mColorExtractor.getColors(anyInt())).thenReturn(mGradientColors);
 
         mStatusArea = new View(getContext());
         when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(mStatusArea);
-        when(mOptionalSmartspaceDataProvider.isPresent()).thenReturn(true);
-        when(mOptionalSmartspaceDataProvider.get()).thenReturn(mSmartspaceDataProvider);
-        when(mSmartspaceDataProvider.getView(any())).thenReturn(mSmartspaceView);
     }
 
     @Test
@@ -255,119 +202,34 @@
 
     @Test
     public void testSmartspaceEnabledRemovesKeyguardStatusArea() {
-        when(mFeatureFlags.isSmartspaceEnabled()).thenReturn(true);
+        when(mSmartspaceController.isEnabled()).thenReturn(true);
+        when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
         mController.init();
 
         assertEquals(View.GONE, mStatusArea.getVisibility());
     }
 
     @Test
-    public void testSmartspaceEnabledNoDataProviderShowsKeyguardStatusArea() {
-        when(mFeatureFlags.isSmartspaceEnabled()).thenReturn(true);
-        when(mOptionalSmartspaceDataProvider.isPresent()).thenReturn(false);
-        mController.init();
-
-        assertEquals(View.VISIBLE, mStatusArea.getVisibility());
-    }
-
-    @Test
     public void testSmartspaceDisabledShowsKeyguardStatusArea() {
-        when(mFeatureFlags.isSmartspaceEnabled()).thenReturn(false);
+        when(mSmartspaceController.isEnabled()).thenReturn(false);
         mController.init();
 
         assertEquals(View.VISIBLE, mStatusArea.getVisibility());
     }
 
     @Test
-    public void testThemeChangeNotifiesSmartspace() {
+    public void testDetachRemovesSmartspaceView() {
+        when(mSmartspaceController.isEnabled()).thenReturn(true);
+        when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
         mController.init();
-        verify(mSmartspaceView).setPrimaryTextColor(anyInt());
+        verify(mView).addView(eq(mFakeSmartspaceView), anyInt(), any());
 
-        mController.getConfigurationListener().onThemeChanged();
-        verify(mSmartspaceView, times(2)).setPrimaryTextColor(anyInt());
-    }
+        ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
+                ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+        verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture());
 
-    @Test
-    public void doNotFilterRegularTarget() {
-        setupPrimaryAndManagedUser();
-        mController.init();
-
-        when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(USER_ID))).thenReturn(0);
-        when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(MANAGED_USER_ID)))
-                .thenReturn(0);
-
-        mController.getSettingsObserver().onChange(true, null);
-
-        SmartspaceTarget t = mock(SmartspaceTarget.class);
-        when(t.isSensitive()).thenReturn(false);
-        when(t.getUserHandle()).thenReturn(new UserHandle(USER_ID));
-        assertEquals(false, mController.filterSmartspaceTarget(t));
-
-        reset(t);
-        when(t.isSensitive()).thenReturn(false);
-        when(t.getUserHandle()).thenReturn(new UserHandle(MANAGED_USER_ID));
-        assertEquals(false, mController.filterSmartspaceTarget(t));
-    }
-
-    @Test
-    public void filterAllSensitiveTargetsAllUsers() {
-        setupPrimaryAndManagedUser();
-        mController.init();
-
-        when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(USER_ID))).thenReturn(0);
-        when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(MANAGED_USER_ID)))
-                .thenReturn(0);
-
-        mController.getSettingsObserver().onChange(true, null);
-
-        SmartspaceTarget t = mock(SmartspaceTarget.class);
-        when(t.isSensitive()).thenReturn(true);
-        when(t.getUserHandle()).thenReturn(new UserHandle(USER_ID));
-        assertEquals(true, mController.filterSmartspaceTarget(t));
-
-        reset(t);
-        when(t.isSensitive()).thenReturn(true);
-        when(t.getUserHandle()).thenReturn(new UserHandle(MANAGED_USER_ID));
-        assertEquals(true, mController.filterSmartspaceTarget(t));
-    }
-
-    @Test
-    public void filterSensitiveManagedUserTargets() {
-        setupPrimaryAndManagedUser();
-        mController.init();
-
-        when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(USER_ID))).thenReturn(1);
-        when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(MANAGED_USER_ID)))
-                .thenReturn(0);
-
-        mController.getSettingsObserver().onChange(true, null);
-
-        SmartspaceTarget t = mock(SmartspaceTarget.class);
-        when(t.isSensitive()).thenReturn(true);
-        when(t.getUserHandle()).thenReturn(new UserHandle(USER_ID));
-        assertEquals(false, mController.filterSmartspaceTarget(t));
-
-        reset(t);
-        when(t.isSensitive()).thenReturn(true);
-        when(t.getUserHandle()).thenReturn(new UserHandle(MANAGED_USER_ID));
-        assertEquals(true, mController.filterSmartspaceTarget(t));
-    }
-
-    private void setupPrimaryAndManagedUser() {
-        UserInfo userInfo = mock(UserInfo.class);
-        when(userInfo.isManagedProfile()).thenReturn(true);
-        when(userInfo.getUserHandle()).thenReturn(new UserHandle(MANAGED_USER_ID));
-        when(mUserTracker.getUserProfiles()).thenReturn(List.of(userInfo));
-
-        when(mUserTracker.getUserId()).thenReturn(USER_ID);
-        when(mUserTracker.getUserHandle()).thenReturn(new UserHandle(USER_ID));
-    }
-
-    private void setupPrimaryAndNoManagedUser() {
-        when(mUserTracker.getUserProfiles()).thenReturn(Collections.emptyList());
-
-        when(mUserTracker.getUserId()).thenReturn(USER_ID);
-        when(mUserTracker.getUserHandle()).thenReturn(new UserHandle(USER_ID));
+        listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView);
+        verify(mView).removeView(mFakeSmartspaceView);
     }
 
     private void verifyAttachment(VerificationMode times) {
@@ -377,25 +239,4 @@
                 any(ColorExtractor.OnColorsChangedListener.class));
         verify(mView, times).updateColors(mGradientColors);
     }
-
-    private static class SmartspaceView extends View
-            implements BcSmartspaceDataPlugin.SmartspaceView {
-        SmartspaceView(Context context, AttributeSet attrs) {
-            super(context, attrs);
-        }
-
-        public void registerDataProvider(BcSmartspaceDataPlugin plugin) { }
-
-        public void setPrimaryTextColor(int color) { }
-
-        public void setDozeAmount(float amount) { }
-
-        public void setIntentStarter(IntentStarter intentStarter) { }
-
-        public void setFalsingManager(FalsingManager falsingManager) { }
-
-        public void setDnd(@Nullable Drawable dndIcon, @Nullable String description) { }
-
-        public void setNextAlarm(@Nullable Drawable dndIcon, @Nullable String description) { }
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java
index c050b62..ba2b37c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java
@@ -30,6 +30,7 @@
 import android.testing.ViewUtils;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.widget.FrameLayout;
 
 import androidx.test.filters.SmallTest;
 
@@ -58,24 +59,26 @@
     private DetailAdapter mMockDetailAdapter;
     private TestableLooper mTestableLooper;
     private UiEventLoggerFake mUiEventLogger;
+    private FrameLayout mParent;
 
     @Before
     public void setup() throws Exception {
         mTestableLooper = TestableLooper.get(this);
         mUiEventLogger = QSEvents.INSTANCE.setLoggerForTesting();
 
-        mTestableLooper.runWithLooper(() -> {
-            mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class);
-            mActivityStarter = mDependency.injectMockDependency(ActivityStarter.class);
-            mQsDetail = (QSDetail) LayoutInflater.from(mContext).inflate(R.layout.qs_detail, null);
-            mQsPanelController = mock(QSPanelController.class);
-            mQuickHeader = mock(QuickStatusBarHeader.class);
-            mQsDetail.setQsPanel(mQsPanelController, mQuickHeader, mock(QSFooter.class));
+        mParent = new FrameLayout(mContext);
+        mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class);
+        mActivityStarter = mDependency.injectMockDependency(ActivityStarter.class);
+        LayoutInflater.from(mContext).inflate(R.layout.qs_detail, mParent);
+        mQsDetail = (QSDetail) mParent.getChildAt(0);
 
-            mMockDetailAdapter = mock(DetailAdapter.class);
-            when(mMockDetailAdapter.createDetailView(any(), any(), any()))
-                    .thenReturn(mock(View.class));
-        });
+        mQsPanelController = mock(QSPanelController.class);
+        mQuickHeader = mock(QuickStatusBarHeader.class);
+        mQsDetail.setQsPanel(mQsPanelController, mQuickHeader, mock(QSFooter.class));
+
+        mMockDetailAdapter = mock(DetailAdapter.class);
+        when(mMockDetailAdapter.createDetailView(any(), any(), any()))
+                .thenReturn(new View(mContext));
 
         // Only detail in use is the user detail
         when(mMockDetailAdapter.openDetailEvent())
@@ -84,16 +87,18 @@
                 .thenReturn(QSUserSwitcherEvent.QS_USER_DETAIL_CLOSE);
         when(mMockDetailAdapter.moreSettingsEvent())
                 .thenReturn(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS);
+        ViewUtils.attachView(mParent);
     }
 
     @After
     public void tearDown() {
         QSEvents.INSTANCE.resetLogger();
+        mTestableLooper.processAllMessages();
+        ViewUtils.detachView(mParent);
     }
 
     @Test
     public void testShowDetail_Metrics() {
-        ViewUtils.attachView(mQsDetail);
         mTestableLooper.processAllMessages();
 
         mQsDetail.handleShowingDetail(mMockDetailAdapter, 0, 0, false);
@@ -107,14 +112,10 @@
 
         assertEquals(1, mUiEventLogger.numLogs());
         assertEquals(QSUserSwitcherEvent.QS_USER_DETAIL_CLOSE.getId(), mUiEventLogger.eventId(0));
-
-        ViewUtils.detachView(mQsDetail);
-        mTestableLooper.processAllMessages();
     }
 
     @Test
     public void testMoreSettingsButton() {
-        ViewUtils.attachView(mQsDetail);
         mTestableLooper.processAllMessages();
 
         mQsDetail.handleShowingDetail(mMockDetailAdapter, 0, 0, false);
@@ -127,9 +128,6 @@
         assertEquals(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS.getId(), mUiEventLogger.eventId(0));
 
         verify(mActivityStarter).postStartActivityDismissingKeyguard(any(), anyInt());
-
-        ViewUtils.detachView(mQsDetail);
-        mTestableLooper.processAllMessages();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
new file mode 100644
index 0000000..5366858
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.lockscreen
+
+
+import android.app.smartspace.SmartspaceManager
+import android.app.smartspace.SmartspaceSession
+import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener
+import android.app.smartspace.SmartspaceTarget
+import android.content.ComponentName
+import android.content.ContentResolver
+import android.content.pm.UserInfo
+import android.database.ContentObserver
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import android.view.View
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import com.android.systemui.util.concurrency.FakeExecution
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.util.Optional
+
+@SmallTest
+class LockscreenSmartspaceControllerTest : SysuiTestCase() {
+    @Mock
+    private lateinit var featureFlags: FeatureFlags
+    @Mock
+    private lateinit var smartspaceManager: SmartspaceManager
+    @Mock
+    private lateinit var smartspaceSession: SmartspaceSession
+    @Mock
+    private lateinit var activityStarter: ActivityStarter
+    @Mock
+    private lateinit var falsingManager: FalsingManager
+    @Mock
+    private lateinit var secureSettings: SecureSettings
+    @Mock
+    private lateinit var userTracker: UserTracker
+    @Mock
+    private lateinit var contentResolver: ContentResolver
+    @Mock
+    private lateinit var configurationController: ConfigurationController
+    @Mock
+    private lateinit var statusBarStateController: StatusBarStateController
+    @Mock
+    private lateinit var handler: Handler
+
+    @Mock
+    private lateinit var plugin: BcSmartspaceDataPlugin
+    @Mock
+    private lateinit var controllerListener: SmartspaceTargetListener
+
+    @Captor
+    private lateinit var sessionListenerCaptor: ArgumentCaptor<OnTargetsAvailableListener>
+    @Captor
+    private lateinit var userTrackerCaptor: ArgumentCaptor<UserTracker.Callback>
+    @Captor
+    private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
+    @Captor
+    private lateinit var configChangeListenerCaptor: ArgumentCaptor<ConfigurationListener>
+    @Captor
+    private lateinit var statusBarStateListenerCaptor: ArgumentCaptor<StateListener>
+
+    private lateinit var sessionListener: OnTargetsAvailableListener
+    private lateinit var userListener: UserTracker.Callback
+    private lateinit var settingsObserver: ContentObserver
+    private lateinit var configChangeListener: ConfigurationListener
+    private lateinit var statusBarStateListener: StateListener
+
+    private val clock = FakeSystemClock()
+    private val executor = FakeExecutor(clock)
+    private val execution = FakeExecution()
+    private val fakeParent = FrameLayout(context)
+    private val fakePrivateLockscreenSettingUri = Uri.Builder().appendPath("test").build()
+
+    private val userHandlePrimary: UserHandle = UserHandle(0)
+    private val userHandleManaged: UserHandle = UserHandle(2)
+    private val userHandleSecondary: UserHandle = UserHandle(3)
+
+    private val userList = listOf(
+            mockUserInfo(userHandlePrimary, isManagedProfile = false),
+            mockUserInfo(userHandleManaged, isManagedProfile = true),
+            mockUserInfo(userHandleSecondary, isManagedProfile = false)
+    )
+
+    private lateinit var controller: LockscreenSmartspaceController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(featureFlags.isSmartspaceEnabled).thenReturn(true)
+
+        `when`(secureSettings.getUriFor(PRIVATE_LOCKSCREEN_SETTING))
+                .thenReturn(fakePrivateLockscreenSettingUri)
+        `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession)
+        `when`(plugin.getView(any())).thenReturn(fakeSmartspaceView)
+        `when`(userTracker.userProfiles).thenReturn(userList)
+        `when`(statusBarStateController.dozeAmount).thenReturn(0.5f)
+
+        setActiveUser(userHandlePrimary)
+        setAllowPrivateNotifications(userHandlePrimary, true)
+        setAllowPrivateNotifications(userHandleManaged, true)
+        setAllowPrivateNotifications(userHandleSecondary, true)
+
+        controller = LockscreenSmartspaceController(
+                context,
+                featureFlags,
+                smartspaceManager,
+                activityStarter,
+                falsingManager,
+                secureSettings,
+                userTracker,
+                contentResolver,
+                configurationController,
+                statusBarStateController,
+                execution,
+                executor,
+                handler,
+                Optional.of(plugin)
+                )
+    }
+
+    @Test(expected = RuntimeException::class)
+    fun testThrowsIfFlagIsDisabled() {
+        // GIVEN the feature flag is disabled
+        `when`(featureFlags.isSmartspaceEnabled).thenReturn(false)
+
+        // WHEN we try to build the view
+        controller.buildAndConnectView(fakeParent)
+
+        // THEN an exception is thrown
+    }
+
+    @Test
+    fun testListenersAreRegistered() {
+        // GIVEN a listener is added after a session is created
+        connectSession()
+
+        // WHEN a listener is registered
+        controller.addListener(controllerListener)
+
+        // THEN the listener is registered to the underlying plugin
+        verify(plugin).registerListener(controllerListener)
+    }
+
+    @Test
+    fun testEarlyRegisteredListenersAreAttachedAfterConnected() {
+        // GIVEN a listener that is registered before the session is created
+        controller.addListener(controllerListener)
+
+        // WHEN the session is created
+        connectSession()
+
+        // THEN the listener is subsequently registered
+        verify(plugin).registerListener(controllerListener)
+    }
+
+    @Test
+    fun testEmptyListIsEmittedAfterDisconnect() {
+        // GIVEN a registered listener on an active session
+        connectSession()
+        clearInvocations(plugin)
+
+        // WHEN the session is closed
+        controller.disconnect()
+
+        // THEN the listener receives an empty list of targets
+        verify(plugin).onTargetsAvailable(emptyList())
+    }
+
+    @Test
+    fun testUserChangeReloadsSmartspace() {
+        // GIVEN a connected smartspace session
+        connectSession()
+
+        // WHEN the active user changes
+        userListener.onUserChanged(-1, context)
+
+        // THEN we request a new smartspace update
+        verify(smartspaceSession).requestSmartspaceUpdate()
+    }
+
+    @Test
+    fun testSettingsChangeReloadsSmartspace() {
+        // GIVEN a connected smartspace session
+        connectSession()
+
+        // WHEN the lockscreen privacy setting changes
+        settingsObserver.onChange(true, null)
+
+        // THEN we request a new smartspace update
+        verify(smartspaceSession).requestSmartspaceUpdate()
+    }
+
+    @Test
+    fun testThemeChangeUpdatesTextColor() {
+        // GIVEN a connected smartspace session
+        connectSession()
+
+        // WHEN the theme changes
+        configChangeListener.onThemeChanged()
+
+        // We update the new text color to match the wallpaper color
+        verify(fakeSmartspaceView).setPrimaryTextColor(anyInt())
+    }
+
+    @Test
+    fun testDozeAmountChangeUpdatesView() {
+        // GIVEN a connected smartspace session
+        connectSession()
+
+        // WHEN the doze amount changes
+        statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
+
+        // We pass that along to the view
+        verify(fakeSmartspaceView).setDozeAmount(0.7f)
+    }
+
+    @Test
+    fun testSensitiveTargetsAreNotFilteredIfAllowed() {
+        // GIVEN the active and managed users allow sensitive content
+        connectSession()
+
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeTarget(1, userHandlePrimary, isSensitive = true),
+                makeTarget(2, userHandleManaged, isSensitive = true),
+                makeTarget(3, userHandlePrimary, isSensitive = true)
+        )
+        sessionListener.onTargetsAvailable(targets)
+
+        // THEN all sensitive content is still shown
+        verify(plugin).onTargetsAvailable(eq(targets))
+    }
+
+    @Test
+    fun testNonSensitiveTargetsAreNeverFiltered() {
+        // GIVEN the active user doesn't allow sensitive lockscreen content
+        setAllowPrivateNotifications(userHandlePrimary, false)
+        connectSession()
+
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeTarget(1, userHandlePrimary),
+                makeTarget(2, userHandlePrimary),
+                makeTarget(3, userHandlePrimary)
+        )
+        sessionListener.onTargetsAvailable(targets)
+
+        // THEN all non-sensitive content is still shown
+        verify(plugin).onTargetsAvailable(eq(targets))
+    }
+
+    @Test
+    fun testSensitiveTargetsAreFilteredOutForAppropriateUsers() {
+        // GIVEN the active and managed users don't allow sensitive lockscreen content
+        setAllowPrivateNotifications(userHandlePrimary, false)
+        setAllowPrivateNotifications(userHandleManaged, false)
+        connectSession()
+
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeTarget(0, userHandlePrimary),
+                makeTarget(1, userHandlePrimary, isSensitive = true),
+                makeTarget(2, userHandleManaged, isSensitive = true),
+                makeTarget(3, userHandleManaged),
+                makeTarget(4, userHandlePrimary, isSensitive = true),
+                makeTarget(5, userHandlePrimary),
+                makeTarget(6, userHandleSecondary, isSensitive = true)
+        )
+        sessionListener.onTargetsAvailable(targets)
+
+        // THEN only non-sensitive content from those accounts is shown
+        verify(plugin).onTargetsAvailable(eq(listOf(
+                targets[0],
+                targets[3],
+                targets[5]
+        )))
+    }
+
+    @Test
+    fun testSettingsAreReloaded() {
+        // GIVEN a connected session where the privacy settings later flip to false
+        connectSession()
+        setAllowPrivateNotifications(userHandlePrimary, false)
+        setAllowPrivateNotifications(userHandleManaged, false)
+        settingsObserver.onChange(true, fakePrivateLockscreenSettingUri)
+
+        // WHEN we receive a new list of targets
+        val targets = listOf(
+                makeTarget(1, userHandlePrimary, isSensitive = true),
+                makeTarget(2, userHandleManaged, isSensitive = true),
+                makeTarget(4, userHandlePrimary, isSensitive = true)
+        )
+        sessionListener.onTargetsAvailable(targets)
+
+        // THEN we filter based on the new settings values
+        verify(plugin).onTargetsAvailable(emptyList())
+    }
+
+    @Test
+    fun testRecognizeSwitchToSecondaryUser() {
+        // GIVEN an inactive secondary user that doesn't allow sensitive content
+        setAllowPrivateNotifications(userHandleSecondary, false)
+        connectSession()
+
+        // WHEN the secondary user becomes the active user
+        setActiveUser(userHandleSecondary)
+        userListener.onUserChanged(userHandleSecondary.identifier, context)
+
+        // WHEN we receive a new list of targets
+        val targets = listOf(
+                makeTarget(0, userHandlePrimary),
+                makeTarget(1, userHandleSecondary),
+                makeTarget(2, userHandleSecondary, isSensitive = true),
+                makeTarget(3, userHandleManaged),
+                makeTarget(4, userHandleSecondary),
+                makeTarget(5, userHandleManaged),
+                makeTarget(6, userHandlePrimary)
+        )
+        sessionListener.onTargetsAvailable(targets)
+
+        // THEN only non-sensitive content from the secondary user is shown
+        verify(plugin).onTargetsAvailable(listOf(
+                targets[1],
+                targets[4]
+        ))
+    }
+
+    @Test
+    fun testUnregisterListenersOnCleanup() {
+        // GIVEN a connected session
+        connectSession()
+
+        // WHEN we are told to cleanup
+        controller.disconnect()
+
+        // THEN we disconnect from the session and unregister any listeners
+        verify(smartspaceSession).removeOnTargetsAvailableListener(sessionListener)
+        verify(smartspaceSession).close()
+        verify(userTracker).removeCallback(userListener)
+        verify(contentResolver).unregisterContentObserver(settingsObserver)
+        verify(configurationController).removeCallback(configChangeListener)
+        verify(statusBarStateController).removeCallback(statusBarStateListener)
+    }
+
+    @Test
+    fun testBuildViewIsIdempotent() {
+        // GIVEN a connected session
+        connectSession()
+        clearInvocations(plugin)
+
+        // WHEN we disconnect and then reconnect
+        controller.disconnect()
+        controller.buildAndConnectView(fakeParent)
+
+        // THEN the view is not rebuilt
+        verify(plugin, never()).getView(any())
+        assertEquals(fakeSmartspaceView, controller.view)
+    }
+
+    @Test
+    fun testDoubleConnectIsIgnored() {
+        // GIVEN a connected session
+        connectSession()
+        clearInvocations(smartspaceManager)
+        clearInvocations(plugin)
+
+        // WHEN we're asked to connect a second time
+        controller.buildAndConnectView(fakeParent)
+
+        // THEN the existing view and session are reused
+        verify(smartspaceManager, never()).createSmartspaceSession(any())
+        verify(plugin, never()).getView(any())
+        assertEquals(fakeSmartspaceView, controller.view)
+    }
+
+    private fun connectSession(): View {
+        val view = controller.buildAndConnectView(fakeParent)
+
+        verify(smartspaceSession)
+                .addOnTargetsAvailableListener(any(), capture(sessionListenerCaptor))
+        sessionListener = sessionListenerCaptor.value
+
+        verify(userTracker).addCallback(capture(userTrackerCaptor), any())
+        userListener = userTrackerCaptor.value
+
+        verify(contentResolver).registerContentObserver(
+                eq(fakePrivateLockscreenSettingUri),
+                eq(true),
+                capture(settingsObserverCaptor),
+                eq(UserHandle.USER_ALL))
+        settingsObserver = settingsObserverCaptor.value
+
+        verify(configurationController).addCallback(configChangeListenerCaptor.capture())
+        configChangeListener = configChangeListenerCaptor.value
+
+        verify(statusBarStateController).addCallback(statusBarStateListenerCaptor.capture())
+        statusBarStateListener = statusBarStateListenerCaptor.value
+
+        verify(smartspaceSession).requestSmartspaceUpdate()
+        clearInvocations(smartspaceSession)
+
+        verify(fakeSmartspaceView).setPrimaryTextColor(anyInt())
+        verify(fakeSmartspaceView).setDozeAmount(0.5f)
+        clearInvocations(fakeSmartspaceView)
+
+        return view
+    }
+
+    private fun setActiveUser(userHandle: UserHandle) {
+        `when`(userTracker.userId).thenReturn(userHandle.identifier)
+        `when`(userTracker.userHandle).thenReturn(userHandle)
+    }
+
+    private fun mockUserInfo(userHandle: UserHandle, isManagedProfile: Boolean): UserInfo {
+        val userInfo = mock(UserInfo::class.java)
+        `when`(userInfo.userHandle).thenReturn(userHandle)
+        `when`(userInfo.isManagedProfile).thenReturn(isManagedProfile)
+        return userInfo
+    }
+
+    fun makeTarget(
+        id: Int,
+        userHandle: UserHandle,
+        isSensitive: Boolean = false
+    ): SmartspaceTarget {
+        return SmartspaceTarget.Builder(
+                "target$id",
+                ComponentName("testpackage", "testclass$id"),
+                userHandle)
+                .setSensitive(isSensitive)
+                .build()
+    }
+
+    private fun setAllowPrivateNotifications(user: UserHandle, value: Boolean) {
+        `when`(secureSettings.getIntForUser(
+                eq(PRIVATE_LOCKSCREEN_SETTING),
+                anyInt(),
+                eq(user.identifier))
+        ).thenReturn(if (value) 1 else 0)
+    }
+
+    private val fakeSmartspaceView = spy(object : View(context), SmartspaceView {
+        override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+        }
+
+        override fun setPrimaryTextColor(color: Int) {
+        }
+
+        override fun setDozeAmount(amount: Float) {
+        }
+
+        override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+        }
+
+        override fun setFalsingManager(falsingManager: FalsingManager?) {
+        }
+
+        override fun setDnd(image: Drawable?, description: String?) {
+        }
+
+        override fun setNextAlarm(image: Drawable?, description: String?) {
+        }
+    })
+}
+
+private const val PRIVATE_LOCKSCREEN_SETTING =
+        Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index 896e330..9a7ab28 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -29,6 +29,7 @@
 import android.view.LayoutInflater
 import android.widget.LinearLayout
 import androidx.test.filters.SmallTest
+import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.plugins.ActivityStarter
@@ -70,6 +71,7 @@
 
     private val clock = FakeSystemClock()
     private val mainExecutor = FakeExecutor(clock)
+    private val uiEventLoggerFake = UiEventLoggerFake()
 
     private lateinit var controller: OngoingCallController
     private lateinit var notifCollectionListener: NotifCollectionListener
@@ -99,7 +101,8 @@
                 clock,
                 mockActivityStarter,
                 mainExecutor,
-                mockIActivityManager)
+                mockIActivityManager,
+                OngoingCallLogger(uiEventLoggerFake))
         controller.init()
         controller.addCallback(mockOngoingCallListener)
         controller.setChipView(chipView)
@@ -256,6 +259,28 @@
                 .onOngoingCallStateChanged(anyBoolean())
     }
 
+    @Test
+    fun chipClicked_clickEventLogged() {
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        chipView.performClick()
+
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+        assertThat(uiEventLoggerFake.eventId(0))
+                .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id)
+    }
+
+    @Test
+    fun notifyChipVisibilityChanged_visibleEventLogged() {
+        controller.notifyChipVisibilityChanged(true)
+
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+        assertThat(uiEventLoggerFake.eventId(0))
+                .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id)
+    }
+    // Other tests for notifyChipVisibilityChanged are in [OngoingCallLogger], since
+    // [OngoingCallController.notifyChipVisibilityChanged] just delegates to that class.
+
     private fun createOngoingCallNotifEntry(): NotificationEntry {
         val notificationEntryBuilder = NotificationEntryBuilder()
         notificationEntryBuilder.modifyNotification(context).style = ongoingCallStyle
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt
new file mode 100644
index 0000000..ecec124
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.ongoingcall
+
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class OngoingCallLoggerTest : SysuiTestCase() {
+    private val uiEventLoggerFake = UiEventLoggerFake()
+    private val ongoingCallLogger = OngoingCallLogger(uiEventLoggerFake)
+
+    @Test
+    fun logChipClicked_clickEventLogged() {
+        ongoingCallLogger.logChipClicked()
+
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+        assertThat(uiEventLoggerFake.eventId(0))
+                .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id)
+    }
+
+    @Test
+    fun logChipVisibilityChanged_changeFromInvisibleToVisible_visibleEventLogged() {
+        ongoingCallLogger.logChipVisibilityChanged(false)
+        ongoingCallLogger.logChipVisibilityChanged(true)
+
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+        assertThat(uiEventLoggerFake.eventId(0))
+                .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id)
+    }
+
+    @Test
+    fun logChipVisibilityChanged_changeFromVisibleToInvisible_eventNotLogged() {
+        // Setting the chip to visible here will trigger a log
+        ongoingCallLogger.logChipVisibilityChanged(true)
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+
+        ongoingCallLogger.logChipVisibilityChanged(false)
+
+        // Expect that there were no new logs
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+    }
+
+    @Test
+    fun logChipVisibilityChanged_visibleThenVisibleAgain_eventNotLogged() {
+        // Setting the chip to visible here will trigger a log
+        ongoingCallLogger.logChipVisibilityChanged(true)
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+
+        ongoingCallLogger.logChipVisibilityChanged(true)
+
+        // Expect that there were no new logs
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+    }
+}
diff --git a/services/core/java/com/android/server/SensorPrivacyService.java b/services/core/java/com/android/server/SensorPrivacyService.java
index a05eb68..ca59ce3 100644
--- a/services/core/java/com/android/server/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/SensorPrivacyService.java
@@ -20,12 +20,17 @@
 import static android.app.ActivityManager.RunningServiceInfo;
 import static android.app.ActivityManager.RunningTaskInfo;
 import static android.app.ActivityManager.getCurrentUser;
+import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.MODE_IGNORED;
 import static android.app.AppOpsManager.OP_CAMERA;
+import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
+import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
 import static android.content.Intent.EXTRA_PACKAGE_NAME;
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.hardware.SensorPrivacyManager.EXTRA_ALL_SENSORS;
 import static android.hardware.SensorPrivacyManager.EXTRA_SENSOR;
 import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
 import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
@@ -60,6 +65,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -141,6 +147,7 @@
     private static final int VER0_INDIVIDUAL_ENABLED = 1;
     private static final int VER1_ENABLED = 0;
     private static final int VER1_INDIVIDUAL_ENABLED = 1;
+    public static final int REMINDER_DIALOG_DELAY_MILLIS = 500;
 
     private final Context mContext;
     private final SensorPrivacyServiceImpl mSensorPrivacyServiceImpl;
@@ -206,6 +213,36 @@
         private ArrayMap<Pair<String, UserHandle>, ArrayList<IBinder>> mSuppressReminders =
                 new ArrayMap<>();
 
+        private final ArrayMap<SensorUseReminderDialogInfo, ArraySet<Integer>>
+                mQueuedSensorUseReminderDialogs = new ArrayMap<>();
+
+        private class SensorUseReminderDialogInfo {
+            private int mTaskId;
+            private UserHandle mUser;
+            private String mPackageName;
+
+            SensorUseReminderDialogInfo(int taskId, UserHandle user, String packageName) {
+                mTaskId = taskId;
+                mUser = user;
+                mPackageName = packageName;
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) return true;
+                if (o == null || !(o instanceof SensorUseReminderDialogInfo)) return false;
+                SensorUseReminderDialogInfo that = (SensorUseReminderDialogInfo) o;
+                return mTaskId == that.mTaskId
+                        && Objects.equals(mUser, that.mUser)
+                        && Objects.equals(mPackageName, that.mPackageName);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mTaskId, mUser, mPackageName);
+            }
+        }
+
         SensorPrivacyServiceImpl() {
             mHandler = new SensorPrivacyHandler(FgThread.get().getLooper(), mContext);
             File sensorPrivacyFile = new File(Environment.getDataSystemDirectory(),
@@ -228,7 +265,8 @@
                 }
             }
 
-            int[] micAndCameraOps = new int[]{OP_RECORD_AUDIO, OP_CAMERA};
+            int[] micAndCameraOps = new int[]{OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE,
+                    OP_CAMERA, OP_PHONE_CALL_CAMERA};
             mAppOpsManager.startWatchingNoted(micAndCameraOps, this);
             mAppOpsManager.startWatchingStarted(micAndCameraOps, this);
 
@@ -254,15 +292,29 @@
         public void onOpNoted(int code, int uid, String packageName,
                 String attributionTag, @AppOpsManager.OpFlags int flags,
                 @AppOpsManager.Mode int result) {
-            if (result != MODE_IGNORED || (flags & AppOpsManager.OP_FLAGS_ALL_TRUSTED) == 0) {
+            if ((flags & AppOpsManager.OP_FLAGS_ALL_TRUSTED) == 0) {
                 return;
             }
 
             int sensor;
-            if (code == OP_RECORD_AUDIO) {
-                sensor = MICROPHONE;
+            if (result == MODE_IGNORED) {
+                if (code == OP_RECORD_AUDIO) {
+                    sensor = MICROPHONE;
+                } else if (code == OP_CAMERA) {
+                    sensor = CAMERA;
+                } else {
+                    return;
+                }
+            } else if (result == MODE_ALLOWED) {
+                if (code == OP_PHONE_CALL_MICROPHONE) {
+                    sensor = MICROPHONE;
+                } else if (code == OP_PHONE_CALL_CAMERA) {
+                    sensor = CAMERA;
+                } else {
+                    return;
+                }
             } else {
-                sensor = CAMERA;
+                return;
             }
 
             long token = Binder.clearCallingIdentity();
@@ -294,6 +346,11 @@
                 }
             }
 
+            if (uid == Process.SYSTEM_UID) {
+                enqueueSensorUseReminderDialogAsync(-1, user, packageName, sensor);
+                return;
+            }
+
             // TODO: Handle reminders with multiple sensors
 
             // - If we have a likely activity that triggered the sensor use overlay a dialog over
@@ -312,7 +369,7 @@
                 if (task.isVisible && task.topActivity.getPackageName().equals(packageName)) {
                     if (task.isFocused) {
                         // There is the one focused activity
-                        showSensorUseReminderDialog(task.taskId, user, packageName, sensor);
+                        enqueueSensorUseReminderDialogAsync(task.taskId, user, packageName, sensor);
                         return;
                     }
 
@@ -323,7 +380,7 @@
             // TODO: Test this case
             // There is one or more non-focused activity
             if (tasksOfPackageUsingSensor.size() == 1) {
-                showSensorUseReminderDialog(tasksOfPackageUsingSensor.get(0).taskId, user,
+                enqueueSensorUseReminderDialogAsync(tasksOfPackageUsingSensor.get(0).taskId, user,
                         packageName, sensor);
                 return;
             } else if (tasksOfPackageUsingSensor.size() > 1) {
@@ -360,21 +417,60 @@
          * @param packageName The name of the package using the sensor.
          * @param sensor The sensor that is being used.
          */
-        private void showSensorUseReminderDialog(int taskId, @NonNull UserHandle user,
+        private void enqueueSensorUseReminderDialogAsync(int taskId, @NonNull UserHandle user,
                 @NonNull String packageName, int sensor) {
+            mHandler.sendMessage(PooledLambda.obtainMessage(
+                    this:: enqueueSensorUseReminderDialog, taskId, user, packageName, sensor));
+        }
+
+        private void enqueueSensorUseReminderDialog(int taskId, @NonNull UserHandle user,
+                @NonNull String packageName, int sensor) {
+            SensorUseReminderDialogInfo info =
+                    new SensorUseReminderDialogInfo(taskId, user, packageName);
+            if (!mQueuedSensorUseReminderDialogs.containsKey(info)) {
+                ArraySet<Integer> sensors = new ArraySet<Integer>();
+                sensors.add(sensor);
+                mQueuedSensorUseReminderDialogs.put(info, sensors);
+                mHandler.sendMessageDelayed(
+                        PooledLambda.obtainMessage(this::showSensorUserReminderDialog, info),
+                        REMINDER_DIALOG_DELAY_MILLIS);
+                return;
+            }
+            ArraySet<Integer> sensors = mQueuedSensorUseReminderDialogs.get(info);
+            sensors.add(sensor);
+        }
+
+        private void showSensorUserReminderDialog(@NonNull SensorUseReminderDialogInfo info) {
+            ArraySet<Integer> sensors = mQueuedSensorUseReminderDialogs.get(info);
+            mQueuedSensorUseReminderDialogs.remove(info);
+            if (sensors == null) {
+                Log.e(TAG, "Unable to show sensor use dialog because sensor set is null."
+                        + " Was the dialog queue modified from outside the handler thread?");
+                return;
+            }
             Intent dialogIntent = new Intent();
             dialogIntent.setComponent(ComponentName.unflattenFromString(
                     mContext.getResources().getString(
                             R.string.config_sensorUseStartedActivity)));
 
             ActivityOptions options = ActivityOptions.makeBasic();
-            options.setLaunchTaskId(taskId);
+            options.setLaunchTaskId(info.mTaskId);
             options.setTaskOverlay(true, true);
 
-            dialogIntent.putExtra(EXTRA_PACKAGE_NAME, packageName);
-            dialogIntent.putExtra(EXTRA_SENSOR, sensor);
+            dialogIntent.addFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
 
-            mContext.startActivityAsUser(dialogIntent, options.toBundle(), user);
+            dialogIntent.putExtra(EXTRA_PACKAGE_NAME, info.mPackageName);
+            if (sensors.size() == 1) {
+                dialogIntent.putExtra(EXTRA_SENSOR, sensors.valueAt(0));
+            } else if (sensors.size() == 2) {
+                dialogIntent.putExtra(EXTRA_ALL_SENSORS, true);
+            } else {
+                // Currently the only cases can be 1 or two
+                Log.e(TAG, "Attempted to show sensor use dialog for " + sensors.size()
+                        + " sensors");
+                return;
+            }
+            mContext.startActivityAsUser(dialogIntent, options.toBundle(), info.mUser);
         }
 
         /**
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 2bb9084..f625843 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -167,7 +167,6 @@
     @NonNull private final VcnNetworkProvider mNetworkProvider;
     @NonNull private final TelephonySubscriptionTrackerCallback mTelephonySubscriptionTrackerCb;
     @NonNull private final TelephonySubscriptionTracker mTelephonySubscriptionTracker;
-    @NonNull private final VcnContext mVcnContext;
     @NonNull private final BroadcastReceiver mPkgChangeReceiver;
 
     @NonNull
@@ -212,7 +211,6 @@
                 mContext, mLooper, mTelephonySubscriptionTrackerCb);
 
         mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE);
-        mVcnContext = mDeps.newVcnContext(mContext, mLooper, mNetworkProvider);
 
         mPkgChangeReceiver = new BroadcastReceiver() {
             @Override
@@ -336,8 +334,9 @@
         public VcnContext newVcnContext(
                 @NonNull Context context,
                 @NonNull Looper looper,
-                @NonNull VcnNetworkProvider vcnNetworkProvider) {
-            return new VcnContext(context, looper, vcnNetworkProvider);
+                @NonNull VcnNetworkProvider vcnNetworkProvider,
+                boolean getIsInTestMode) {
+            return new VcnContext(context, looper, vcnNetworkProvider, getIsInTestMode);
         }
 
         /** Creates a new Vcn instance using the provided configuration */
@@ -419,6 +418,14 @@
                 "Carrier privilege required for subscription group to set VCN Config");
     }
 
+    private void enforceManageTestNetworksForTestMode(@NonNull VcnConfig vcnConfig) {
+        if (vcnConfig.isTestModeProfile()) {
+            mContext.enforceCallingPermission(
+                    android.Manifest.permission.MANAGE_TEST_NETWORKS,
+                    "Test-mode require the MANAGE_TEST_NETWORKS permission");
+        }
+    }
+
     private class VcnSubscriptionTrackerCallback implements TelephonySubscriptionTrackerCallback {
         /**
          * Handles subscription group changes, as notified by {@link TelephonySubscriptionTracker}
@@ -542,8 +549,11 @@
 
         final VcnCallbackImpl vcnCallback = new VcnCallbackImpl(subscriptionGroup);
 
+        final VcnContext vcnContext =
+                mDeps.newVcnContext(
+                        mContext, mLooper, mNetworkProvider, config.isTestModeProfile());
         final Vcn newInstance =
-                mDeps.newVcn(mVcnContext, subscriptionGroup, config, mLastSnapshot, vcnCallback);
+                mDeps.newVcn(vcnContext, subscriptionGroup, config, mLastSnapshot, vcnCallback);
         mVcns.put(subscriptionGroup, newInstance);
 
         // Now that a new VCN has started, notify all registered listeners to refresh their
@@ -587,6 +597,7 @@
 
         mContext.getSystemService(AppOpsManager.class)
                 .checkPackage(mDeps.getBinderCallingUid(), config.getProvisioningPackageName());
+        enforceManageTestNetworksForTestMode(config);
         enforceCallingUserAndCarrierPrivilege(subscriptionGroup, opPkgName);
 
         Binder.withCleanCallingIdentity(() -> {
diff --git a/services/core/java/com/android/server/pm/DumpState.java b/services/core/java/com/android/server/pm/DumpState.java
index ec79483..ed00609 100644
--- a/services/core/java/com/android/server/pm/DumpState.java
+++ b/services/core/java/com/android/server/pm/DumpState.java
@@ -58,6 +58,7 @@
     private boolean mTitlePrinted;
     private boolean mFullPreferred;
     private boolean mCheckIn;
+    private boolean mBrief;
 
     private String mTargetPackageName;
 
@@ -128,4 +129,12 @@
     public void setCheckIn(boolean checkIn) {
         mCheckIn = checkIn;
     }
+
+    public boolean isBrief() {
+        return mBrief;
+    }
+
+    public void setBrief(boolean brief) {
+        mBrief = brief;
+    }
 }
diff --git a/services/core/java/com/android/server/pm/KeySetManagerService.java b/services/core/java/com/android/server/pm/KeySetManagerService.java
index 2015c78..34caaf5 100644
--- a/services/core/java/com/android/server/pm/KeySetManagerService.java
+++ b/services/core/java/com/android/server/pm/KeySetManagerService.java
@@ -30,6 +30,7 @@
 import android.util.TypedXmlSerializer;
 
 import com.android.server.pm.parsing.pkg.AndroidPackage;
+import com.android.server.utils.WatchedArrayMap;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -65,7 +66,7 @@
 
     protected final LongSparseArray<ArraySet<Long>> mKeySetMapping;
 
-    private final ArrayMap<String, PackageSetting> mPackages;
+    private final WatchedArrayMap<String, PackageSetting> mPackages;
 
     private long lastIssuedKeySetId = 0;
 
@@ -114,7 +115,7 @@
         }
     }
 
-    public KeySetManagerService(ArrayMap<String, PackageSetting> packages) {
+    public KeySetManagerService(WatchedArrayMap<String, PackageSetting> packages) {
         mKeySets = new LongSparseArray<KeySetHandle>();
         mPublicKeys = new LongSparseArray<PublicKeyHandle>();
         mKeySetMapping = new LongSparseArray<ArraySet<Long>>();
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 679042f..427bb2d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -404,6 +404,7 @@
 import com.android.server.rollback.RollbackManagerInternal;
 import com.android.server.storage.DeviceStorageMonitorInternal;
 import com.android.server.uri.UriGrantsManagerInternal;
+import com.android.server.utils.SnapshotCache;
 import com.android.server.utils.TimingsTraceAndSlog;
 import com.android.server.utils.Watchable;
 import com.android.server.utils.Watched;
@@ -871,12 +872,17 @@
     @Watched
     @GuardedBy("mLock")
     final WatchedArrayMap<String, AndroidPackage> mPackages = new WatchedArrayMap<>();
+    private final SnapshotCache<WatchedArrayMap<String, AndroidPackage>> mPackagesSnapshot =
+            new SnapshotCache.Auto(mPackages, mPackages, "PackageManagerService.mPackages");
 
     // Keys are isolated uids and values are the uid of the application
     // that created the isolated process.
     @Watched
     @GuardedBy("mLock")
     final WatchedSparseIntArray mIsolatedOwners = new WatchedSparseIntArray();
+    private final SnapshotCache<WatchedSparseIntArray> mIsolatedOwnersSnapshot =
+            new SnapshotCache.Auto(mIsolatedOwners, mIsolatedOwners,
+                                   "PackageManagerService.mIsolatedOwners");
 
     /**
      * Tracks new system packages [received in an OTA] that we expect to
@@ -1309,14 +1315,17 @@
             // Avoid invalidation-thrashing by preventing cache invalidations from causing property
             // writes if the cache isn't enabled yet.  We re-enable writes later when we're
             // done initializing.
-            sSnapshotCorked = true;
+            sSnapshotCorked.incrementAndGet();
             PackageManager.corkPackageInfoCache();
         }
 
         @Override
         public void enablePackageCaches() {
             // Uncork cache invalidations and allow clients to cache package information.
-            sSnapshotCorked = false;
+            int corking = sSnapshotCorked.decrementAndGet();
+            if (TRACE_SNAPSHOTS && corking == 0) {
+                Log.i(TAG, "snapshot: corking returns to 0");
+            }
             PackageManager.uncorkPackageInfoCache();
         }
     }
@@ -1395,14 +1404,27 @@
     @Watched
     final WatchedArrayMap<String, WatchedLongSparseArray<SharedLibraryInfo>>
             mSharedLibraries = new WatchedArrayMap<>();
+    private final SnapshotCache<WatchedArrayMap<String, WatchedLongSparseArray<SharedLibraryInfo>>>
+            mSharedLibrariesSnapshot =
+            new SnapshotCache.Auto<>(mSharedLibraries, mSharedLibraries,
+                                     "PackageManagerService.mSharedLibraries");
     @Watched
     final WatchedArrayMap<String, WatchedLongSparseArray<SharedLibraryInfo>>
             mStaticLibsByDeclaringPackage = new WatchedArrayMap<>();
+    private final SnapshotCache<WatchedArrayMap<String, WatchedLongSparseArray<SharedLibraryInfo>>>
+            mStaticLibsByDeclaringPackageSnapshot =
+            new SnapshotCache.Auto<>(mSharedLibraries, mSharedLibraries,
+                                     "PackageManagerService.mSharedLibraries");
 
     // Mapping from instrumentation class names to info about them.
     @Watched
     final WatchedArrayMap<ComponentName, ParsedInstrumentation> mInstrumentation =
             new WatchedArrayMap<>();
+    private final SnapshotCache<WatchedArrayMap<ComponentName, ParsedInstrumentation>>
+            mInstrumentationSnapshot =
+            new SnapshotCache.Auto<>(mInstrumentation, mInstrumentation,
+                                     "PackageManagerService.mInstrumentation");
+
 
     // Packages whose data we have transfered into another package, thus
     // should no longer exist.
@@ -1588,6 +1610,7 @@
     static final int INTEGRITY_VERIFICATION_COMPLETE = 25;
     static final int CHECK_PENDING_INTEGRITY_VERIFICATION = 26;
     static final int DOMAIN_VERIFICATION = 27;
+    static final int SNAPSHOT_UNCORK = 28;
 
     static final int DEFERRED_NO_KILL_POST_DELETE_DELAY_MS = 3 * 1000;
     static final int DEFERRED_NO_KILL_INSTALL_OBSERVER_DELAY_MS = 500;
@@ -1834,11 +1857,11 @@
         Snapshot(int type) {
             if (type == Snapshot.SNAPPED) {
                 settings = mSettings.snapshot();
-                isolatedOwners = mIsolatedOwners.snapshot();
-                packages = mPackages.snapshot();
-                sharedLibs = mSharedLibraries.snapshot();
-                staticLibs = mStaticLibsByDeclaringPackage.snapshot();
-                instrumentation = mInstrumentation.snapshot();
+                isolatedOwners = mIsolatedOwnersSnapshot.snapshot();
+                packages = mPackagesSnapshot.snapshot();
+                sharedLibs = mSharedLibrariesSnapshot.snapshot();
+                staticLibs = mStaticLibsByDeclaringPackageSnapshot.snapshot();
+                instrumentation = mInstrumentationSnapshot.snapshot();
                 resolveComponentName = mResolveComponentName.clone();
                 resolveActivity = new ActivityInfo(mResolveActivity);
                 instantAppInstallerActivity =
@@ -4874,12 +4897,16 @@
     // A lock-free cache for frequently called functions.
     private volatile Computer mSnapshotComputer;
     // If true, the snapshot is invalid (stale).  The attribute is static since it may be
-    // set from outside classes.
-    private static volatile boolean sSnapshotInvalid = true;
+    // set from outside classes.  The attribute may be set to true anywhere, although it
+    // should only be set true while holding mLock.  However, the attribute id guaranteed
+    // to be set false only while mLock and mSnapshotLock are both held.
+    private static AtomicBoolean sSnapshotInvalid = new AtomicBoolean(true);
+    // The package manager that is using snapshots.
+    private static PackageManagerService sSnapshotConsumer = null;
     // If true, the snapshot is corked.  Do not create a new snapshot but use the live
     // computer.  This throttles snapshot creation during periods of churn in Package
     // Manager.
-    private static volatile boolean sSnapshotCorked = false;
+    private static AtomicInteger sSnapshotCorked = new AtomicInteger(0);
 
     /**
      * This lock is used to make reads from {@link #sSnapshotInvalid} and
@@ -4897,7 +4924,10 @@
 
     // The snapshot disable/enable switch.  An image with the flag set true uses snapshots
     // and an image with the flag set false does not use snapshots.
-    private static final boolean SNAPSHOT_ENABLED = false;
+    private static final boolean SNAPSHOT_ENABLED = true;
+
+    // The default auto-cork delay for snapshots.  This is 1s.
+    private static final long SNAPSHOT_AUTOCORK_DELAY_MS = TimeUnit.SECONDS.toMillis(1);
 
     // The per-instance snapshot disable/enable flag.  This is generally set to false in
     // test instances and set to SNAPSHOT_ENABLED in operational instances.
@@ -4922,15 +4952,16 @@
             // If the current thread holds mLock then it may have modified state but not
             // yet invalidated the snapshot.  Always give the thread the live computer.
             return mLiveComputer;
+        } else if (sSnapshotCorked.get() > 0) {
+            // Snapshots are corked, which means new ones should not be built right now.
+            mSnapshotStatistics.corked();
+            return mLiveComputer;
         }
         synchronized (mSnapshotLock) {
+            // This synchronization block serializes access to the snapshot computer and
+            // to the code that samples mSnapshotInvalid.
             Computer c = mSnapshotComputer;
-            if (sSnapshotCorked && (c != null)) {
-                // Snapshots are corked, which means new ones should not be built right now.
-                c.use();
-                return c;
-            }
-            if (sSnapshotInvalid || (c == null)) {
+            if (sSnapshotInvalid.getAndSet(false) || (c == null)) {
                 // The snapshot is invalid if it is marked as invalid or if it is null.  If it
                 // is null, then it is currently being rebuilt by rebuildSnapshot().
                 synchronized (mLock) {
@@ -4938,9 +4969,7 @@
                     // invalidated as it is rebuilt.  However, the snapshot is still
                     // self-consistent (the lock is being held) and is current as of the time
                     // this function is entered.
-                    if (sSnapshotInvalid) {
-                        rebuildSnapshot();
-                    }
+                    rebuildSnapshot();
 
                     // Guaranteed to be non-null.  mSnapshotComputer is only be set to null
                     // temporarily in rebuildSnapshot(), which is guarded by mLock().  Since
@@ -4958,12 +4987,11 @@
      * Rebuild the cached computer.  mSnapshotComputer is temporarily set to null to block other
      * threads from using the invalid computer until it is rebuilt.
      */
-    @GuardedBy("mLock")
+    @GuardedBy({ "mLock", "mSnapshotLock"})
     private void rebuildSnapshot() {
         final long now = SystemClock.currentTimeMicro();
         final int hits = mSnapshotComputer == null ? -1 : mSnapshotComputer.getUsed();
         mSnapshotComputer = null;
-        sSnapshotInvalid = false;
         final Snapshot args = new Snapshot(Snapshot.SNAPPED);
         mSnapshotComputer = new ComputerEngine(args);
         final long done = SystemClock.currentTimeMicro();
@@ -4972,6 +5000,30 @@
     }
 
     /**
+     * Create a new snapshot.  Used for testing only.  This does collect statistics or
+     * update the snapshot used by other actors.  It does not alter the invalidation
+     * flag.  This method takes the mLock internally.
+     */
+    private Computer createNewSnapshot() {
+        synchronized (mLock) {
+            final Snapshot args = new Snapshot(Snapshot.SNAPPED);
+            return new ComputerEngine(args);
+        }
+    }
+
+    /**
+     * Cork snapshots.  This times out after the programmed delay.
+     */
+    private void corkSnapshots(int multiplier) {
+        int corking = sSnapshotCorked.getAndIncrement();
+        if (TRACE_SNAPSHOTS && corking == 0) {
+            Log.i(TAG, "snapshot: corking goes positive");
+        }
+        Message message = mHandler.obtainMessage(SNAPSHOT_UNCORK);
+        mHandler.sendMessageDelayed(message, SNAPSHOT_AUTOCORK_DELAY_MS * multiplier);
+    }
+
+    /**
      * Create a live computer
      */
     private ComputerLocked createLiveComputer() {
@@ -4986,9 +5038,9 @@
      */
     public static void onChange(@Nullable Watchable what) {
         if (TRACE_SNAPSHOTS) {
-            Log.e(TAG, "snapshot: onChange(" + what + ")");
+            Log.i(TAG, "snapshot: onChange(" + what + ")");
         }
-        sSnapshotInvalid = true;
+        sSnapshotInvalid.set(true);
     }
 
     /**
@@ -5367,6 +5419,13 @@
                     mDomainVerificationManager.runMessage(messageCode, object);
                     break;
                 }
+                case SNAPSHOT_UNCORK: {
+                    int corking = sSnapshotCorked.decrementAndGet();
+                    if (TRACE_SNAPSHOTS && corking == 0) {
+                        Log.e(TAG, "snapshot: corking goes to zero in message handler");
+                    }
+                    break;
+                }
             }
         }
     }
@@ -6383,12 +6442,13 @@
             // constructor, at which time the invalidation method updates it.  The cache is
             // corked initially to ensure a cached computer is not built until the end of the
             // constructor.
-            mSnapshotEnabled = SNAPSHOT_ENABLED;
-            sSnapshotCorked = true;
-            sSnapshotInvalid = true;
             mSnapshotStatistics = new SnapshotStatistics();
+            sSnapshotConsumer = this;
+            sSnapshotCorked.set(1);
+            sSnapshotInvalid.set(true);
             mLiveComputer = createLiveComputer();
             mSnapshotComputer = null;
+            mSnapshotEnabled = SNAPSHOT_ENABLED;
             registerObserver();
         }
 
@@ -18521,7 +18581,7 @@
         }
     }
 
-    @GuardedBy({"mInstallLock", "mLock"})
+    @GuardedBy("mInstallLock")
     private void installPackagesTracedLI(List<InstallRequest> requests) {
         try {
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackages");
@@ -24018,6 +24078,15 @@
                 dumpState.setDump(DumpState.DUMP_PER_UID_READ_TIMEOUTS);
             } else if ("snapshot".equals(cmd)) {
                 dumpState.setDump(DumpState.DUMP_SNAPSHOT_STATISTICS);
+                if (opti < args.length) {
+                    if ("--full".equals(args[opti])) {
+                        dumpState.setBrief(false);
+                        opti++;
+                    } else if ("--brief".equals(args[opti])) {
+                        dumpState.setBrief(true);
+                        opti++;
+                    }
+                }
             } else if ("write".equals(cmd)) {
                 synchronized (mLock) {
                     writeSettingsLPrTEMP();
@@ -24353,13 +24422,14 @@
                 pw.println("  Snapshots disabled");
             } else {
                 int hits = 0;
+                int level = sSnapshotCorked.get();
                 synchronized (mSnapshotLock) {
                     if (mSnapshotComputer != null) {
                         hits = mSnapshotComputer.getUsed();
                     }
                 }
                 final long now = SystemClock.currentTimeMicro();
-                mSnapshotStatistics.dump(pw, "  ", now, hits, true);
+                mSnapshotStatistics.dump(pw, "  ", now, hits, level, dumpState.isBrief());
             }
         }
     }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 1b8eee3..f5a13d5 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -351,6 +351,7 @@
 
     private final PackageManagerTracedLock mLock;
 
+    @Watched(manual = true)
     private final RuntimePermissionPersistence mRuntimePermissionsPersistence;
 
     private final File mSettingsFilename;
@@ -364,19 +365,21 @@
     /** Map from package name to settings */
     @Watched
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    final WatchedArrayMap<String, PackageSetting> mPackages = new WatchedArrayMap<>();
+    final WatchedArrayMap<String, PackageSetting> mPackages;
+    private final SnapshotCache<WatchedArrayMap<String, PackageSetting>> mPackagesSnapshot;
 
     /**
      * List of packages that were involved in installing other packages, i.e. are listed
      * in at least one app's InstallSource.
      */
     @Watched
-    private final WatchedArraySet<String> mInstallerPackages = new WatchedArraySet<>();
+    private final WatchedArraySet<String> mInstallerPackages;
+    private final SnapshotCache<WatchedArraySet<String>> mInstallerPackagesSnapshot;
 
     /** Map from package name to appId and excluded userids */
     @Watched
-    private final WatchedArrayMap<String, KernelPackageState> mKernelMapping =
-            new WatchedArrayMap<>();
+    private final WatchedArrayMap<String, KernelPackageState> mKernelMapping;
+    private final SnapshotCache<WatchedArrayMap<String, KernelPackageState>> mKernelMappingSnapshot;
 
     // List of replaced system applications
     @Watched
@@ -397,7 +400,7 @@
 
     /** Map from volume UUID to {@link VersionInfo} */
     @Watched
-    private WatchedArrayMap<String, VersionInfo> mVersion = new WatchedArrayMap<>();
+    private final WatchedArrayMap<String, VersionInfo> mVersion = new WatchedArrayMap<>();
 
     /**
      * Version details for a storage volume that may hold apps.
@@ -435,6 +438,7 @@
     }
 
     /** Device identity for the purpose of package verification. */
+    @Watched(manual = true)
     private VerifierDeviceIdentity mVerifierDeviceIdentity;
 
     // The user's preferred activities associated with particular intent
@@ -462,10 +466,12 @@
     private final WatchedSparseArray<SettingBase> mOtherAppIds;
 
     // For reading/writing settings file.
-    private final ArrayList<Signature> mPastSignatures =
-            new ArrayList<Signature>();
-    private final ArrayMap<Long, Integer> mKeySetRefs =
-            new ArrayMap<Long, Integer>();
+    @Watched
+    private final WatchedArrayList<Signature> mPastSignatures =
+            new WatchedArrayList<Signature>();
+    @Watched
+    private final WatchedArrayMap<Long, Integer> mKeySetRefs =
+            new WatchedArrayMap<Long, Integer>();
 
     // Packages that have been renamed since they were first installed.
     // Keys are the new names of the packages, values are the original
@@ -495,18 +501,21 @@
      * TODO: make this just a local variable that is passed in during package
      * scanning to make it less confusing.
      */
-    private final ArrayList<PackageSetting> mPendingPackages = new ArrayList<>();
+    @Watched
+    private final WatchedArrayList<PackageSetting> mPendingPackages = new WatchedArrayList<>();
 
     private final File mSystemDir;
 
-    public final KeySetManagerService mKeySetManagerService =
-            new KeySetManagerService(mPackages.untrackedStorage());
+    private final KeySetManagerService mKeySetManagerService;
 
     /** Settings and other information about permissions */
+    @Watched(manual = true)
     final LegacyPermissionSettings mPermissions;
 
+    @Watched(manual = true)
     private final LegacyPermissionDataProvider mPermissionDataProvider;
 
+    @Watched(manual = true)
     private final DomainVerificationManagerInternal mDomainVerificationManager;
 
     /**
@@ -532,23 +541,7 @@
             }};
     }
 
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    public Settings(Map<String, PackageSetting> pkgSettings) {
-        mLock = new PackageManagerTracedLock();
-        mPackages.putAll(pkgSettings);
-        mAppIds = new WatchedArrayList<>();
-        mOtherAppIds = new WatchedSparseArray<>();
-        mSystemDir = null;
-        mPermissions = null;
-        mRuntimePermissionsPersistence = null;
-        mPermissionDataProvider = null;
-        mSettingsFilename = null;
-        mBackupSettingsFilename = null;
-        mPackageListFilename = null;
-        mStoppedPackagesFilename = null;
-        mBackupStoppedPackagesFilename = null;
-        mKernelMappingFilename = null;
-        mDomainVerificationManager = null;
+    private void registerObservers() {
         mPackages.registerObserver(mObserver);
         mInstallerPackages.registerObserver(mObserver);
         mKernelMapping.registerObserver(mObserver);
@@ -564,7 +557,43 @@
         mRenamedPackages.registerObserver(mObserver);
         mNextAppLinkGeneration.registerObserver(mObserver);
         mDefaultBrowserApp.registerObserver(mObserver);
+        mPendingPackages.registerObserver(mObserver);
+        mPastSignatures.registerObserver(mObserver);
+        mKeySetRefs.registerObserver(mObserver);
+    }
 
+    // CONSTRUCTOR
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public Settings(Map<String, PackageSetting> pkgSettings) {
+        mPackages = new WatchedArrayMap<>();
+        mPackagesSnapshot =
+                new SnapshotCache.Auto<>(mPackages, mPackages, "Settings.mPackages");
+        mKernelMapping = new WatchedArrayMap<>();
+        mKernelMappingSnapshot =
+                new SnapshotCache.Auto<>(mKernelMapping, mKernelMapping, "Settings.mKernelMapping");
+        mInstallerPackages = new WatchedArraySet<>();
+        mInstallerPackagesSnapshot =
+                new SnapshotCache.Auto<>(mInstallerPackages, mInstallerPackages,
+                                         "Settings.mInstallerPackages");
+        mKeySetManagerService = new KeySetManagerService(mPackages);
+
+        mLock = new PackageManagerTracedLock();
+        mPackages.putAll(pkgSettings);
+        mAppIds = new WatchedArrayList<>();
+        mOtherAppIds = new WatchedSparseArray<>();
+        mSystemDir = null;
+        mPermissions = null;
+        mRuntimePermissionsPersistence = null;
+        mPermissionDataProvider = null;
+        mSettingsFilename = null;
+        mBackupSettingsFilename = null;
+        mPackageListFilename = null;
+        mStoppedPackagesFilename = null;
+        mBackupStoppedPackagesFilename = null;
+        mKernelMappingFilename = null;
+        mDomainVerificationManager = null;
+
+        registerObservers();
         Watchable.verifyWatchedAttributes(this, mObserver);
 
         mSnapshot = makeCache();
@@ -574,6 +603,18 @@
             LegacyPermissionDataProvider permissionDataProvider,
             @NonNull DomainVerificationManagerInternal domainVerificationManager,
             @NonNull PackageManagerTracedLock lock)  {
+        mPackages = new WatchedArrayMap<>();
+        mPackagesSnapshot  =
+                new SnapshotCache.Auto<>(mPackages, mPackages, "Settings.mPackages");
+        mKernelMapping = new WatchedArrayMap<>();
+        mKernelMappingSnapshot =
+                new SnapshotCache.Auto<>(mKernelMapping, mKernelMapping, "Settings.mKernelMapping");
+        mInstallerPackages = new WatchedArraySet<>();
+        mInstallerPackagesSnapshot =
+                new SnapshotCache.Auto<>(mInstallerPackages, mInstallerPackages,
+                                         "Settings.mInstallerPackages");
+        mKeySetManagerService = new KeySetManagerService(mPackages);
+
         mLock = lock;
         mAppIds = new WatchedArrayList<>();
         mOtherAppIds = new WatchedSparseArray<>();
@@ -602,22 +643,7 @@
 
         mDomainVerificationManager = domainVerificationManager;
 
-        mPackages.registerObserver(mObserver);
-        mInstallerPackages.registerObserver(mObserver);
-        mKernelMapping.registerObserver(mObserver);
-        mDisabledSysPackages.registerObserver(mObserver);
-        mBlockUninstallPackages.registerObserver(mObserver);
-        mVersion.registerObserver(mObserver);
-        mPreferredActivities.registerObserver(mObserver);
-        mPersistentPreferredActivities.registerObserver(mObserver);
-        mCrossProfileIntentResolvers.registerObserver(mObserver);
-        mSharedUsers.registerObserver(mObserver);
-        mAppIds.registerObserver(mObserver);
-        mOtherAppIds.registerObserver(mObserver);
-        mRenamedPackages.registerObserver(mObserver);
-        mNextAppLinkGeneration.registerObserver(mObserver);
-        mDefaultBrowserApp.registerObserver(mObserver);
-
+        registerObservers();
         Watchable.verifyWatchedAttributes(this, mObserver);
 
         mSnapshot = makeCache();
@@ -629,8 +655,13 @@
      * are changed by PackageManagerService APIs are deep-copied
      */
     private Settings(Settings r) {
-        final int mPackagesSize = r.mPackages.size();
-        mPackages.putAll(r.mPackages);
+        mPackages = r.mPackagesSnapshot.snapshot();
+        mPackagesSnapshot  = new SnapshotCache.Sealed<>();
+        mKernelMapping = r.mKernelMappingSnapshot.snapshot();
+        mKernelMappingSnapshot = new SnapshotCache.Sealed<>();
+        mInstallerPackages = r.mInstallerPackagesSnapshot.snapshot();
+        mInstallerPackagesSnapshot = new SnapshotCache.Sealed<>();
+        mKeySetManagerService = new KeySetManagerService(mPackages);
 
         // The following assignments satisfy Java requirements but are not
         // needed by the read-only methods.  Note especially that the lock
@@ -647,9 +678,7 @@
 
         mDomainVerificationManager = r.mDomainVerificationManager;
 
-        mInstallerPackages.addAll(r.mInstallerPackages);
-        mKernelMapping.putAll(r.mKernelMapping);
-        mDisabledSysPackages.putAll(r.mDisabledSysPackages);
+        mDisabledSysPackages.snapshot(r.mDisabledSysPackages);
         mBlockUninstallPackages.snapshot(r.mBlockUninstallPackages);
         mVersion.putAll(r.mVersion);
         mVerifierDeviceIdentity = r.mVerifierDeviceIdentity;
@@ -659,23 +688,26 @@
                 mPersistentPreferredActivities, r.mPersistentPreferredActivities);
         WatchedSparseArray.snapshot(
                 mCrossProfileIntentResolvers, r.mCrossProfileIntentResolvers);
-        mSharedUsers.putAll(r.mSharedUsers);
+        mSharedUsers.snapshot(r.mSharedUsers);
         mAppIds = r.mAppIds.snapshot();
         mOtherAppIds = r.mOtherAppIds.snapshot();
-        mPastSignatures.addAll(r.mPastSignatures);
-        mKeySetRefs.putAll(r.mKeySetRefs);
+        WatchedArrayList.snapshot(
+                mPastSignatures, r.mPastSignatures);
+        WatchedArrayMap.snapshot(
+                mKeySetRefs, r.mKeySetRefs);
         mRenamedPackages.snapshot(r.mRenamedPackages);
         mNextAppLinkGeneration.snapshot(r.mNextAppLinkGeneration);
         mDefaultBrowserApp.snapshot(r.mDefaultBrowserApp);
         // mReadMessages
-        mPendingPackages.addAll(r.mPendingPackages);
+        WatchedArrayList.snapshot(
+                mPendingPackages, r.mPendingPackages);
         mSystemDir = null;
         // mKeySetManagerService;
         mPermissions = r.mPermissions;
         mPermissionDataProvider = r.mPermissionDataProvider;
 
         // Do not register any Watchables and do not create a snapshot cache.
-        mSnapshot = null;
+        mSnapshot = new SnapshotCache.Sealed();
     }
 
     /**
@@ -2326,7 +2358,7 @@
                 serializer.startTag(null, "shared-user");
                 serializer.attribute(null, ATTR_NAME, usr.name);
                 serializer.attributeInt(null, "userId", usr.userId);
-                usr.signatures.writeXml(serializer, "sigs", mPastSignatures);
+                usr.signatures.writeXml(serializer, "sigs", mPastSignatures.untrackedStorage());
                 serializer.endTag(null, "shared-user");
             }
 
@@ -2736,11 +2768,11 @@
 
         writeUsesStaticLibLPw(serializer, pkg.usesStaticLibraries, pkg.usesStaticLibrariesVersions);
 
-        pkg.signatures.writeXml(serializer, "sigs", mPastSignatures);
+        pkg.signatures.writeXml(serializer, "sigs", mPastSignatures.untrackedStorage());
 
         if (installSource.initiatingPackageSignatures != null) {
             installSource.initiatingPackageSignatures.writeXml(
-                    serializer, "install-initiator-sigs", mPastSignatures);
+                    serializer, "install-initiator-sigs", mPastSignatures.untrackedStorage());
         }
 
         writeSigningKeySetLPr(serializer, pkg.keySetData);
@@ -2909,7 +2941,7 @@
                 } else if (TAG_READ_EXTERNAL_STORAGE.equals(tagName)) {
                     // No longer used.
                 } else if (tagName.equals("keyset-settings")) {
-                    mKeySetManagerService.readKeySetsLPw(parser, mKeySetRefs);
+                    mKeySetManagerService.readKeySetsLPw(parser, mKeySetRefs.untrackedStorage());
                 } else if (TAG_VERSION.equals(tagName)) {
                     final String volumeUuid = XmlUtils.readStringAttribute(parser,
                             ATTR_VOLUME_UUID);
@@ -3697,7 +3729,7 @@
                 } else if (tagName.equals(TAG_ENABLED_COMPONENTS)) {
                     readEnabledComponentsLPw(packageSetting, parser, 0);
                 } else if (tagName.equals("sigs")) {
-                    packageSetting.signatures.readXml(parser, mPastSignatures);
+                    packageSetting.signatures.readXml(parser, mPastSignatures.untrackedStorage());
                 } else if (tagName.equals(TAG_PERMISSIONS)) {
                     readInstallPermissionsLPr(parser,
                             packageSetting.getLegacyPermissionState(), users);
@@ -3728,7 +3760,7 @@
                     packageSetting.keySetData.addDefinedKeySet(id, alias);
                 } else if (tagName.equals("install-initiator-sigs")) {
                     final PackageSignatures signatures = new PackageSignatures();
-                    signatures.readXml(parser, mPastSignatures);
+                    signatures.readXml(parser, mPastSignatures.untrackedStorage());
                     packageSetting.installSource =
                             packageSetting.installSource.setInitiatingPackageSignatures(signatures);
                 } else if (tagName.equals(TAG_DOMAIN_VERIFICATION)) {
@@ -3923,7 +3955,7 @@
 
                 String tagName = parser.getName();
                 if (tagName.equals("sigs")) {
-                    su.signatures.readXml(parser, mPastSignatures);
+                    su.signatures.readXml(parser, mPastSignatures.untrackedStorage());
                 } else if (tagName.equals("perms")) {
                     readInstallPermissionsLPr(parser, su.getLegacyPermissionState(), users);
                 } else {
diff --git a/services/core/java/com/android/server/pm/SnapshotStatistics.java b/services/core/java/com/android/server/pm/SnapshotStatistics.java
index c425bad5..7bf00603 100644
--- a/services/core/java/com/android/server/pm/SnapshotStatistics.java
+++ b/services/core/java/com/android/server/pm/SnapshotStatistics.java
@@ -23,6 +23,7 @@
 import android.os.SystemClock;
 import android.text.TextUtils;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.server.EventLogTags;
 
 import java.io.PrintWriter;
@@ -239,6 +240,11 @@
         public int mTotalUsed = 0;
 
         /**
+         * The total number of times a snapshot was bypassed because corking was in effect.
+         */
+        public int mTotalCorked = 0;
+
+        /**
          * The total number of builds that count as big, which means they took longer than
          * SNAPSHOT_BIG_BUILD_TIME_NS.
          */
@@ -291,6 +297,13 @@
             }
         }
 
+        /**
+         * Record a cork.
+         */
+        private void corked() {
+            mTotalCorked++;
+        }
+
         private Stats(long now) {
             mStartTimeUs = now;
             mTimes = new int[mTimeBins.count()];
@@ -308,6 +321,7 @@
             mUsed = Arrays.copyOf(orig.mUsed, orig.mUsed.length);
             mTotalBuilds = orig.mTotalBuilds;
             mTotalUsed = orig.mTotalUsed;
+            mTotalCorked = orig.mTotalCorked;
             mBigBuilds = orig.mBigBuilds;
             mShortLived = orig.mShortLived;
             mTotalTimeUs = orig.mTotalTimeUs;
@@ -365,6 +379,7 @@
          * Dump the summary statistics record.  Choose the header or the data.
          *    number of builds
          *    number of uses
+         *    number of corks
          *    number of big builds
          *    number of short lifetimes
          *    cumulative build time, in seconds
@@ -373,13 +388,13 @@
         private void dumpStats(PrintWriter pw, String indent, long now, boolean header) {
             dumpPrefix(pw, indent, now, header, "Summary stats");
             if (header) {
-                pw.format(Locale.US, "  %10s  %10s  %10s  %10s  %10s  %10s",
-                          "TotBlds", "TotUsed", "BigBlds", "ShortLvd",
+                pw.format(Locale.US, "  %10s  %10s  %10s  %10s  %10s  %10s  %10s",
+                          "TotBlds", "TotUsed", "TotCork", "BigBlds", "ShortLvd",
                           "TotTime", "MaxTime");
             } else {
                 pw.format(Locale.US,
-                        "  %10d  %10d  %10d  %10d  %10d  %10d",
-                        mTotalBuilds, mTotalUsed, mBigBuilds, mShortLived,
+                        "  %10d  %10d  %10d  %10d  %10d  %10d  %10d",
+                        mTotalBuilds, mTotalUsed, mTotalCorked, mBigBuilds, mShortLived,
                         mTotalTimeUs / 1000, mMaxBuildTimeUs / 1000);
             }
             pw.println();
@@ -516,7 +531,7 @@
      * @param done The time at which the snapshot rebuild completed, in ns.
      * @param hits The number of times the previous snapshot was used.
      */
-    public void rebuild(long now, long done, int hits) {
+    public final void rebuild(long now, long done, int hits) {
         // The duration has a span of about 2000s
         final int duration = (int) (done - now);
         boolean reportEvent = false;
@@ -544,9 +559,20 @@
     }
 
     /**
+     * Record a corked snapshot request.
+     */
+    public final void corked() {
+        synchronized (mLock) {
+            mShort[0].corked();
+            mLong[0].corked();
+        }
+    }
+
+    /**
      * Roll a stats array.  Shift the elements up an index and create a new element at
      * index zero.  The old element zero is completed with the specified time.
      */
+    @GuardedBy("mLock")
     private void shift(Stats[] s, long now) {
         s[0].complete(now);
         for (int i = s.length - 1; i > 0; i--) {
@@ -598,7 +624,8 @@
      * Dump the statistics.  The format is compatible with the PackageManager dumpsys
      * output.
      */
-    public void dump(PrintWriter pw, String indent, long now, int unrecorded, boolean full) {
+    public void dump(PrintWriter pw, String indent, long now, int unrecorded,
+                     int corkLevel, boolean full) {
         // Grab the raw statistics under lock, but print them outside of the lock.
         Stats[] l;
         Stats[] s;
@@ -608,7 +635,8 @@
             s = Arrays.copyOf(mShort, mShort.length);
             s[0] = new Stats(s[0]);
         }
-        pw.format(Locale.US, "%s Unrecorded hits %d", indent, unrecorded);
+        pw.format(Locale.US, "%s Unrecorded-hits: %d  Cork-level: %d", indent,
+                  unrecorded, corkLevel);
         pw.println();
         dump(pw, indent, now, l, s, "stats");
         if (!full) {
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
index 68e7bdb..09f8941 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
@@ -179,7 +179,7 @@
             true, /* enableQuickDoze */
             true, /* forceAllAppsStandby */
             true, /* forceBackgroundCheck */
-            PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF, /* locationMode */
+            PowerManager.LOCATION_MODE_FOREGROUND_ONLY, /* locationMode */
             PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY /* soundTriggerMode */
     );
 
diff --git a/services/core/java/com/android/server/utils/SnapshotCache.java b/services/core/java/com/android/server/utils/SnapshotCache.java
index b4b8835..42b9b23 100644
--- a/services/core/java/com/android/server/utils/SnapshotCache.java
+++ b/services/core/java/com/android/server/utils/SnapshotCache.java
@@ -19,6 +19,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
 /**
  * A class that caches snapshots.  Instances are instantiated on a {@link Watchable}; when the
  * {@link Watchable} reports a change, the cache is cleared.  The snapshot() method fetches the
@@ -35,25 +38,65 @@
      */
     private static final boolean ENABLED = true;
 
+    /**
+     * The statistics for a single cache.  The object records the number of times a
+     * snapshot was reused and the number of times a snapshot was rebuilt.
+     */
+    private static class Statistics {
+        final String mName;
+        private final AtomicInteger mReused = new AtomicInteger(0);
+        private final AtomicInteger mRebuilt = new AtomicInteger(0);
+        Statistics(@NonNull String n) {
+            mName = n;
+        }
+    }
+
     // The source object from which snapshots are created.  This may be null if createSnapshot()
     // does not require it.
     protected final T mSource;
 
     // The cached snapshot
-    private T mSnapshot = null;
+    private volatile T mSnapshot = null;
 
     // True if the snapshot is sealed and may not be modified.
-    private boolean mSealed = false;
+    private volatile boolean mSealed = false;
+
+    // The statistics for this cache.  This may be null.
+    private final Statistics mStatistics;
+
+    /**
+     * The global list of caches.
+     */
+    private static final WeakHashMap<SnapshotCache, Void> sCaches = new WeakHashMap<>();
 
     /**
      * Create a cache with a source object for rebuilding snapshots and a
-     * {@link Watchable} that notifies when the cache is invalid.
+     * {@link Watchable} that notifies when the cache is invalid.  If the name is null
+     * then statistics are not collected for this cache.
+     * @param source Source data for rebuilding snapshots.
+     * @param watchable The object that notifies when the cache is invalid.
+     * @param name The name of the cache, for statistics reporting.
+     */
+    public SnapshotCache(@Nullable T source, @NonNull Watchable watchable, @Nullable String name) {
+        mSource = source;
+        watchable.registerObserver(this);
+        if (name != null) {
+            mStatistics = new Statistics(name);
+            sCaches.put(this, null);
+        } else {
+            mStatistics = null;
+        }
+    }
+
+    /**
+     * Create a cache with a source object for rebuilding snapshots and a
+     * {@link Watchable} that notifies when the cache is invalid.  The name is null in
+     * this API.
      * @param source Source data for rebuilding snapshots.
      * @param watchable The object that notifies when the cache is invalid.
      */
     public SnapshotCache(@Nullable T source, @NonNull Watchable watchable) {
-        mSource = source;
-        watchable.registerObserver(this);
+        this(source, watchable, null);
     }
 
     /**
@@ -63,13 +106,14 @@
     public SnapshotCache() {
         mSource = null;
         mSealed = true;
+        mStatistics = null;
     }
 
     /**
      * Notify the object that the source object has changed.  If the local object is sealed then
      * IllegalStateException is thrown.  Otherwise, the cache is cleared.
      */
-    public void onChange(@Nullable Watchable what) {
+    public final void onChange(@Nullable Watchable what) {
         if (mSealed) {
             throw new IllegalStateException("attempt to change a sealed object");
         }
@@ -79,7 +123,7 @@
     /**
      * Seal the cache.  Attempts to modify the cache will generate an exception.
      */
-    public void seal() {
+    public final void seal() {
         mSealed = true;
     }
 
@@ -88,11 +132,14 @@
      * new snapshot and saves it in the cache.
      * @return A snapshot as returned by createSnapshot() and possibly cached.
      */
-    public T snapshot() {
+    public final T snapshot() {
         T s = mSnapshot;
         if (s == null || !ENABLED) {
             s = createSnapshot();
             mSnapshot = s;
+            if (mStatistics != null) mStatistics.mRebuilt.incrementAndGet();
+        } else {
+            if (mStatistics != null) mStatistics.mReused.incrementAndGet();
         }
         return s;
     }
@@ -123,4 +170,25 @@
             throw new UnsupportedOperationException("cannot snapshot a sealed snaphot");
         }
     }
+
+    /**
+     * A snapshot cache suitable for Snappable types.  The key is that Snappable types
+     * have a known implementation of createSnapshot() so that this class is concrete.
+     * @param <T> The class whose snapshot is being cached.
+     */
+    public static class Auto<T extends Snappable<T>> extends SnapshotCache<T> {
+        public Auto(@NonNull T source, @NonNull Watchable watchable, @Nullable String name) {
+            super(source, watchable, name);
+        }
+        public Auto(@NonNull T source, @NonNull Watchable watchable) {
+            this(source, watchable, null);
+        }
+        /**
+         * Concrete createSnapshot() using the snapshot() method of <T>.
+         */
+        public T createSnapshot() {
+            return mSource.snapshot();
+        }
+    }
+
 }
diff --git a/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java b/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java
index 8818023..3c6bb64 100644
--- a/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java
+++ b/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java
@@ -158,8 +158,15 @@
      * carrier owned networks may be selected, as the request specifies only subIds in the VCN's
      * subscription group, while the VCN networks are excluded by virtue of not having subIds set on
      * the VCN-exposed networks.
+     *
+     * <p>If the VCN that this UnderlyingNetworkTracker belongs to is in test-mode, this will return
+     * a NetworkRequest that only matches Test Networks.
      */
     private NetworkRequest getRouteSelectionRequest() {
+        if (mVcnContext.isInTestMode()) {
+            return getTestNetworkRequest(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup));
+        }
+
         return getBaseNetworkRequestBuilder()
                 .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
                 .build();
@@ -210,6 +217,16 @@
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
     }
 
+    /** Builds and returns a NetworkRequest for the given subIds to match Test Networks. */
+    private NetworkRequest getTestNetworkRequest(@NonNull Set<Integer> subIds) {
+        return getBaseNetworkRequestBuilder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .setSubscriptionIds(subIds)
+                .build();
+    }
+
     /**
      * Update this UnderlyingNetworkTracker's TelephonySubscriptionSnapshot.
      *
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index 7399e56..d958222 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -31,14 +31,17 @@
     @NonNull private final Context mContext;
     @NonNull private final Looper mLooper;
     @NonNull private final VcnNetworkProvider mVcnNetworkProvider;
+    private final boolean mIsInTestMode;
 
     public VcnContext(
             @NonNull Context context,
             @NonNull Looper looper,
-            @NonNull VcnNetworkProvider vcnNetworkProvider) {
+            @NonNull VcnNetworkProvider vcnNetworkProvider,
+            boolean isInTestMode) {
         mContext = Objects.requireNonNull(context, "Missing context");
         mLooper = Objects.requireNonNull(looper, "Missing looper");
         mVcnNetworkProvider = Objects.requireNonNull(vcnNetworkProvider, "Missing networkProvider");
+        mIsInTestMode = isInTestMode;
     }
 
     @NonNull
@@ -56,6 +59,10 @@
         return mVcnNetworkProvider;
     }
 
+    public boolean isInTestMode() {
+        return mIsInTestMode;
+    }
+
     /**
      * Verifies that the caller is running on the VcnContext Thread.
      *
diff --git a/services/tests/servicestests/src/com/android/server/pm/KeySetManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/KeySetManagerServiceTest.java
index 709b009..1b6bddc 100644
--- a/services/tests/servicestests/src/com/android/server/pm/KeySetManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/KeySetManagerServiceTest.java
@@ -25,6 +25,7 @@
 import android.util.LongSparseArray;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.server.utils.WatchedArrayMap;
 
 import java.io.File;
 import java.io.IOException;
@@ -33,7 +34,7 @@
 
 public class KeySetManagerServiceTest extends AndroidTestCase {
 
-    private ArrayMap<String, PackageSetting> mPackagesMap;
+    private WatchedArrayMap<String, PackageSetting> mPackagesMap;
     private KeySetManagerService mKsms;
 
     public PackageSetting generateFakePackageSetting(String name) {
@@ -46,7 +47,7 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        mPackagesMap = new ArrayMap<String, PackageSetting>();
+        mPackagesMap = new WatchedArrayMap<String, PackageSetting>();
         mKsms = new KeySetManagerService(mPackagesMap);
     }
 
@@ -94,7 +95,8 @@
     }
 
     public void testEncodePublicKey() throws IOException {
-        ArrayMap<String, PackageSetting> packagesMap = new ArrayMap<String, PackageSetting>();
+        WatchedArrayMap<String, PackageSetting> packagesMap =
+                new WatchedArrayMap<String, PackageSetting>();
         KeySetManagerService ksms = new KeySetManagerService(packagesMap);
 
         PublicKey keyA = PackageParser.parsePublicKey(KeySetStrings.ctsKeySetPublicKeyA);
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java
index a231169..29f4aa9 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -47,7 +47,6 @@
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserHandle;
-import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Log;
@@ -64,6 +63,7 @@
 import com.android.server.pm.permission.LegacyPermissionDataProvider;
 import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
 import com.android.server.utils.WatchableTester;
+import com.android.server.utils.WatchedArrayMap;
 
 import com.google.common.truth.Truth;
 
@@ -1202,9 +1202,8 @@
 
     private void verifyKeySetMetaData(Settings settings)
             throws ReflectiveOperationException, IllegalAccessException {
-        ArrayMap<String, PackageSetting> packages =
-                settings.mPackages.untrackedStorage();
-        KeySetManagerService ksms = settings.mKeySetManagerService;
+        WatchedArrayMap<String, PackageSetting> packages = settings.mPackages;
+        KeySetManagerService ksms = settings.getKeySetManagerService();
 
         /* verify keyset and public key ref counts */
         assertThat(KeySetUtils.getKeySetRefCount(ksms, 1), is(2));
diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
index 9001b3d..443476c 100644
--- a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
@@ -48,7 +48,7 @@
     private static final float PRECISION = 0.001f;
     private static final int GPS_MODE = 0; // LOCATION_MODE_NO_CHANGE
     private static final int DEFAULT_GPS_MODE =
-            PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF;
+            PowerManager.LOCATION_MODE_FOREGROUND_ONLY;
     private static final int SOUND_TRIGGER_MODE = 0; // SOUND_TRIGGER_MODE_ALL_ENABLED
     private static final int DEFAULT_SOUND_TRIGGER_MODE =
             PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY;
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 73f1783..30403f4 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -806,6 +806,15 @@
      */
     public static final String EXTRA_AUDIO_CODEC_BANDWIDTH_KHZ =
             "android.telecom.extra.AUDIO_CODEC_BANDWIDTH_KHZ";
+
+    /**
+     * Boolean connection extra key used to indicate whether device to device communication is
+     * available for the current call.
+     * @hide
+     */
+    public static final String EXTRA_IS_DEVICE_TO_DEVICE_COMMUNICATION_AVAILABLE =
+            "android.telecom.extra.IS_DEVICE_TO_DEVICE_COMMUNICATION_AVAILABLE";
+
     /**
      * Connection event used to inform Telecom that it should play the on hold tone.  This is used
      * to play a tone when the peer puts the current call on hold.  Sent to Telecom via
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
index 9410886..c59dcf8 100644
--- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -16,13 +16,17 @@
 
 package android.net.vcn;
 
+import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_MOBIKE;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.net.NetworkCapabilities;
+import android.net.ipsec.ike.IkeSessionParams;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
+import android.net.vcn.persistablebundleutils.IkeSessionParamsUtilsTest;
 import android.net.vcn.persistablebundleutils.TunnelConnectionParamsUtilsTest;
 
 import androidx.test.filters.SmallTest;
@@ -120,6 +124,21 @@
     }
 
     @Test
+    public void testBuilderRequiresMobikeEnabled() {
+        try {
+            final IkeSessionParams ikeParams =
+                    IkeSessionParamsUtilsTest.createBuilderMinimum()
+                            .removeIkeOption(IKE_OPTION_MOBIKE)
+                            .build();
+            final IkeTunnelConnectionParams tunnelParams =
+                    TunnelConnectionParamsUtilsTest.buildTestParams(ikeParams);
+            new VcnGatewayConnectionConfig.Builder(GATEWAY_CONNECTION_NAME_PREFIX, tunnelParams);
+            fail("Expected exception due to MOBIKE not enabled");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
     public void testBuilderRequiresNonEmptyExposedCaps() {
         try {
             newBuilder()
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
index 393787f..f385113 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
@@ -52,8 +52,8 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class IkeSessionParamsUtilsTest {
-    // Package private for use in EncryptedTunnelParamsUtilsTest
-    static IkeSessionParams.Builder createBuilderMinimum() {
+    // Public for use in VcnGatewayConnectionConfigTest, EncryptedTunnelParamsUtilsTest
+    public static IkeSessionParams.Builder createBuilderMinimum() {
         final InetAddress serverAddress = InetAddresses.parseNumericAddress("192.0.2.100");
 
         // TODO: b/185941731 Make sure all valid IKE_OPTIONS are added and validated.
@@ -63,6 +63,7 @@
                 .setLocalIdentification(new IkeFqdnIdentification("client.test.android.net"))
                 .setRemoteIdentification(new IkeFqdnIdentification("server.test.android.net"))
                 .addIkeOption(IkeSessionParams.IKE_OPTION_FORCE_PORT_4500)
+                .addIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE)
                 .setAuthPsk("psk".getBytes());
     }
 
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
index 0c8ad32..f9dc9eb 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import android.net.ipsec.ike.IkeSessionParams;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
 
 import androidx.test.filters.SmallTest;
@@ -31,9 +32,13 @@
 public class TunnelConnectionParamsUtilsTest {
     // Public for use in VcnGatewayConnectionConfigTest
     public static IkeTunnelConnectionParams buildTestParams() {
+        return buildTestParams(IkeSessionParamsUtilsTest.createBuilderMinimum().build());
+    }
+
+    // Public for use in VcnGatewayConnectionConfigTest
+    public static IkeTunnelConnectionParams buildTestParams(IkeSessionParams params) {
         return new IkeTunnelConnectionParams(
-                IkeSessionParamsUtilsTest.createBuilderMinimum().build(),
-                TunnelModeChildSessionParamsUtilsTest.createBuilderMinimum().build());
+                params, TunnelModeChildSessionParamsUtilsTest.createBuilderMinimum().build());
     }
 
     @Test
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 9ecd82f..3360d40 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -37,6 +37,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.CALLS_REAL_METHODS;
@@ -66,6 +67,7 @@
 import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
 import android.net.vcn.VcnConfig;
 import android.net.vcn.VcnConfigTest;
+import android.net.vcn.VcnGatewayConnectionConfigTest;
 import android.net.vcn.VcnManager;
 import android.net.vcn.VcnUnderlyingNetworkPolicy;
 import android.os.IBinder;
@@ -197,7 +199,8 @@
                 .newVcnContext(
                         eq(mMockContext),
                         eq(mTestLooper.getLooper()),
-                        any(VcnNetworkProvider.class));
+                        any(VcnNetworkProvider.class),
+                        anyBoolean());
         doReturn(mSubscriptionTracker)
                 .when(mMockDeps)
                 .newTelephonySubscriptionTracker(
@@ -371,6 +374,12 @@
         TelephonySubscriptionSnapshot snapshot =
                 triggerSubscriptionTrackerCbAndGetSnapshot(Collections.singleton(TEST_UUID_1));
         verify(mMockDeps)
+                .newVcnContext(
+                        eq(mMockContext),
+                        eq(mTestLooper.getLooper()),
+                        any(VcnNetworkProvider.class),
+                        anyBoolean());
+        verify(mMockDeps)
                 .newVcn(eq(mVcnContext), eq(TEST_UUID_1), eq(TEST_VCN_CONFIG), eq(snapshot), any());
     }
 
@@ -528,6 +537,28 @@
     }
 
     @Test
+    public void testSetVcnConfigTestModeRequiresPermission() throws Exception {
+        doThrow(new SecurityException("Requires MANAGE_TEST_NETWORKS"))
+                .when(mMockContext)
+                .enforceCallingPermission(
+                        eq(android.Manifest.permission.MANAGE_TEST_NETWORKS), any());
+
+        final VcnConfig vcnConfig =
+                new VcnConfig.Builder(mMockContext)
+                        .addGatewayConnectionConfig(
+                                VcnGatewayConnectionConfigTest.buildTestConfig())
+                        .setIsTestModeProfile()
+                        .build();
+
+        try {
+            mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, vcnConfig, TEST_PACKAGE_NAME);
+            fail("Expected exception due to using test-mode without permission");
+        } catch (SecurityException e) {
+            verify(mMockPolicyListener, never()).onPolicyChanged();
+        }
+    }
+
+    @Test
     public void testSetVcnConfigNotifiesStatusCallback() throws Exception {
         triggerSubscriptionTrackerCbAndGetSnapshot(Collections.singleton(TEST_UUID_2));
 
diff --git a/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java b/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java
index 8289e85..6f63c4b 100644
--- a/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -112,8 +113,14 @@
         MockitoAnnotations.initMocks(this);
 
         mTestLooper = new TestLooper();
-        mVcnContext = spy(new VcnContext(mContext, mTestLooper.getLooper(), mVcnNetworkProvider));
-        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
+        mVcnContext =
+                spy(
+                        new VcnContext(
+                                mContext,
+                                mTestLooper.getLooper(),
+                                mVcnNetworkProvider,
+                                false /* isInTestMode */));
+        resetVcnContext();
 
         setupSystemService(
                 mContext,
@@ -132,6 +139,11 @@
                         mNetworkTrackerCb);
     }
 
+    private void resetVcnContext() {
+        reset(mVcnContext);
+        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
+    }
+
     private static LinkProperties getLinkPropertiesWithName(String iface) {
         LinkProperties linkProperties = new LinkProperties();
         linkProperties.setInterfaceName(iface);
@@ -149,7 +161,29 @@
         verifyNetworkRequestsRegistered(INITIAL_SUB_IDS);
     }
 
+    @Test
+    public void testNetworkCallbacksRegisteredOnStartupForTestMode() {
+        resetVcnContext();
+        when(mVcnContext.isInTestMode()).thenReturn(true);
+        reset(mConnectivityManager);
+
+        mUnderlyingNetworkTracker =
+                new UnderlyingNetworkTracker(
+                        mVcnContext,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        Collections.singleton(NetworkCapabilities.NET_CAPABILITY_INTERNET),
+                        mNetworkTrackerCb);
+
+        verifyNetworkRequestsRegistered(INITIAL_SUB_IDS, true /* expectTestMode */);
+    }
+
     private void verifyNetworkRequestsRegistered(Set<Integer> expectedSubIds) {
+        verifyNetworkRequestsRegistered(expectedSubIds, false /* expectTestMode */);
+    }
+
+    private void verifyNetworkRequestsRegistered(
+            Set<Integer> expectedSubIds, boolean expectTestMode) {
         verify(mConnectivityManager)
                 .requestBackgroundNetwork(
                         eq(getWifiRequest(expectedSubIds)),
@@ -162,10 +196,16 @@
                             any(NetworkBringupCallback.class), any());
         }
 
+        final NetworkRequest expectedRouteSelectionRequest =
+                expectTestMode
+                        ? getTestNetworkRequest(expectedSubIds)
+                        : getRouteSelectionRequest(expectedSubIds);
+
         verify(mConnectivityManager)
                 .requestBackgroundNetwork(
-                        eq(getRouteSelectionRequest(expectedSubIds)),
-                        any(RouteSelectionCallback.class), any());
+                        eq(expectedRouteSelectionRequest),
+                        any(RouteSelectionCallback.class),
+                        any());
     }
 
     @Test
@@ -204,6 +244,15 @@
         return getExpectedRequestBase().setSubscriptionIds(netCapsSubIds).build();
     }
 
+    private NetworkRequest getTestNetworkRequest(Set<Integer> netCapsSubIds) {
+        return getExpectedRequestBase()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .setSubscriptionIds(netCapsSubIds)
+                .build();
+    }
+
     private NetworkRequest.Builder getExpectedRequestBase() {
         final NetworkRequest.Builder builder =
                 new NetworkRequest.Builder()