Merge "[AAPM] Disable for unsupported form factors." into main
diff --git a/apct-tests/perftests/tracing/Android.bp b/apct-tests/perftests/tracing/Android.bp
index 08e365b..8814216 100644
--- a/apct-tests/perftests/tracing/Android.bp
+++ b/apct-tests/perftests/tracing/Android.bp
@@ -22,6 +22,7 @@
         "apct-perftests-utils",
         "collector-device-lib",
         "platform-test-annotations",
+        "perfetto_trace_java_protos",
     ],
     test_suites: [
         "device-tests",
diff --git a/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java b/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java
index f4759b8..7ef8c53 100644
--- a/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java
+++ b/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java
@@ -17,10 +17,12 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.os.ServiceManager.ServiceNotFoundException;
 import android.perftests.utils.Stats;
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.IProtoLogGroup;
 import com.android.internal.protolog.common.LogLevel;
 
@@ -31,6 +33,8 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
+import perfetto.protos.ProtologCommon;
+
 import java.util.ArrayList;
 import java.util.Collection;
 
@@ -65,24 +69,48 @@
         };
     }
 
+    private IProtoLog mProcessedProtoLogger;
+    private static final String MOCK_TEST_FILE_PATH = "mock/file/path";
+    private static final perfetto.protos.Protolog.ProtoLogViewerConfig VIEWER_CONFIG =
+            perfetto.protos.Protolog.ProtoLogViewerConfig.newBuilder()
+                .addGroups(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder()
+                                .setId(1)
+                                .setName(TestProtoLogGroup.TEST_GROUP.toString())
+                                .setTag(TestProtoLogGroup.TEST_GROUP.getTag())
+                ).addMessages(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(123)
+                                .setMessage("My Test Debug Log Message %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG)
+                                .setGroupId(1)
+                                .setLocation("com/test/MyTestClass.java:123")
+                ).build();
+
     @BeforeClass
     public static void init() {
         ProtoLog.init(TestProtoLogGroup.values());
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws ServiceNotFoundException {
         TestProtoLogGroup.TEST_GROUP.setLogToProto(mLogToProto);
         TestProtoLogGroup.TEST_GROUP.setLogToLogcat(mLogToLogcat);
+
+        mProcessedProtoLogger = new ProcessedPerfettoProtoLogImpl(
+                MOCK_TEST_FILE_PATH,
+                () -> new AutoClosableProtoInputStream(VIEWER_CONFIG.toByteArray()),
+                () -> {},
+                TestProtoLogGroup.values()
+        );
     }
 
     @Test
     public void log_Processed_NoArgs() {
-        final var protoLog = ProtoLog.getSingleInstance();
         final var perfMonitor = new PerfMonitor();
 
         while (perfMonitor.keepRunning()) {
-            protoLog.log(
+            mProcessedProtoLogger.log(
                     LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123,
                     0, (Object[]) null);
         }
@@ -90,11 +118,10 @@
 
     @Test
     public void log_Processed_WithArgs() {
-        final var protoLog = ProtoLog.getSingleInstance();
         final var perfMonitor = new PerfMonitor();
 
         while (perfMonitor.keepRunning()) {
-            protoLog.log(
+            mProcessedProtoLogger.log(
                     LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123,
                     0b1110101001010100,
                     new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 047d1fa..26ffa11 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -39,3 +39,11 @@
   description: "This flag controls whether LSKF fallback is removed from biometric prompt when the phone is outside trusted locations"
   bug: "322081563"
 }
+
+flag {
+  name: "screen_off_unlock_udfps"
+  is_exported: true
+  namespace: "biometrics_integration"
+  description: "This flag controls Whether to enable fp unlock when screen turns off on udfps devices"
+  bug: "373792870"
+}
diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
index 47541ca..59a602ca 100644
--- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java
+++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
@@ -18,6 +18,7 @@
 
 import android.annotation.TestApi;
 import android.content.Context;
+import android.hardware.biometrics.Flags;
 import android.os.Build;
 import android.os.SystemProperties;
 import android.provider.Settings;
@@ -41,6 +42,7 @@
     private final Context mContext;
     private final boolean mAlwaysOnByDefault;
     private final boolean mPickupGestureEnabledByDefault;
+    private final boolean mScreenOffUdfpsEnabledByDefault;
 
     /** Copied from android.provider.Settings.Secure since these keys are hidden. */
     private static final String[] DOZE_SETTINGS = {
@@ -68,6 +70,8 @@
         mAlwaysOnByDefault = mContext.getResources().getBoolean(R.bool.config_dozeAlwaysOnEnabled);
         mPickupGestureEnabledByDefault =
                 mContext.getResources().getBoolean(R.bool.config_dozePickupGestureEnabled);
+        mScreenOffUdfpsEnabledByDefault =
+                mContext.getResources().getBoolean(R.bool.config_screen_off_udfps_enabled);
     }
 
     /** @hide */
@@ -146,7 +150,9 @@
     /** @hide */
     public boolean screenOffUdfpsEnabled(int user) {
         return !TextUtils.isEmpty(udfpsLongPressSensorType())
-            && boolSettingDefaultOff("screen_off_udfps_enabled", user);
+                && ((mScreenOffUdfpsEnabledByDefault && Flags.screenOffUnlockUdfps())
+                ? boolSettingDefaultOn("screen_off_udfps_enabled", user)
+                : boolSettingDefaultOff("screen_off_udfps_enabled", user));
     }
 
     /** @hide */
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index d8252f2..003393c 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -230,7 +230,7 @@
         mRendererWrapper = mSurfaceOnly ? null : renderer;
         mMetricsWrapper = mSurfaceOnly ? null : metrics;
         mViewRoot = mSurfaceOnly ? null : viewRootWrapper;
-        mObserver = mSurfaceOnly
+        mObserver = mSurfaceOnly || (Flags.useSfFrameDuration() && Flags.ignoreHwuiIsFirstFrame())
                 ? null
                 : new HardwareRendererObserver(this, mMetricsWrapper.getTiming(),
                         mHandler, /* waitForPresentTime= */ false);
@@ -581,7 +581,7 @@
     }
 
     private boolean callbacksReceived(JankInfo info) {
-        return mSurfaceOnly
+        return mObserver == null
                 ? info.surfaceControlCallbackFired
                 : info.hwuiCallbackFired && info.surfaceControlCallbackFired;
     }
@@ -619,7 +619,7 @@
         for (int i = 0; i < mJankInfos.size(); i++) {
             JankInfo info = mJankInfos.valueAt(i);
             final boolean isFirstDrawn = !mSurfaceOnly && info.isFirstFrame;
-            if (isFirstDrawn) {
+            if (isFirstDrawn && !Flags.ignoreHwuiIsFirstFrame()) {
                 continue;
             }
             if (info.frameVsyncId > mEndVsyncId) {
@@ -656,7 +656,7 @@
                 }
                 // TODO (b/174755489): Early latch currently gets fired way too often, so we have
                 // to ignore it for now.
-                if (!mSurfaceOnly && !info.hwuiCallbackFired) {
+                if (mObserver != null && !info.hwuiCallbackFired) {
                     markEvent("FT#MissedHWUICallback", info.frameVsyncId);
                     Log.w(TAG, "Missing HWUI jank callback for vsyncId: " + info.frameVsyncId
                             + ", CUJ=" + name);
@@ -782,7 +782,9 @@
          * @param observer observer
          */
         public void addObserver(HardwareRendererObserver observer) {
-            mRenderer.addObserver(observer);
+            if (observer != null) {
+                mRenderer.addObserver(observer);
+            }
         }
 
         /**
@@ -790,7 +792,9 @@
          * @param observer observer
          */
         public void removeObserver(HardwareRendererObserver observer) {
-            mRenderer.removeObserver(observer);
+            if (observer != null) {
+                mRenderer.removeObserver(observer);
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/jank/flags.aconfig b/core/java/com/android/internal/jank/flags.aconfig
index 82f50ae..287ad18 100644
--- a/core/java/com/android/internal/jank/flags.aconfig
+++ b/core/java/com/android/internal/jank/flags.aconfig
@@ -8,3 +8,10 @@
   bug: "354763298"
   is_fixed_read_only: true
 }
+flag {
+  name: "ignore_hwui_is_first_frame"
+  namespace: "window_surfaces"
+  description: "Whether to remove the usage of the HWUI 'is first frame' flag to ignore jank"
+  bug: "354763298"
+  is_fixed_read_only: true
+}
diff --git a/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java b/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java
new file mode 100644
index 0000000..1acb34f
--- /dev/null
+++ b/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 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.internal.protolog;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.proto.ProtoInputStream;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+public final class AutoClosableProtoInputStream implements AutoCloseable {
+    @NonNull
+    private final ProtoInputStream mProtoInputStream;
+    @Nullable
+    private final FileInputStream mFileInputStream;
+
+    public AutoClosableProtoInputStream(@NonNull FileInputStream fileInputStream) {
+        mProtoInputStream = new ProtoInputStream(fileInputStream);
+        mFileInputStream = fileInputStream;
+    }
+
+    public AutoClosableProtoInputStream(@NonNull byte[] input) {
+        mProtoInputStream = new ProtoInputStream(input);
+        mFileInputStream = null;
+    }
+
+    /**
+     * @return the ProtoInputStream this class is wrapping
+     */
+    @NonNull
+    public ProtoInputStream get() {
+        return mProtoInputStream;
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (mFileInputStream != null) {
+            mFileInputStream.close();
+        }
+    }
+}
diff --git a/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java b/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java
new file mode 100644
index 0000000..15987664
--- /dev/null
+++ b/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 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.internal.protolog;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogLevel;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Class should only be used as a temporary solution to missing viewer config file on device.
+ * In particular this class should only be initialized in Robolectric tests, if it's being used
+ * otherwise please report it.
+ *
+ * @deprecated
+ */
+@Deprecated
+public class NoViewerConfigProtoLogImpl implements IProtoLog {
+    private static final String LOG_TAG = "ProtoLog";
+
+    @Override
+    public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
+            Object[] args) {
+        Log.w(LOG_TAG, "ProtoLogging is not available due to missing viewer config file...");
+        logMessage(logLevel, group.getTag(), "PROTOLOG#" + messageHash + "("
+                + Arrays.stream(args).map(Object::toString).collect(Collectors.joining()) + ")");
+    }
+
+    @Override
+    public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args) {
+        logMessage(logLevel, group.getTag(), TextUtils.formatSimple(messageString, args));
+    }
+
+    @Override
+    public boolean isProtoEnabled() {
+        return false;
+    }
+
+    @Override
+    public int startLoggingToLogcat(String[] groups, ILogger logger) {
+        return 0;
+    }
+
+    @Override
+    public int stopLoggingToLogcat(String[] groups, ILogger logger) {
+        return 0;
+    }
+
+    @Override
+    public boolean isEnabled(IProtoLogGroup group, LogLevel level) {
+        return false;
+    }
+
+    @Override
+    public List<IProtoLogGroup> getRegisteredGroups() {
+        return List.of();
+    }
+
+    private void logMessage(LogLevel logLevel, String tag, String message) {
+        switch (logLevel) {
+            case VERBOSE -> Log.v(tag, message);
+            case INFO -> Log.i(tag, message);
+            case DEBUG -> Log.d(tag, message);
+            case WARN -> Log.w(tag, message);
+            case ERROR -> Log.e(tag, message);
+            case WTF -> Log.wtf(tag, message);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index a037cb4..a1c987f 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -60,18 +60,16 @@
 import android.util.Log;
 import android.util.LongArray;
 import android.util.Slog;
-import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs;
 import com.android.internal.protolog.common.ILogger;
 import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.IProtoLogGroup;
 import com.android.internal.protolog.common.LogDataType;
 import com.android.internal.protolog.common.LogLevel;
 
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
@@ -93,26 +91,18 @@
 /**
  * A service for the ProtoLog logging system.
  */
-public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog {
+public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog {
     private static final String LOG_TAG = "ProtoLog";
     public static final String NULL_STRING = "null";
     private final AtomicInteger mTracingInstances = new AtomicInteger();
 
     @NonNull
-    private final ProtoLogDataSource mDataSource;
-    @Nullable
-    private final ProtoLogViewerConfigReader mViewerConfigReader;
-    @Deprecated
-    @Nullable
-    private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+    protected final ProtoLogDataSource mDataSource;
     @NonNull
-    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
+    protected final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
     @NonNull
     private final Runnable mCacheUpdater;
 
-    @Nullable // null when the flag android.tracing.client_side_proto_logging is not flipped
-    private final IProtoLogConfigurationService mProtoLogConfigurationService;
-
     @NonNull
     private final int[] mDefaultLogLevelCounts = new int[LogLevel.values().length];
     @NonNull
@@ -121,68 +111,15 @@
     private final Map<String, Integer> mCollectStackTraceGroupCounts = new ArrayMap<>();
 
     private final Lock mBackgroundServiceLock = new ReentrantLock();
-    private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor();
+    protected ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor();
 
-    public PerfettoProtoLogImpl(@NonNull IProtoLogGroup[] groups)
-            throws ServiceManager.ServiceNotFoundException {
-        this(null, null, null, () -> {}, groups);
-    }
+    // Set to true once this is ready to accept protolog to logcat requests.
+    private boolean mLogcatReady = false;
 
-    public PerfettoProtoLogImpl(@NonNull Runnable cacheUpdater, @NonNull IProtoLogGroup[] groups)
-            throws ServiceManager.ServiceNotFoundException {
-        this(null, null, null, cacheUpdater, groups);
-    }
-
-    public PerfettoProtoLogImpl(
-            @NonNull String viewerConfigFilePath,
+    protected PerfettoProtoLogImpl(
             @NonNull Runnable cacheUpdater,
             @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
-        this(viewerConfigFilePath,
-                null,
-                new ProtoLogViewerConfigReader(() -> {
-                    try {
-                        return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
-                    } catch (FileNotFoundException e) {
-                        throw new RuntimeException(
-                                "Failed to load viewer config file " + viewerConfigFilePath, e);
-                    }
-                }),
-                cacheUpdater, groups);
-    }
-
-    @Deprecated
-    @VisibleForTesting
-    public PerfettoProtoLogImpl(
-            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
-            @NonNull Runnable cacheUpdater,
-            @NonNull IProtoLogGroup[] groups,
-            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
-            @NonNull ProtoLogConfigurationService configurationService) {
-        this(null, viewerConfigInputStreamProvider, viewerConfigReader, cacheUpdater,
-                groups, dataSourceBuilder, configurationService);
-    }
-
-    @VisibleForTesting
-    public PerfettoProtoLogImpl(
-            @Nullable String viewerConfigFilePath,
-            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
-            @NonNull Runnable cacheUpdater,
-            @NonNull IProtoLogGroup[] groups,
-            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
-            @NonNull ProtoLogConfigurationService configurationService) {
-        this(viewerConfigFilePath, null, viewerConfigReader, cacheUpdater,
-                groups, dataSourceBuilder, configurationService);
-    }
-
-    private PerfettoProtoLogImpl(
-            @Nullable String viewerConfigFilePath,
-            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
-            @NonNull Runnable cacheUpdater,
-            @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
-        this(viewerConfigFilePath, viewerConfigInputStreamProvider, viewerConfigReader,
-                cacheUpdater, groups,
+        this(cacheUpdater, groups,
                 ProtoLogDataSource::new,
                 android.tracing.Flags.clientSideProtoLogging() ?
                     IProtoLogConfigurationService.Stub.asInterface(
@@ -191,19 +128,11 @@
         );
     }
 
-    private PerfettoProtoLogImpl(
-            @Nullable String viewerConfigFilePath,
-            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
+    protected PerfettoProtoLogImpl(
             @NonNull Runnable cacheUpdater,
             @NonNull IProtoLogGroup[] groups,
             @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
             @Nullable IProtoLogConfigurationService configurationService) {
-        if (viewerConfigFilePath != null && viewerConfigInputStreamProvider != null) {
-            throw new RuntimeException("Only one of viewerConfigFilePath and "
-                    + "viewerConfigInputStreamProvider can be set");
-        }
-
         mDataSource = dataSourceBuilder.build(
                 this::onTracingInstanceStart,
                 this::onTracingFlush,
@@ -219,55 +148,33 @@
         // for some messages logged right after the construction of this class.
         mDataSource.register(params);
 
-        if (viewerConfigInputStreamProvider == null && viewerConfigFilePath != null) {
-            viewerConfigInputStreamProvider = new ViewerConfigInputStreamProvider() {
-                @NonNull
-                @Override
-                public ProtoInputStream getInputStream() {
-                    try {
-                        return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
-                    } catch (FileNotFoundException e) {
-                        throw new RuntimeException(
-                                "Failed to load viewer config file " + viewerConfigFilePath, e);
-                    }
-                }
-            };
-        }
-
-        this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
-        this.mViewerConfigReader = viewerConfigReader;
         this.mCacheUpdater = cacheUpdater;
 
         registerGroupsLocally(groups);
 
         if (android.tracing.Flags.clientSideProtoLogging()) {
-            mProtoLogConfigurationService = configurationService;
-            Objects.requireNonNull(mProtoLogConfigurationService,
+            Objects.requireNonNull(configurationService,
                     "A null ProtoLog Configuration Service was provided!");
 
             try {
-                var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs();
-
-                if (viewerConfigFilePath != null) {
-                    args.setViewerConfigFile(viewerConfigFilePath);
-                }
+                var args = createConfigurationServiceRegisterClientArgs();
 
                 final var groupArgs = Stream.of(groups)
-                        .map(group -> new ProtoLogConfigurationServiceImpl.RegisterClientArgs
+                        .map(group -> new RegisterClientArgs
                                 .GroupConfig(group.name(), group.isLogToLogcat()))
-                        .toArray(ProtoLogConfigurationServiceImpl
-                                .RegisterClientArgs.GroupConfig[]::new);
+                        .toArray(RegisterClientArgs.GroupConfig[]::new);
                 args.setGroups(groupArgs);
 
-                mProtoLogConfigurationService.registerClient(this, args);
+                configurationService.registerClient(this, args);
             } catch (RemoteException e) {
                 throw new RuntimeException("Failed to register ProtoLog client");
             }
-        } else {
-            mProtoLogConfigurationService = null;
         }
     }
 
+    @NonNull
+    protected abstract RegisterClientArgs createConfigurationServiceRegisterClientArgs();
+
     /**
      * Main log method, do not call directly.
      */
@@ -334,9 +241,6 @@
      * @return status code
      */
     public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
-        if (mViewerConfigReader != null) {
-            mViewerConfigReader.loadViewerConfig(groups, logger);
-        }
         return setTextLogging(true, logger, groups);
     }
 
@@ -347,9 +251,6 @@
      * @return status code
      */
     public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
-        if (mViewerConfigReader != null) {
-            mViewerConfigReader.unloadViewerConfig(groups, logger);
-        }
         return setTextLogging(false, logger, groups);
     }
 
@@ -372,21 +273,8 @@
         // we might want to manually specify an id for the group with a collision
         verifyNoCollisionsOrDuplicates(protoLogGroups);
 
-        final var groupsLoggingToLogcat = new ArrayList<String>();
         for (IProtoLogGroup protoLogGroup : protoLogGroups) {
             mLogGroups.put(protoLogGroup.name(), protoLogGroup);
-
-            if (protoLogGroup.isLogToLogcat()) {
-                groupsLoggingToLogcat.add(protoLogGroup.name());
-            }
-        }
-
-        if (mViewerConfigReader != null) {
-            // Load in background to avoid delay in boot process.
-            // The caveat is that any log message that is also logged to logcat will not be
-            // successfully decoded until this completes.
-            mBackgroundLoggingService.execute(() -> mViewerConfigReader
-                    .loadViewerConfig(groupsLoggingToLogcat.toArray(new String[0])));
         }
     }
 
@@ -403,6 +291,10 @@
         }
     }
 
+    protected void readyToLogToLogcat() {
+        mLogcatReady = true;
+    }
+
     /**
      * Responds to a shell command.
      */
@@ -499,57 +391,21 @@
     }
 
     @Deprecated
-    private void dumpViewerConfig() {
-        if (mViewerConfigInputStreamProvider == null) {
-            // No viewer config available
+    abstract void dumpViewerConfig();
+
+    @NonNull
+    abstract String getLogcatMessageString(@NonNull Message message);
+
+    private void logToLogcat(@NonNull String tag, @NonNull LogLevel level, @NonNull Message message,
+            @Nullable Object[] args) {
+        if (!mLogcatReady) {
+            Log.w(LOG_TAG, "Trying to log a protolog message with hash "
+                    + message.getMessageHash() + " to logcat before the service is ready to accept "
+                    + "such requests.");
             return;
         }
 
-        Log.d(LOG_TAG, "Dumping viewer config to trace");
-
-        Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider);
-
-        Log.d(LOG_TAG, "Dumped viewer config to trace");
-    }
-
-    private void logToLogcat(String tag, LogLevel level, Message message,
-            @Nullable Object[] args) {
-        String messageString;
-        if (mViewerConfigReader == null) {
-            messageString = message.getMessage();
-
-            if (messageString == null) {
-                Log.e(LOG_TAG, "Failed to decode message for logcat. "
-                        + "Message not available without ViewerConfig to decode the hash.");
-            }
-        } else {
-            messageString = message.getMessage(mViewerConfigReader);
-
-            if (messageString == null) {
-                Log.e(LOG_TAG, "Failed to decode message for logcat. "
-                        + "Message hash either not available in viewerConfig file or "
-                        + "not loaded into memory from file before decoding.");
-            }
-        }
-
-        if (messageString == null) {
-            StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE");
-            if (args != null) {
-                builder.append(" args = (");
-                builder.append(String.join(", ", Arrays.stream(args)
-                        .map(it -> {
-                            if (it == null) {
-                                return "null";
-                            } else {
-                                return it.toString();
-                            }
-                        }).toList()));
-                builder.append(")");
-            }
-            messageString = builder.toString();
-            args = new Object[0];
-        }
-
+        String messageString = getLogcatMessageString(message);
         logToLogcat(tag, level, messageString, args);
     }
 
diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
new file mode 100644
index 0000000..febe1f3
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 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.internal.protolog;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs;
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+
+public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl {
+    private static final String LOG_TAG = "PerfettoProtoLogImpl";
+
+    @NonNull
+    private final ProtoLogViewerConfigReader mViewerConfigReader;
+    @Deprecated
+    @NonNull
+    private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+    @NonNull
+    private final String mViewerConfigFilePath;
+
+    public ProcessedPerfettoProtoLogImpl(
+            @NonNull String viewerConfigFilePath,
+            @NonNull Runnable cacheUpdater,
+            @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
+        this(viewerConfigFilePath, new ViewerConfigInputStreamProvider() {
+                    @NonNull
+                    @Override
+                    public AutoClosableProtoInputStream getInputStream() {
+                        try {
+                            final var protoFileInputStream =
+                                    new FileInputStream(viewerConfigFilePath);
+                            return new AutoClosableProtoInputStream(protoFileInputStream);
+                        } catch (FileNotFoundException e) {
+                            throw new RuntimeException(
+                                    "Failed to load viewer config file " + viewerConfigFilePath, e);
+                        }
+                    }
+                },
+                cacheUpdater, groups);
+    }
+
+    @VisibleForTesting
+    public ProcessedPerfettoProtoLogImpl(
+            @NonNull String viewerConfigFilePath,
+            @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            @NonNull Runnable cacheUpdater,
+            @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
+        super(cacheUpdater, groups);
+
+        this.mViewerConfigFilePath = viewerConfigFilePath;
+
+        this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
+        this.mViewerConfigReader = new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider);
+
+        loadLogcatGroupsViewerConfig(groups);
+    }
+
+    @VisibleForTesting
+    public ProcessedPerfettoProtoLogImpl(
+            @NonNull String viewerConfigFilePath,
+            @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            @NonNull ProtoLogViewerConfigReader viewerConfigReader,
+            @NonNull Runnable cacheUpdater,
+            @NonNull IProtoLogGroup[] groups,
+            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
+            @Nullable IProtoLogConfigurationService configurationService)
+            throws ServiceManager.ServiceNotFoundException {
+        super(cacheUpdater, groups, dataSourceBuilder, configurationService);
+
+        this.mViewerConfigFilePath = viewerConfigFilePath;
+
+        this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
+        this.mViewerConfigReader = viewerConfigReader;
+
+        loadLogcatGroupsViewerConfig(groups);
+    }
+
+    @NonNull
+    @Override
+    protected RegisterClientArgs createConfigurationServiceRegisterClientArgs() {
+        return new RegisterClientArgs()
+                .setViewerConfigFile(mViewerConfigFilePath);
+    }
+
+    /**
+     * Start text logging
+     * @param groups Groups to start text logging for
+     * @param logger A logger to write status updates to
+     * @return status code
+     */
+    @Override
+    public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
+        mViewerConfigReader.loadViewerConfig(groups, logger);
+        return super.startLoggingToLogcat(groups, logger);
+    }
+
+    /**
+     * Stop text logging
+     * @param groups Groups to start text logging for
+     * @param logger A logger to write status updates to
+     * @return status code
+     */
+    @Override
+    public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
+        mViewerConfigReader.unloadViewerConfig(groups, logger);
+        return super.stopLoggingToLogcat(groups, logger);
+    }
+
+    @Deprecated
+    @Override
+    void dumpViewerConfig() {
+        Log.d(LOG_TAG, "Dumping viewer config to trace");
+        Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider);
+        Log.d(LOG_TAG, "Dumped viewer config to trace");
+    }
+
+    @NonNull
+    @Override
+    String getLogcatMessageString(@NonNull Message message) {
+        String messageString;
+        messageString = message.getMessage(mViewerConfigReader);
+
+        if (messageString == null) {
+            throw new RuntimeException("Failed to decode message for logcat. "
+                    + "Message hash (" + message.getMessageHash() + ") either not available in "
+                    + "viewerConfig file (" +  mViewerConfigFilePath + ") or "
+                    + "not loaded into memory from file before decoding.");
+        }
+
+        return messageString;
+    }
+
+    private void loadLogcatGroupsViewerConfig(@NonNull IProtoLogGroup[] protoLogGroups) {
+        final var groupsLoggingToLogcat = new ArrayList<String>();
+        for (IProtoLogGroup protoLogGroup : protoLogGroups) {
+            if (protoLogGroup.isLogToLogcat()) {
+                groupsLoggingToLogcat.add(protoLogGroup.name());
+            }
+        }
+
+        // Load in background to avoid delay in boot process.
+        // The caveat is that any log message that is also logged to logcat will not be
+        // successfully decoded until this completes.
+        mBackgroundLoggingService.execute(() -> {
+            mViewerConfigReader.loadViewerConfig(groupsLoggingToLogcat.toArray(new String[0]));
+            readyToLogToLogcat();
+        });
+    }
+}
diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java
index 60213b1..d117e93 100644
--- a/core/java/com/android/internal/protolog/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/ProtoLog.java
@@ -70,16 +70,16 @@
         // directly to the generated tracing implementations.
         if (android.tracing.Flags.perfettoProtologTracing()) {
             synchronized (sInitLock) {
+                final var allGroups = new HashSet<>(Arrays.stream(groups).toList());
                 if (sProtoLogInstance != null) {
                     // The ProtoLog instance has already been initialized in this process
                     final var alreadyRegisteredGroups = sProtoLogInstance.getRegisteredGroups();
-                    final var allGroups = new HashSet<>(alreadyRegisteredGroups);
-                    allGroups.addAll(Arrays.stream(groups).toList());
-                    groups = allGroups.toArray(new IProtoLogGroup[0]);
+                    allGroups.addAll(alreadyRegisteredGroups);
                 }
 
                 try {
-                    sProtoLogInstance = new PerfettoProtoLogImpl(groups);
+                    sProtoLogInstance = new UnprocessedPerfettoProtoLogImpl(
+                            allGroups.toArray(new IProtoLogGroup[0]));
                 } catch (ServiceManager.ServiceNotFoundException e) {
                     throw new RuntimeException(e);
                 }
diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
index 8d37899..e9a8770 100644
--- a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
@@ -379,7 +379,7 @@
             @NonNull String viewerConfigFilePath) {
         Utils.dumpViewerConfig(dataSource, () -> {
             try {
-                return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
+                return new AutoClosableProtoInputStream(new FileInputStream(viewerConfigFilePath));
             } catch (FileNotFoundException e) {
                 throw new RuntimeException(
                         "Failed to load viewer config file " + viewerConfigFilePath, e);
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 5d67534..3378d08 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -105,31 +105,10 @@
                     + "viewerConfigPath = " + sViewerConfigPath);
 
             final var groups = sLogGroups.values().toArray(new IProtoLogGroup[0]);
-
             if (android.tracing.Flags.perfettoProtologTracing()) {
-                try {
-                    File f = new File(sViewerConfigPath);
-                    if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) {
-                        // TODO(b/353530422): Remove - temporary fix to unblock b/352290057
-                        // In some tests the viewer config file might not exist in which we don't
-                        // want to provide config path to the user
-                        Log.w(LOG_TAG, "Failed to find viewerConfigFile when setting up "
-                                + ProtoLogImpl.class.getSimpleName() + ". "
-                                + "Setting up without a viewer config instead...");
-
-                        sServiceInstance = new PerfettoProtoLogImpl(sCacheUpdater, groups);
-                    } else {
-                        sServiceInstance =
-                                new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater, groups);
-                    }
-                } catch (ServiceManager.ServiceNotFoundException e) {
-                    throw new RuntimeException(e);
-                }
+                sServiceInstance = createProtoLogImpl(groups);
             } else {
-                var protologImpl = new LegacyProtoLogImpl(
-                        sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater);
-                protologImpl.registerGroups(groups);
-                sServiceInstance = protologImpl;
+                sServiceInstance = createLegacyProtoLogImpl(groups);
             }
 
             sCacheUpdater.run();
@@ -137,6 +116,34 @@
         return sServiceInstance;
     }
 
+    private static IProtoLog createProtoLogImpl(IProtoLogGroup[] groups) {
+        try {
+            File f = new File(sViewerConfigPath);
+            if (!f.exists()) {
+                // TODO(b/353530422): Remove - temporary fix to unblock b/352290057
+                // In robolectric tests the viewer config file isn't current available, so we cannot
+                // use the ProcessedPerfettoProtoLogImpl.
+                Log.e(LOG_TAG, "Failed to find viewer config file " + sViewerConfigPath
+                        + " when setting up " + ProtoLogImpl.class.getSimpleName() + ". "
+                        + "ProtoLog will not work here!");
+
+                return new NoViewerConfigProtoLogImpl();
+            } else {
+                return new ProcessedPerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater, groups);
+            }
+        } catch (ServiceManager.ServiceNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static LegacyProtoLogImpl createLegacyProtoLogImpl(IProtoLogGroup[] groups) {
+        var protologImpl = new LegacyProtoLogImpl(
+                sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater);
+        protologImpl.registerGroups(groups);
+
+        return protologImpl;
+    }
+
     @VisibleForTesting
     public static synchronized void setSingleInstance(@Nullable IProtoLog instance) {
         sServiceInstance = instance;
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index 571fe0b..524f642 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -106,46 +106,47 @@
         long targetGroupId = loadGroupId(group);
 
         final Map<Long, String> hashesForGroup = new TreeMap<>();
-        final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+        try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) {
+            final var pis = pisWrapper.get();
+            while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (pis.getFieldNumber() == (int) MESSAGES) {
+                    final long inMessageToken = pis.start(MESSAGES);
 
-        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-            if (pis.getFieldNumber() == (int) MESSAGES) {
-                final long inMessageToken = pis.start(MESSAGES);
-
-                long messageId = 0;
-                String message = null;
-                int groupId = 0;
-                while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-                    switch (pis.getFieldNumber()) {
-                        case (int) MESSAGE_ID:
-                            messageId = pis.readLong(MESSAGE_ID);
-                            break;
-                        case (int) MESSAGE:
-                            message = pis.readString(MESSAGE);
-                            break;
-                        case (int) GROUP_ID:
-                            groupId = pis.readInt(GROUP_ID);
-                            break;
+                    long messageId = 0;
+                    String message = null;
+                    int groupId = 0;
+                    while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                        switch (pis.getFieldNumber()) {
+                            case (int) MESSAGE_ID:
+                                messageId = pis.readLong(MESSAGE_ID);
+                                break;
+                            case (int) MESSAGE:
+                                message = pis.readString(MESSAGE);
+                                break;
+                            case (int) GROUP_ID:
+                                groupId = pis.readInt(GROUP_ID);
+                                break;
+                        }
                     }
-                }
 
-                if (groupId == 0) {
-                    throw new IOException("Failed to get group id");
-                }
+                    if (groupId == 0) {
+                        throw new IOException("Failed to get group id");
+                    }
 
-                if (messageId == 0) {
-                    throw new IOException("Failed to get message id");
-                }
+                    if (messageId == 0) {
+                        throw new IOException("Failed to get message id");
+                    }
 
-                if (message == null) {
-                    throw new IOException("Failed to get message string");
-                }
+                    if (message == null) {
+                        throw new IOException("Failed to get message string");
+                    }
 
-                if (groupId == targetGroupId) {
-                    hashesForGroup.put(messageId, message);
-                }
+                    if (groupId == targetGroupId) {
+                        hashesForGroup.put(messageId, message);
+                    }
 
-                pis.end(inMessageToken);
+                    pis.end(inMessageToken);
+                }
             }
         }
 
@@ -153,30 +154,32 @@
     }
 
     private long loadGroupId(@NonNull String group) throws IOException {
-        final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+        try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) {
+            final var pis = pisWrapper.get();
 
-        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-            if (pis.getFieldNumber() == (int) GROUPS) {
-                final long inMessageToken = pis.start(GROUPS);
+            while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (pis.getFieldNumber() == (int) GROUPS) {
+                    final long inMessageToken = pis.start(GROUPS);
 
-                long groupId = 0;
-                String groupName = null;
-                while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-                    switch (pis.getFieldNumber()) {
-                        case (int) ID:
-                            groupId = pis.readInt(ID);
-                            break;
-                        case (int) NAME:
-                            groupName = pis.readString(NAME);
-                            break;
+                    long groupId = 0;
+                    String groupName = null;
+                    while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                        switch (pis.getFieldNumber()) {
+                            case (int) ID:
+                                groupId = pis.readInt(ID);
+                                break;
+                            case (int) NAME:
+                                groupName = pis.readString(NAME);
+                                break;
+                        }
                     }
-                }
 
-                if (Objects.equals(groupName, group)) {
-                    return groupId;
-                }
+                    if (Objects.equals(groupName, group)) {
+                        return groupId;
+                    }
 
-                pis.end(inMessageToken);
+                    pis.end(inMessageToken);
+                }
             }
         }
 
diff --git a/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java
new file mode 100644
index 0000000..f3fe580
--- /dev/null
+++ b/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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.internal.protolog;
+
+import android.annotation.NonNull;
+import android.os.ServiceManager;
+
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+public class UnprocessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl {
+    public UnprocessedPerfettoProtoLogImpl(@NonNull IProtoLogGroup[] groups)
+            throws ServiceManager.ServiceNotFoundException {
+        this(() -> {}, groups);
+    }
+
+    public UnprocessedPerfettoProtoLogImpl(@NonNull Runnable cacheUpdater,
+            @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
+        super(cacheUpdater, groups);
+        readyToLogToLogcat();
+    }
+
+    @NonNull
+    @Override
+    protected RegisterClientArgs createConfigurationServiceRegisterClientArgs() {
+        return new RegisterClientArgs();
+    }
+
+    @Override
+    void dumpViewerConfig() {
+        // No-op
+    }
+
+    @NonNull
+    @Override
+    String getLogcatMessageString(@NonNull Message message) {
+        String messageString;
+        messageString = message.getMessage();
+
+        if (messageString == null) {
+            throw new RuntimeException("Failed to decode message for logcat. "
+                    + "Message not available without ViewerConfig to decode the hash.");
+        }
+
+        return messageString;
+    }
+}
diff --git a/core/java/com/android/internal/protolog/Utils.java b/core/java/com/android/internal/protolog/Utils.java
index 00ef80a..629682c 100644
--- a/core/java/com/android/internal/protolog/Utils.java
+++ b/core/java/com/android/internal/protolog/Utils.java
@@ -48,8 +48,8 @@
     public static void dumpViewerConfig(@NonNull ProtoLogDataSource dataSource,
             @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
         dataSource.trace(ctx -> {
-            try {
-                ProtoInputStream pis = viewerConfigInputStreamProvider.getInputStream();
+            try (var pisWrapper = viewerConfigInputStreamProvider.getInputStream()) {
+                final var pis = pisWrapper.get();
 
                 final ProtoOutputStream os = ctx.newTracePacket();
 
diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
index 14bc8e4..60c9892 100644
--- a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
+++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
@@ -17,12 +17,12 @@
 package com.android.internal.protolog;
 
 import android.annotation.NonNull;
-import android.util.proto.ProtoInputStream;
 
 public interface ViewerConfigInputStreamProvider {
     /**
      * @return a ProtoInputStream.
      */
     @NonNull
-    ProtoInputStream getInputStream();
+    AutoClosableProtoInputStream getInputStream();
 }
+
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 42ac90dd..9c92e5c 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7178,4 +7178,7 @@
 
     <!-- The name of the service for forensic backup transport. -->
     <string name="config_forensicBackupTransport" translatable="false"></string>
+
+    <!-- Whether to enable fp unlock when screen turns off on udfps devices -->
+    <bool name="config_screen_off_udfps_enabled">false</bool>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index dfee85a..712b994 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5639,4 +5639,7 @@
 
   <!-- Forensic backup transport -->
   <java-symbol type="string" name="config_forensicBackupTransport" />
+
+  <!-- Fingerprint screen off unlock config -->
+  <java-symbol type="bool" name="config_screen_off_udfps_enabled" />
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 427df17..4c25889 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -267,7 +267,8 @@
             AppHandleEducationController appHandleEducationController,
             WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
-            FocusTransitionObserver focusTransitionObserver) {
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return new DesktopModeWindowDecorViewModel(
                     context,
@@ -295,7 +296,8 @@
                     appHandleEducationController,
                     windowDecorCaptionHandleRepository,
                     desktopActivityOrientationHandler,
-                    focusTransitionObserver);
+                    focusTransitionObserver,
+                    desktopModeEventLogger);
         }
         return new CaptionWindowDecorViewModel(
                 context,
@@ -647,7 +649,8 @@
             Optional<RecentTasksController> recentTasksController,
             InteractionJankMonitor interactionJankMonitor,
             InputManager inputManager,
-            FocusTransitionObserver focusTransitionObserver) {
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger) {
         return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
                 displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
                 dragAndDropController, transitions, keyguardManager,
@@ -659,7 +662,8 @@
                 desktopModeLoggerTransitionObserver, launchAdjacentController,
                 recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
                 recentTasksController.orElse(null), interactionJankMonitor, mainHandler,
-                inputManager, focusTransitionObserver);
+                inputManager, focusTransitionObserver,
+                desktopModeEventLogger);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 8ebe503..255ca6e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -16,11 +16,20 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.app.ActivityManager.RunningTaskInfo
+import android.util.Size
+import android.view.InputDevice.SOURCE_MOUSE
+import android.view.InputDevice.SOURCE_TOUCHSCREEN
+import android.view.MotionEvent
+import android.view.MotionEvent.TOOL_TYPE_FINGER
+import android.view.MotionEvent.TOOL_TYPE_MOUSE
+import android.view.MotionEvent.TOOL_TYPE_STYLUS
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.protolog.ProtoLog
 import com.android.internal.util.FrameworkStatsLog
 import com.android.window.flags.Flags
 import com.android.wm.shell.EventLogTags
+import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import java.security.SecureRandom
 import java.util.Random
@@ -176,7 +185,13 @@
      * Logs that a task resize event is starting with [taskSizeUpdate] within a Desktop mode
      * session.
      */
-    fun logTaskResizingStarted(taskSizeUpdate: TaskSizeUpdate) {
+    fun logTaskResizingStarted(
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
+        taskInfo: RunningTaskInfo,
+        displayController: DisplayController? = null,
+        displayLayoutSize: Size? = null,
+    ) {
         if (!Flags.enableResizingMetrics()) return
 
         val sessionId = currentSessionId.get()
@@ -188,11 +203,19 @@
             return
         }
 
+        val taskSizeUpdate = createTaskSizeUpdate(
+            resizeTrigger,
+            motionEvent,
+            taskInfo,
+            displayController = displayController,
+            displayLayoutSize = displayLayoutSize,
+        )
+
         ProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopModeLogger: Logging task resize is starting, session: %s taskId: %s",
+            "DesktopModeLogger: Logging task resize is starting, session: %s, taskSizeUpdate: %s",
             sessionId,
-            taskSizeUpdate.instanceId
+            taskSizeUpdate
         )
         logTaskSizeUpdated(
             FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE,
@@ -203,7 +226,15 @@
     /**
      * Logs that a task resize event is ending with [taskSizeUpdate] within a Desktop mode session.
      */
-    fun logTaskResizingEnded(taskSizeUpdate: TaskSizeUpdate) {
+    fun logTaskResizingEnded(
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
+        taskInfo: RunningTaskInfo,
+        taskHeight: Int? = null,
+        taskWidth: Int? = null,
+        displayController: DisplayController? = null,
+        displayLayoutSize: Size? = null,
+    ) {
         if (!Flags.enableResizingMetrics()) return
 
         val sessionId = currentSessionId.get()
@@ -215,18 +246,61 @@
             return
         }
 
+        val taskSizeUpdate = createTaskSizeUpdate(
+            resizeTrigger,
+            motionEvent,
+            taskInfo,
+            taskHeight,
+            taskWidth,
+            displayController,
+            displayLayoutSize,
+        )
+
         ProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopModeLogger: Logging task resize is ending, session: %s taskId: %s",
+            "DesktopModeLogger: Logging task resize is ending, session: %s, taskSizeUpdate: %s",
             sessionId,
-            taskSizeUpdate.instanceId
+            taskSizeUpdate
         )
+
         logTaskSizeUpdated(
             FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE,
             sessionId, taskSizeUpdate
         )
     }
 
+    private fun createTaskSizeUpdate(
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
+        taskInfo: RunningTaskInfo,
+        taskHeight: Int? = null,
+        taskWidth: Int? = null,
+        displayController: DisplayController? = null,
+        displayLayoutSize: Size? = null,
+    ): TaskSizeUpdate {
+        val taskBounds = taskInfo.configuration.windowConfiguration.bounds
+
+        val height = taskHeight ?: taskBounds.height()
+        val width = taskWidth ?: taskBounds.width()
+
+        val displaySize = when {
+            displayLayoutSize != null -> displayLayoutSize.height * displayLayoutSize.width
+            displayController != null -> displayController.getDisplayLayout(taskInfo.displayId)
+                ?.let { it.height() * it.width() }
+            else -> null
+        }
+
+        return TaskSizeUpdate(
+            resizeTrigger,
+            getInputMethodFromMotionEvent(motionEvent),
+            taskInfo.taskId,
+            taskInfo.effectiveUid,
+            height,
+            width,
+            displaySize,
+        )
+    }
+
     fun logTaskInfoStateInit() {
         logTaskUpdate(
             FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD,
@@ -238,7 +312,8 @@
                 taskHeight = 0,
                 taskWidth = 0,
                 taskX = 0,
-                taskY = 0)
+                taskY = 0
+            )
         )
     }
 
@@ -314,7 +389,7 @@
             /* task_width */
             taskSizeUpdate.taskWidth,
             /* display_area */
-            taskSizeUpdate.displayArea
+            taskSizeUpdate.displayArea ?: -1
         )
     }
 
@@ -364,9 +439,24 @@
             val uid: Int,
             val taskHeight: Int,
             val taskWidth: Int,
-            val displayArea: Int,
+            val displayArea: Int?,
         )
 
+        private fun getInputMethodFromMotionEvent(e: MotionEvent?): InputMethod {
+            if (e == null) return InputMethod.UNKNOWN_INPUT_METHOD
+
+            val toolType = e.getToolType(
+                e.findPointerIndex(e.getPointerId(0))
+            )
+            return when {
+                toolType == TOOL_TYPE_STYLUS -> InputMethod.STYLUS
+                toolType == TOOL_TYPE_MOUSE -> InputMethod.MOUSE
+                toolType == TOOL_TYPE_FINGER && e.source == SOURCE_MOUSE -> InputMethod.TOUCHPAD
+                toolType == TOOL_TYPE_FINGER && e.source == SOURCE_TOUCHSCREEN -> InputMethod.TOUCH
+                else -> InputMethod.UNKNOWN_INPUT_METHOD
+            }
+        }
+
         // Default value used when the task was not minimized.
         @VisibleForTesting
         const val UNSET_MINIMIZE_REASON =
@@ -499,6 +589,10 @@
                 FrameworkStatsLog
                     .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__SNAP_RIGHT_MENU_RESIZE_TRIGGER
             ),
+            MAXIMIZE_MENU(
+                FrameworkStatsLog
+                    .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__MAXIMIZE_MENU_RESIZE_TRIGGER
+            ),
         }
 
         /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 443e417..85a3126 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -30,7 +30,6 @@
 import com.android.internal.protolog.ProtoLog
 import com.android.window.flags.Flags
 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
-import com.android.wm.shell.desktopmode.persistence.DesktopTask
 import com.android.wm.shell.desktopmode.persistence.DesktopTaskState
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -124,7 +123,8 @@
         if (!Flags.enableDesktopWindowingPersistence()) return
         //  TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized
         mainCoroutineScope.launch {
-            val desktop = persistentRepository.readDesktop()
+            val desktop = persistentRepository.readDesktop() ?: return@launch
+
             val maxTasks =
                 DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 }
                     ?: desktop.zOrderedTasksCount
@@ -132,13 +132,11 @@
             desktop.zOrderedTasksList
                 // Reverse it so we initialize the repo from bottom to top.
                 .reversed()
-                .map { taskId ->
-                    desktop.tasksByTaskIdMap.getOrDefault(
-                        taskId,
-                        DesktopTask.getDefaultInstance()
-                    )
+                .mapNotNull { taskId ->
+                    desktop.tasksByTaskIdMap[taskId]?.takeIf {
+                        it.desktopTaskState == DesktopTaskState.VISIBLE
+                    }
                 }
-                .filter { task -> task.desktopTaskState == DesktopTaskState.VISIBLE }
                 .take(maxTasks)
                 .forEach { task ->
                     addOrMoveFreeformTaskToTop(desktop.displayId, task.taskId)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index b505bee..69776cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -46,6 +46,7 @@
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.DragEvent
 import android.view.KeyEvent
+import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.view.WindowManager.TRANSIT_CLOSE
@@ -125,7 +126,7 @@
 import java.util.Optional
 import java.util.concurrent.Executor
 import java.util.function.Consumer
-
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 /** Handles moving tasks in and out of desktop */
 class DesktopTasksController(
     private val context: Context,
@@ -158,6 +159,7 @@
     @ShellMainThread private val handler: Handler,
     private val inputManager: InputManager,
     private val focusTransitionObserver: FocusTransitionObserver,
+    private val desktopModeEventLogger: DesktopModeEventLogger,
 ) :
     RemoteCallable<DesktopTasksController>,
     Transitions.TransitionHandler,
@@ -747,7 +749,11 @@
      * bounds) and a free floating state (either the last saved bounds if available or the default
      * bounds otherwise).
      */
-    fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) {
+    fun toggleDesktopTaskSize(
+        taskInfo: RunningTaskInfo,
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
+    ) {
         val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
 
         val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
@@ -794,7 +800,10 @@
 
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding)
         val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
-
+        desktopModeEventLogger.logTaskResizingEnded(
+            resizeTrigger, motionEvent, taskInfo, destinationBounds.height(),
+            destinationBounds.width(), displayController
+        )
         toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
     }
 
@@ -884,9 +893,19 @@
         taskInfo: RunningTaskInfo,
         taskSurface: SurfaceControl,
         currentDragBounds: Rect,
-        position: SnapPosition
+        position: SnapPosition,
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
     ) {
         val destinationBounds = getSnapBounds(taskInfo, position)
+        desktopModeEventLogger.logTaskResizingEnded(
+            resizeTrigger,
+            motionEvent,
+            taskInfo,
+            destinationBounds.height(),
+            destinationBounds.width(),
+            displayController,
+        )
         if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) {
             // Handle the case where we attempt to snap resize when already snap resized: the task
             // position won't need to change but we want to animate the surface going back to the
@@ -915,7 +934,8 @@
         position: SnapPosition,
         taskSurface: SurfaceControl,
         currentDragBounds: Rect,
-        dragStartBounds: Rect
+        dragStartBounds: Rect,
+        motionEvent: MotionEvent,
     ) {
         releaseVisualIndicator()
         if (!taskInfo.isResizeable && DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) {
@@ -932,10 +952,25 @@
                 isResizable = taskInfo.isResizeable,
             )
         } else {
+            val resizeTrigger = if (position == SnapPosition.LEFT) {
+                ResizeTrigger.DRAG_LEFT
+            } else {
+                ResizeTrigger.DRAG_RIGHT
+            }
+            desktopModeEventLogger.logTaskResizingStarted(
+                resizeTrigger, motionEvent, taskInfo, displayController
+            )
             interactionJankMonitor.begin(
                 taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable"
             )
-            snapToHalfScreen(taskInfo, taskSurface, currentDragBounds, position)
+            snapToHalfScreen(
+                taskInfo,
+                taskSurface,
+                currentDragBounds,
+                position,
+                resizeTrigger,
+                motionEvent,
+            )
         }
     }
 
@@ -1735,6 +1770,7 @@
         currentDragBounds: Rect,
         validDragArea: Rect,
         dragStartBounds: Rect,
+        motionEvent: MotionEvent,
     ) {
         if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
             return
@@ -1755,12 +1791,22 @@
             }
             IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
                 handleSnapResizingTask(
-                    taskInfo, SnapPosition.LEFT, taskSurface, currentDragBounds, dragStartBounds
+                    taskInfo,
+                    SnapPosition.LEFT,
+                    taskSurface,
+                    currentDragBounds,
+                    dragStartBounds,
+                    motionEvent,
                 )
             }
             IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
                 handleSnapResizingTask(
-                    taskInfo, SnapPosition.RIGHT, taskSurface, currentDragBounds, dragStartBounds
+                    taskInfo,
+                    SnapPosition.RIGHT,
+                    taskSurface,
+                    currentDragBounds,
+                    dragStartBounds,
+                    motionEvent,
                 )
             }
             IndicatorType.NO_INDICATOR -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
index 3f41d7c..2d11e02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
@@ -73,15 +73,14 @@
      */
     private suspend fun getDesktopRepositoryState(
         userId: Int = DEFAULT_USER_ID
-    ): DesktopRepositoryState =
+    ): DesktopRepositoryState? =
         try {
             dataStoreFlow
                 .first()
-                .desktopRepoByUserMap
-                .getOrDefault(userId, DesktopRepositoryState.getDefaultInstance())
+                .desktopRepoByUserMap[userId]
         } catch (e: Exception) {
             Log.e(TAG, "Unable to read from datastore", e)
-            DesktopRepositoryState.getDefaultInstance()
+            null
         }
 
     /**
@@ -91,13 +90,13 @@
     suspend fun readDesktop(
         userId: Int = DEFAULT_USER_ID,
         desktopId: Int = DEFAULT_DESKTOP_ID,
-    ): Desktop =
+    ): Desktop? =
         try {
             val repository = getDesktopRepositoryState(userId)
-            repository.getDesktopOrThrow(desktopId)
+            repository?.getDesktopOrThrow(desktopId)
         } catch (e: Exception) {
             Log.e(TAG, "Unable to get desktop info from persistent repository", e)
-            Desktop.getDefaultInstance()
+            null
         }
 
     /** Adds or updates a desktop stored in the datastore */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 509cb85..fde01ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -274,6 +274,7 @@
             closeDragResizeListener();
             mDragResizeListener = new DragResizeInputListener(
                     mContext,
+                    mTaskInfo,
                     mHandler,
                     mChoreographer,
                     mDisplay.getDisplayId(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index a06b4a2..a775cbc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -32,6 +32,7 @@
 
 import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU;
 import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing;
+import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger;
 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR;
 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
@@ -103,6 +104,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
 import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
@@ -221,6 +223,7 @@
             };
     private final TaskPositionerFactory mTaskPositionerFactory;
     private final FocusTransitionObserver mFocusTransitionObserver;
+    private final DesktopModeEventLogger mDesktopModeEventLogger;
 
     public DesktopModeWindowDecorViewModel(
             Context context,
@@ -248,7 +251,8 @@
             AppHandleEducationController appHandleEducationController,
             WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
-            FocusTransitionObserver focusTransitionObserver) {
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger) {
         this(
                 context,
                 shellExecutor,
@@ -281,7 +285,8 @@
                 windowDecorCaptionHandleRepository,
                 activityOrientationChangeHandler,
                 new TaskPositionerFactory(),
-                focusTransitionObserver);
+                focusTransitionObserver,
+                desktopModeEventLogger);
     }
 
     @VisibleForTesting
@@ -317,7 +322,8 @@
             WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
             TaskPositionerFactory taskPositionerFactory,
-            FocusTransitionObserver focusTransitionObserver) {
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger) {
         mContext = context;
         mMainExecutor = shellExecutor;
         mMainHandler = mainHandler;
@@ -378,6 +384,7 @@
         };
         mTaskPositionerFactory = taskPositionerFactory;
         mFocusTransitionObserver = focusTransitionObserver;
+        mDesktopModeEventLogger = desktopModeEventLogger;
 
         shellInit.addInitCallback(this::onInit, this);
     }
@@ -547,15 +554,20 @@
                 >= MANAGE_WINDOWS_MINIMUM_INSTANCES);
     }
 
-    private void onMaximizeOrRestore(int taskId, String source) {
+    private void onMaximizeOrRestore(int taskId, String source, ResizeTrigger resizeTrigger,
+            MotionEvent motionEvent) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
         if (decoration == null) {
             return;
         }
+        mDesktopModeEventLogger.logTaskResizingStarted(resizeTrigger, motionEvent,
+                decoration.mTaskInfo,
+                mDisplayController, /* displayLayoutSize= */ null);
         mInteractionJankMonitor.begin(
                 decoration.mTaskSurface, mContext, mMainHandler,
                 Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, source);
-        mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
+        mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo, resizeTrigger,
+                motionEvent);
         decoration.closeHandleMenu();
         decoration.closeMaximizeMenu();
     }
@@ -568,7 +580,7 @@
         mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo);
     }
 
-    private void onSnapResize(int taskId, boolean left) {
+    private void onSnapResize(int taskId, boolean left, MotionEvent motionEvent) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
         if (decoration == null) {
             return;
@@ -579,13 +591,20 @@
             Toast.makeText(mContext,
                     R.string.desktop_mode_non_resizable_snap_text, Toast.LENGTH_SHORT).show();
         } else {
+            ResizeTrigger resizeTrigger =
+                    left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU;
+            mDesktopModeEventLogger.logTaskResizingStarted(resizeTrigger, motionEvent,
+                    decoration.mTaskInfo,
+                    mDisplayController, /* displayLayoutSize= */ null);
             mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler,
                     Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable");
             mDesktopTasksController.snapToHalfScreen(
                     decoration.mTaskInfo,
                     decoration.mTaskSurface,
                     decoration.mTaskInfo.configuration.windowConfiguration.getBounds(),
-                    left ? SnapPosition.LEFT : SnapPosition.RIGHT);
+                    left ? SnapPosition.LEFT : SnapPosition.RIGHT,
+                    resizeTrigger,
+                    motionEvent);
         }
 
         decoration.closeHandleMenu();
@@ -737,6 +756,7 @@
         private boolean mTouchscreenInUse;
         private boolean mHasLongClicked;
         private int mDragPointerId = -1;
+        private MotionEvent mMotionEvent;
 
         private DesktopModeTouchEventListener(
                 RunningTaskInfo taskInfo,
@@ -798,7 +818,8 @@
                 } else {
                     // Full immersive is disabled or task doesn't request/support it, so just
                     // toggle between maximize/restore states.
-                    onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+                    onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button",
+                            ResizeTrigger.MAXIMIZE_BUTTON, mMotionEvent);
                 }
             } else if (id == R.id.minimize_window) {
                 mDesktopTasksController.minimizeTask(decoration.mTaskInfo);
@@ -807,6 +828,7 @@
 
         @Override
         public boolean onTouch(View v, MotionEvent e) {
+            mMotionEvent = e;
             final int id = v.getId();
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             if ((e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN) {
@@ -897,6 +919,7 @@
          */
         @Override
         public boolean onGenericMotion(View v, MotionEvent ev) {
+            mMotionEvent = ev;
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             final int id = v.getId();
             if (ev.getAction() == ACTION_HOVER_ENTER && id == R.id.maximize_window) {
@@ -1040,7 +1063,7 @@
                             taskInfo, decoration.mTaskSurface, position,
                             new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
                             newTaskBounds, decoration.calculateValidDragArea(),
-                            new Rect(mOnDragStartInitialBounds));
+                            new Rect(mOnDragStartInitialBounds), e);
                     if (touchingButton && !mHasLongClicked) {
                         // We need the input event to not be consumed here to end the ripple
                         // effect on the touched button. We will reset drag state in the ensuing
@@ -1087,7 +1110,7 @@
                 // Disallow double-tap to resize when in full immersive.
                 return false;
             }
-            onMaximizeOrRestore(mTaskId, "double_tap");
+            onMaximizeOrRestore(mTaskId, "double_tap", ResizeTrigger.DOUBLE_TAP_APP_HEADER, e);
             return true;
         }
     }
@@ -1484,7 +1507,8 @@
                         mGenericLinksParser,
                         mAssistContentRequester,
                         mMultiInstanceHelper,
-                        mWindowDecorCaptionHandleRepository);
+                        mWindowDecorCaptionHandleRepository,
+                        mDesktopModeEventLogger);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
 
         final TaskPositioner taskPositioner = mTaskPositionerFactory.create(
@@ -1501,15 +1525,16 @@
         final DesktopModeTouchEventListener touchEventListener =
                 new DesktopModeTouchEventListener(taskInfo, taskPositioner);
         windowDecoration.setOnMaximizeOrRestoreClickListener(() -> {
-            onMaximizeOrRestore(taskInfo.taskId, "maximize_menu");
+            onMaximizeOrRestore(taskInfo.taskId, "maximize_menu", ResizeTrigger.MAXIMIZE_MENU,
+                    touchEventListener.mMotionEvent);
             return Unit.INSTANCE;
         });
         windowDecoration.setOnLeftSnapClickListener(() -> {
-            onSnapResize(taskInfo.taskId, true /* isLeft */);
+            onSnapResize(taskInfo.taskId, /* isLeft= */ true, touchEventListener.mMotionEvent);
             return Unit.INSTANCE;
         });
         windowDecoration.setOnRightSnapClickListener(() -> {
-            onSnapResize(taskInfo.taskId, false /* isLeft */);
+            onSnapResize(taskInfo.taskId, /* isLeft= */ false, touchEventListener.mMotionEvent);
             return Unit.INSTANCE;
         });
         windowDecoration.setOnToDesktopClickListener(desktopModeTransitionSource -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 6eb20b9..d94f3a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -94,6 +94,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
@@ -216,7 +217,8 @@
             AppToWebGenericLinksParser genericLinksParser,
             AssistContentRequester assistContentRequester,
             MultiInstanceHelper multiInstanceHelper,
-            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
+            DesktopModeEventLogger desktopModeEventLogger) {
         this (context, userContext, displayController, splitScreenController, desktopRepository,
                 taskOrganizer, taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
                 appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser,
@@ -227,7 +229,7 @@
                 new SurfaceControlViewHostFactory() {},
                 DefaultMaximizeMenuFactory.INSTANCE,
                 DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper,
-                windowDecorCaptionHandleRepository);
+                windowDecorCaptionHandleRepository, desktopModeEventLogger);
     }
 
     DesktopModeWindowDecoration(
@@ -256,11 +258,12 @@
             MaximizeMenuFactory maximizeMenuFactory,
             HandleMenuFactory handleMenuFactory,
             MultiInstanceHelper multiInstanceHelper,
-            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
+            DesktopModeEventLogger desktopModeEventLogger) {
         super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                 windowContainerTransactionSupplier, surfaceControlSupplier,
-                surfaceControlViewHostFactory);
+                surfaceControlViewHostFactory, desktopModeEventLogger);
         mSplitScreenController = splitScreenController;
         mHandler = handler;
         mBgExecutor = bgExecutor;
@@ -605,6 +608,7 @@
             Trace.beginSection("DesktopModeWindowDecoration#relayout-DragResizeInputListener");
             mDragResizeListener = new DragResizeInputListener(
                     mContext,
+                    mTaskInfo,
                     mHandler,
                     mChoreographer,
                     mDisplay.getDisplayId(),
@@ -612,7 +616,8 @@
                     mDragPositioningCallback,
                     mSurfaceControlBuilderSupplier,
                     mSurfaceControlTransactionSupplier,
-                    mDisplayController);
+                    mDisplayController,
+                    mDesktopModeEventLogger);
             Trace.endSection();
         }
 
@@ -1700,7 +1705,8 @@
                 AppToWebGenericLinksParser genericLinksParser,
                 AssistContentRequester assistContentRequester,
                 MultiInstanceHelper multiInstanceHelper,
-                WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
+                WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
+                DesktopModeEventLogger desktopModeEventLogger) {
             return new DesktopModeWindowDecoration(
                     context,
                     userContext,
@@ -1719,7 +1725,8 @@
                     genericLinksParser,
                     assistContentRequester,
                     multiInstanceHelper,
-                    windowDecorCaptionHandleRepository);
+                    windowDecorCaptionHandleRepository,
+                    desktopModeEventLogger);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index 4ff394e..4204097 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -29,10 +29,12 @@
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT;
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP;
+import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEventFromTouchscreen;
 
 import android.annotation.NonNull;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -59,6 +61,7 @@
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 
 import java.util.function.Consumer;
 import java.util.function.Supplier;
@@ -83,14 +86,17 @@
     private final TaskResizeInputEventReceiver mInputEventReceiver;
 
     private final Context mContext;
+    private final RunningTaskInfo mTaskInfo;
     private final SurfaceControl mInputSinkSurface;
     private final IBinder mSinkClientToken;
     private final InputChannel mSinkInputChannel;
     private final DisplayController mDisplayController;
+    private final DesktopModeEventLogger mDesktopModeEventLogger;
     private final Region mTouchRegion = new Region();
 
     DragResizeInputListener(
             Context context,
+            RunningTaskInfo taskInfo,
             Handler handler,
             Choreographer choreographer,
             int displayId,
@@ -98,12 +104,15 @@
             DragPositioningCallback callback,
             Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
             Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
-            DisplayController displayController) {
+            DisplayController displayController,
+            DesktopModeEventLogger desktopModeEventLogger) {
         mContext = context;
+        mTaskInfo = taskInfo;
         mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
         mDisplayId = displayId;
         mDecorationSurface = decorationSurface;
         mDisplayController = displayController;
+        mDesktopModeEventLogger = desktopModeEventLogger;
         mClientToken = new Binder();
         final InputTransferToken inputTransferToken = new InputTransferToken();
         mInputChannel = new InputChannel();
@@ -125,11 +134,12 @@
             e.rethrowFromSystemServer();
         }
 
-        mInputEventReceiver = new TaskResizeInputEventReceiver(context, mInputChannel, callback,
+        mInputEventReceiver = new TaskResizeInputEventReceiver(context, mTaskInfo, mInputChannel,
+                callback,
                 handler, choreographer, () -> {
             final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId);
             return new Size(layout.width(), layout.height());
-        }, this::updateSinkInputChannel);
+        }, this::updateSinkInputChannel, mDesktopModeEventLogger);
         mInputEventReceiver.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
 
         mInputSinkSurface = surfaceControlBuilderSupplier.get()
@@ -163,6 +173,22 @@
         }
     }
 
+    DragResizeInputListener(
+            Context context,
+            RunningTaskInfo taskInfo,
+            Handler handler,
+            Choreographer choreographer,
+            int displayId,
+            SurfaceControl decorationSurface,
+            DragPositioningCallback callback,
+            Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
+            Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
+            DisplayController displayController) {
+        this(context, taskInfo, handler, choreographer, displayId, decorationSurface, callback,
+                surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, displayController,
+                new DesktopModeEventLogger());
+    }
+
     /**
      * Updates the geometry (the touch region) of this drag resize handler.
      *
@@ -274,6 +300,7 @@
     private static class TaskResizeInputEventReceiver extends InputEventReceiver implements
             DragDetector.MotionEventHandler {
         @NonNull private final Context mContext;
+        @NonNull private final RunningTaskInfo mTaskInfo;
         private final InputManager mInputManager;
         @NonNull private final InputChannel mInputChannel;
         @NonNull private final DragPositioningCallback mCallback;
@@ -282,6 +309,7 @@
         @NonNull private final DragDetector mDragDetector;
         @NonNull private final Supplier<Size> mDisplayLayoutSizeSupplier;
         @NonNull private final Consumer<Region> mTouchRegionConsumer;
+        @NonNull private final DesktopModeEventLogger mDesktopModeEventLogger;
         private final Rect mTmpRect = new Rect();
         private boolean mConsumeBatchEventScheduled;
         private DragResizeWindowGeometry mDragResizeWindowGeometry;
@@ -293,15 +321,24 @@
         // resize events. For example, if multiple fingers are touching the screen, then each one
         // has a separate pointer id, but we only accept drag input from one.
         private int mDragPointerId = -1;
+        // The type of resizing that is currently being done. Used to track the same resize trigger
+        // on start and end of the resizing action.
+        private ResizeTrigger mResizeTrigger = ResizeTrigger.UNKNOWN_RESIZE_TRIGGER;
+        // The last MotionEvent on ACTION_DOWN, used to track the input tool type and source for
+        // logging the start and end of the resizing action.
+        private MotionEvent mLastMotionEventOnDown;
 
         private TaskResizeInputEventReceiver(@NonNull Context context,
+                @NonNull RunningTaskInfo taskInfo,
                 @NonNull InputChannel inputChannel,
                 @NonNull DragPositioningCallback callback, @NonNull Handler handler,
                 @NonNull Choreographer choreographer,
                 @NonNull Supplier<Size> displayLayoutSizeSupplier,
-                @NonNull Consumer<Region> touchRegionConsumer) {
+                @NonNull Consumer<Region> touchRegionConsumer,
+                @NonNull DesktopModeEventLogger desktopModeEventLogger) {
             super(inputChannel, handler.getLooper());
             mContext = context;
+            mTaskInfo = taskInfo;
             mInputManager = context.getSystemService(InputManager.class);
             mInputChannel = inputChannel;
             mCallback = callback;
@@ -322,6 +359,7 @@
                     ViewConfiguration.get(mContext).getScaledTouchSlop());
             mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier;
             mTouchRegionConsumer = touchRegionConsumer;
+            mDesktopModeEventLogger = desktopModeEventLogger;
         }
 
         /**
@@ -395,6 +433,7 @@
         @Override
         public boolean handleMotionEvent(View v, MotionEvent e) {
             boolean result = false;
+
             // Check if this is a touch event vs mouse event.
             // Touch events are tracked in four corners. Other events are tracked in resize edges.
             switch (e.getActionMasked()) {
@@ -416,6 +455,13 @@
                                 "%s: Handling action down, update ctrlType to %d", TAG, ctrlType);
                         mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType,
                                 rawX, rawY);
+                        mLastMotionEventOnDown = e;
+                        mResizeTrigger = (ctrlType == CTRL_TYPE_BOTTOM || ctrlType == CTRL_TYPE_TOP
+                                || ctrlType == CTRL_TYPE_RIGHT || ctrlType == CTRL_TYPE_LEFT)
+                                ? ResizeTrigger.EDGE : ResizeTrigger.CORNER;
+                        mDesktopModeEventLogger.logTaskResizingStarted(mResizeTrigger,
+                                e, mTaskInfo, /* displayController= */ null,
+                                /* displayLayoutSize= */ mDisplayLayoutSizeSupplier.get());
                         // Increase the input sink region to cover the whole screen; this is to
                         // prevent input and focus from going to other tasks during a drag resize.
                         updateInputSinkRegionForDrag(mDragStartTaskBounds);
@@ -464,6 +510,12 @@
                         if (taskBounds.equals(mDragStartTaskBounds)) {
                             mTouchRegionConsumer.accept(mTouchRegion);
                         }
+
+                        mDesktopModeEventLogger.logTaskResizingEnded(mResizeTrigger,
+                                mLastMotionEventOnDown, mTaskInfo, taskBounds.height(),
+                                taskBounds.width(),
+                                /* displayController= */ null,
+                                /* displayLayoutSize= */ mDisplayLayoutSizeSupplier.get());
                     }
                     mShouldHandleEvents = false;
                     mDragPointerId = -1;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index 33d1c26..844ceb3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -181,7 +181,7 @@
     }
 
     private boolean isInEdgeResizeBounds(float x, float y) {
-        return calculateEdgeResizeCtrlType(x, y) != 0;
+        return calculateEdgeResizeCtrlType(x, y) != CTRL_TYPE_UNDEFINED;
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 6b3b357..34cc098 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -58,6 +58,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement;
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer;
@@ -111,6 +112,7 @@
     final Context mContext;
     final @NonNull Context mUserContext;
     final @NonNull DisplayController mDisplayController;
+    final @NonNull DesktopModeEventLogger mDesktopModeEventLogger;
     final ShellTaskOrganizer mTaskOrganizer;
     final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier;
     final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
@@ -163,7 +165,7 @@
         this(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                 SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
                 WindowContainerTransaction::new, SurfaceControl::new,
-                new SurfaceControlViewHostFactory() {});
+                new SurfaceControlViewHostFactory() {}, new DesktopModeEventLogger());
     }
 
     WindowDecoration(
@@ -177,13 +179,16 @@
             Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
             Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
             Supplier<SurfaceControl> surfaceControlSupplier,
-            SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
+            SurfaceControlViewHostFactory surfaceControlViewHostFactory,
+            @NonNull DesktopModeEventLogger desktopModeEventLogger
+    ) {
         mContext = context;
         mUserContext = userContext;
         mDisplayController = displayController;
         mTaskOrganizer = taskOrganizer;
         mTaskInfo = taskInfo;
         mTaskSurface = cloneSurfaceControl(taskSurface, surfaceControlSupplier);
+        mDesktopModeEventLogger = desktopModeEventLogger;
         mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier;
         mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
         mWindowContainerTransactionSupplier = windowContainerTransactionSupplier;
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
index b7ddfd1..4fe66f3 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
@@ -298,12 +298,18 @@
             FlickerConfigEntry(
                 scenarioId = ScenarioId("MAXIMIZE_APP"),
                 extractor =
-                TaggedScenarioExtractorBuilder()
-                    .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
-                    .setTransitionMatcher(
-                        TaggedCujTransitionMatcher(associatedTransitionRequired = false)
-                    )
-                    .build(),
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                            return transitions.filter {
+                                it.type == TransitionType.DESKTOP_MODE_TOGGLE_RESIZE
+                            }
+                        }
+                    }
+                ),
                 assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
                         listOf(
                             AppLayerIncreasesInSize(DESKTOP_MODE_APP),
@@ -316,12 +322,18 @@
             FlickerConfigEntry(
                 scenarioId = ScenarioId("MAXIMIZE_APP_NON_RESIZABLE"),
                 extractor =
-                TaggedScenarioExtractorBuilder()
-                    .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
-                    .setTransitionMatcher(
-                        TaggedCujTransitionMatcher(associatedTransitionRequired = false)
-                    )
-                    .build(),
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                            return transitions.filter {
+                                it.type == TransitionType.DESKTOP_MODE_TOGGLE_RESIZE
+                            }
+                        }
+                    }
+                ),
                 assertions =
                 AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
                         listOf(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
index e6bd05b..f935ac7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
@@ -40,6 +40,8 @@
 
     private WindowContainerToken mToken = createMockWCToken();
     private int mParentTaskId = INVALID_TASK_ID;
+    private int mUid = INVALID_TASK_ID;
+    private int mTaskId = INVALID_TASK_ID;
     private Intent mBaseIntent = new Intent();
     private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD;
     private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED;
@@ -73,6 +75,18 @@
         return this;
     }
 
+    /** Sets the task info's effective UID. */
+    public TestRunningTaskInfoBuilder setUid(int uid) {
+        mUid = uid;
+        return this;
+    }
+
+    /** Sets the task info's UID. */
+    public TestRunningTaskInfoBuilder setTaskId(int taskId) {
+        mTaskId = taskId;
+        return this;
+    }
+
     /**
      * Set {@link ActivityManager.RunningTaskInfo#baseIntent} for the task info, by default
      * an empty intent is assigned
@@ -132,7 +146,8 @@
 
     public ActivityManager.RunningTaskInfo build() {
         final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
-        info.taskId = sNextTaskId++;
+        info.taskId = (mTaskId == INVALID_TASK_ID) ? sNextTaskId++ : mTaskId;
+        info.effectiveUid = mUid;
         info.baseIntent = mBaseIntent;
         info.parentTaskId = mParentTaskId;
         info.displayId = mDisplayId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index 0825b6b..2a82e6e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -16,9 +16,12 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker
 import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
 import com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions
@@ -27,6 +30,9 @@
 import com.android.window.flags.Flags
 import com.android.wm.shell.EventLogTags
 import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
@@ -39,9 +45,13 @@
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UNSET_UNMINIMIZE_REASON
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 /**
  * Tests for [DesktopModeEventLogger].
@@ -49,6 +59,8 @@
 class DesktopModeEventLoggerTest : ShellTestCase() {
 
     private val desktopModeEventLogger = DesktopModeEventLogger()
+    val displayController = mock<DisplayController>()
+    val displayLayout = mock<DisplayLayout>()
 
     @JvmField
     @Rule(order = 0)
@@ -60,6 +72,13 @@
     @Rule(order = 1)
     val setFlagsRule = SetFlagsRule()
 
+    @Before
+    fun setUp() {
+        doReturn(displayLayout).whenever(displayController).getDisplayLayout(anyInt())
+        doReturn(DISPLAY_WIDTH).whenever(displayLayout).width()
+        doReturn(DISPLAY_HEIGHT).whenever(displayLayout).height()
+    }
+
     @Test
     fun logSessionEnter_logsEnterReasonWithNewSessionId() {
         desktopModeEventLogger.logSessionEnter(EnterReason.KEYBOARD_SHORTCUT_ENTER)
@@ -467,7 +486,8 @@
 
     @Test
     fun logTaskResizingStarted_noOngoingSession_doesNotLog() {
-        desktopModeEventLogger.logTaskResizingStarted(TASK_SIZE_UPDATE)
+        desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER,
+            null, createTaskInfo())
 
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -478,13 +498,14 @@
     fun logTaskResizingStarted_logsTaskSizeUpdatedWithStartResizingStage() {
         val sessionId = startDesktopModeSession()
 
-        desktopModeEventLogger.logTaskResizingStarted(TASK_SIZE_UPDATE)
+        desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER,
+            null, createTaskInfo(), displayController)
 
         verify {
             FrameworkStatsLog.write(
                 eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
                 /* resize_trigger */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__UNKNOWN_RESIZE_TRIGGER),
+                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER),
                 /* resizing_stage */
                 eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE),
                 /* input_method */
@@ -500,7 +521,7 @@
                 /* task_width */
                 eq(TASK_SIZE_UPDATE.taskWidth),
                 /* display_area */
-                eq(TASK_SIZE_UPDATE.displayArea),
+                eq(DISPLAY_AREA),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
@@ -508,7 +529,8 @@
 
     @Test
     fun logTaskResizingEnded_noOngoingSession_doesNotLog() {
-        desktopModeEventLogger.logTaskResizingEnded(TASK_SIZE_UPDATE)
+        desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER,
+            null, createTaskInfo())
 
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -519,13 +541,14 @@
     fun logTaskResizingEnded_logsTaskSizeUpdatedWithEndResizingStage() {
         val sessionId = startDesktopModeSession()
 
-        desktopModeEventLogger.logTaskResizingEnded(TASK_SIZE_UPDATE)
+        desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER,
+            null, createTaskInfo(), displayController = displayController)
 
         verify {
             FrameworkStatsLog.write(
                 eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
                 /* resize_trigger */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__UNKNOWN_RESIZE_TRIGGER),
+                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER),
                 /* resizing_stage */
                 eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE),
                 /* input_method */
@@ -541,7 +564,7 @@
                 /* task_width */
                 eq(TASK_SIZE_UPDATE.taskWidth),
                 /* display_area */
-                eq(TASK_SIZE_UPDATE.displayArea),
+                eq(DISPLAY_AREA),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
@@ -585,8 +608,14 @@
         }
     }
 
+    private fun createTaskInfo(): RunningTaskInfo {
+        return TestRunningTaskInfoBuilder().setTaskId(TASK_ID)
+            .setUid(TASK_UID)
+            .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT))
+            .build()
+    }
+
     private companion object {
-        private const val sessionId = 1
         private const val TASK_ID = 1
         private const val TASK_UID = 1
         private const val TASK_X = 0
@@ -594,7 +623,9 @@
         private const val TASK_HEIGHT = 100
         private const val TASK_WIDTH = 100
         private const val TASK_COUNT = 1
-        private const val DISPLAY_AREA = 1000
+        private const val DISPLAY_WIDTH = 500
+        private const val DISPLAY_HEIGHT = 500
+        private const val DISPLAY_AREA = DISPLAY_HEIGHT * DISPLAY_WIDTH
 
         private val TASK_UPDATE = TaskUpdate(
             TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 55b44ac..7c336cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -54,6 +54,7 @@
 import android.view.DragEvent
 import android.view.Gravity
 import android.view.KeyEvent
+import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.WindowInsets
 import android.view.WindowManager
@@ -99,6 +100,7 @@
 import com.android.wm.shell.common.MultiInstanceHelper
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
@@ -214,9 +216,11 @@
   @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
   @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
   @Mock private lateinit var mockHandler: Handler
+  @Mock private lateinit var desktopModeEventLogger: DesktopModeEventLogger
   @Mock lateinit var persistentRepository: DesktopPersistentRepository
   @Mock private lateinit var mockInputManager: InputManager
   @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
+  @Mock lateinit var motionEvent: MotionEvent
 
   private lateinit var mockitoSession: StaticMockitoSession
   private lateinit var controller: DesktopTasksController
@@ -295,6 +299,8 @@
     recentsTransitionStateListener = captor.value
 
     controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+
+    assumeTrue(ENABLE_SHELL_TRANSITIONS)
   }
 
   private fun createController(): DesktopTasksController {
@@ -329,6 +335,7 @@
         mockHandler,
         mockInputManager,
         mockFocusTransitionObserver,
+        desktopModeEventLogger,
       )
   }
 
@@ -357,9 +364,17 @@
     val task1 = setUpFreeformTask()
 
     val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-    controller.toggleDesktopTaskSize(task1)
-    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+    controller.toggleDesktopTaskSize(task1, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
+    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task1,
+      STABLE_BOUNDS.height(),
+      STABLE_BOUNDS.width(),
+      displayController
+    )
     assertThat(argumentCaptor.value).isTrue()
   }
 
@@ -376,9 +391,17 @@
     val task1 = setUpFreeformTask(bounds = stableBounds, active = true)
 
     val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-    controller.toggleDesktopTaskSize(task1)
-    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+    controller.toggleDesktopTaskSize(task1, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
+    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task1,
+      0,
+      0,
+      displayController
+    )
     assertThat(argumentCaptor.value).isFalse()
   }
 
@@ -754,7 +777,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
   fun handleRequest_newFreeformTaskLaunch_cascadeApplied() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     setUpLandscapeDisplay()
     val stableBounds = Rect()
     displayLayout.getStableBoundsForDesktopMode(stableBounds)
@@ -773,7 +795,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
   fun handleRequest_freeformTaskAlreadyExistsInDesktopMode_cascadeNotApplied() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     setUpLandscapeDisplay()
     val stableBounds = Rect()
     displayLayout.getStableBoundsForDesktopMode(stableBounds)
@@ -1773,8 +1794,6 @@
 
   @Test
   fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val homeTask = setUpHomeTask()
     val freeformTask = setUpFreeformTask()
     markTaskVisible(freeformTask)
@@ -1791,8 +1810,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val homeTask = setUpHomeTask()
     val freeformTask = setUpFreeformTask()
     markTaskVisible(freeformTask)
@@ -1818,8 +1835,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask = setUpFreeformTask()
     markTaskVisible(freeformTask)
     val fullscreenTask = createFullscreenTask()
@@ -1833,8 +1848,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     freeformTasks.forEach { markTaskVisible(it) }
     val fullscreenTask = createFullscreenTask()
@@ -1849,8 +1862,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     freeformTasks.forEach { markTaskVisible(it) }
     val fullscreenTask = createFullscreenTask()
@@ -1866,8 +1877,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val minimizedTask = setUpFreeformTask()
     taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId)
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
@@ -1888,7 +1897,6 @@
 
   @Test
   fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -1905,7 +1913,6 @@
 
   @Test
   fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
@@ -1918,8 +1925,6 @@
 
   @Test
   fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask = setUpFreeformTask()
     markTaskHidden(freeformTask)
     val fullscreenTask = createFullscreenTask()
@@ -1928,16 +1933,12 @@
 
   @Test
   fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val fullscreenTask = createFullscreenTask()
     assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
   }
 
   @Test
   fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
     createFreeformTask(displayId = SECOND_DISPLAY)
 
@@ -1947,8 +1948,6 @@
 
   @Test
   fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     freeformTasks.forEach { markTaskVisible(it) }
     val newFreeformTask = createFreeformTask()
@@ -1961,8 +1960,6 @@
 
   @Test
   fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask = setUpFreeformTask()
     markTaskHidden(freeformTask)
 
@@ -1977,7 +1974,6 @@
 
   @Test
   fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -1992,7 +1988,6 @@
 
   @Test
   fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
@@ -2009,8 +2004,6 @@
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask1 = setUpFreeformTask()
     val freeformTask2 = createFreeformTask()
 
@@ -2026,8 +2019,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask1 = setUpFreeformTask()
     val freeformTask2 = createFreeformTask()
 
@@ -2048,8 +2039,6 @@
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val task = createFreeformTask()
     val result = controller.handleRequest(Binder(), createTransition(task))
 
@@ -2061,8 +2050,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val task = createFreeformTask()
     val result = controller.handleRequest(Binder(), createTransition(task))
 
@@ -2077,8 +2064,6 @@
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
     // Second display task
     createFreeformTask(displayId = SECOND_DISPLAY)
@@ -2093,8 +2078,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
     // Second display task
     createFreeformTask(displayId = SECOND_DISPLAY)
@@ -2111,7 +2094,6 @@
 
   @Test
   fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false)
 
     val freeformTask1 = setUpFreeformTask()
@@ -2125,7 +2107,6 @@
 
   @Test
   fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true)
 
     val freeformTask1 = setUpFreeformTask()
@@ -2139,7 +2120,6 @@
 
   @Test
   fun handleRequest_freeformTask_keyguardLocked_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
     val freeformTask = createFreeformTask(displayId = DEFAULT_DISPLAY)
 
@@ -2150,8 +2130,6 @@
 
   @Test
   fun handleRequest_notOpenOrToFrontTransition_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val task =
         TestRunningTaskInfoBuilder()
             .setActivityType(ACTIVITY_TYPE_STANDARD)
@@ -2164,21 +2142,17 @@
 
   @Test
   fun handleRequest_noTriggerTask_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull()
   }
 
   @Test
   fun handleRequest_triggerTaskNotStandard_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
     assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
   }
 
   @Test
   fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val task =
         TestRunningTaskInfoBuilder()
             .setActivityType(ACTIVITY_TYPE_STANDARD)
@@ -2860,7 +2834,8 @@
         PointF(200f, -200f), /* inputCoordinate */
         Rect(100, -100, 500, 1000), /* currentDragBounds */
         Rect(0, 50, 2000, 2000), /* validDragArea */
-        Rect() /* dragStartBounds */ )
+        Rect() /* dragStartBounds */,
+        motionEvent)
     val rectAfterEnd = Rect(100, 50, 500, 1150)
     verify(transitions)
         .startTransition(
@@ -2895,7 +2870,8 @@
       PointF(200f, 300f), /* inputCoordinate */
       currentDragBounds, /* currentDragBounds */
       Rect(0, 50, 2000, 2000) /* validDragArea */,
-      Rect() /* dragStartBounds */)
+      Rect() /* dragStartBounds */,
+      motionEvent)
 
 
     verify(transitions)
@@ -3142,10 +3118,19 @@
     val bounds = Rect(0, 0, 100, 100)
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
 
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
+
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      STABLE_BOUNDS.height(),
+      STABLE_BOUNDS.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3164,15 +3149,22 @@
       STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
     )
 
-    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT)
+    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
     assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.SNAP_LEFT_MENU,
+      motionEvent,
+      task,
+      expectedBounds.height(),
+      expectedBounds.width(),
+      displayController
+    )
   }
 
   @Test
   fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     // Set up task to already be in snapped-left bounds
     val bounds = Rect(
       STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
@@ -3187,7 +3179,7 @@
 
     // Attempt to snap left again
     val currentDragBounds = Rect(bounds).apply { offset(-100, 0) }
-    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT)
+    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
 
     // Assert that task is NOT updated via WCT
     verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
@@ -3200,6 +3192,14 @@
       eq(bounds),
       eq(true)
     )
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.SNAP_LEFT_MENU,
+      motionEvent,
+      task,
+      bounds.height(),
+      bounds.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3210,12 +3210,22 @@
     }
     val preDragBounds = Rect(100, 100, 400, 500)
     val currentDragBounds = Rect(0, 100, 300, 500)
+    val expectedBounds =
+      Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom)
 
     controller.handleSnapResizingTask(
-      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds)
+      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent
+    )
     val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
     assertThat(findBoundsChange(wct, task)).isEqualTo(
-      Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom))
+      expectedBounds
+    )
+    verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
+      ResizeTrigger.DRAG_LEFT,
+      motionEvent,
+      task,
+      displayController
+    )
   }
 
   @Test
@@ -3228,7 +3238,7 @@
     val currentDragBounds = Rect(0, 100, 300, 500)
 
     controller.handleSnapResizingTask(
-      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds)
+      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent)
     verify(mReturnToDragStartAnimator).start(
       eq(task.taskId),
       eq(mockSurface),
@@ -3236,6 +3246,13 @@
       eq(preDragBounds),
       eq(false)
     )
+    verify(desktopModeEventLogger, never()).logTaskResizingStarted(
+      any(),
+      any(),
+      any(),
+      any(),
+      any()
+    )
   }
 
   @Test
@@ -3254,10 +3271,19 @@
     // Bounds should be 1000 x 500, vertically centered in the 1000 x 1000 stable bounds
     val expectedBounds = Rect(STABLE_BOUNDS.left, 250, STABLE_BOUNDS.right, 750)
 
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
+
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      expectedBounds.height(),
+      expectedBounds.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3265,8 +3291,12 @@
     val bounds = Rect(0, 0, 100, 100)
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
 
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds)
+    verify(desktopModeEventLogger, never()).logTaskResizingEnded(
+      any(), any(), any(), any(),
+      any(), any(), any()
+    )
   }
 
   @Test
@@ -3275,15 +3305,23 @@
     val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
 
     // Maximize
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
 
     // Restore
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
     // Assert bounds set to last bounds before maximize
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      boundsBeforeMaximize.height(),
+      boundsBeforeMaximize.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3294,16 +3332,24 @@
     }
 
     // Maximize
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS.left,
       boundsBeforeMaximize.top, STABLE_BOUNDS.right, boundsBeforeMaximize.bottom)
 
     // Restore
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
     // Assert bounds set to last bounds before maximize
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      boundsBeforeMaximize.height(),
+      boundsBeforeMaximize.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3314,16 +3360,24 @@
     }
 
     // Maximize
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     task.configuration.windowConfiguration.bounds.set(boundsBeforeMaximize.left,
       STABLE_BOUNDS.top, boundsBeforeMaximize.right, STABLE_BOUNDS.bottom)
 
     // Restore
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
     // Assert bounds set to last bounds before maximize
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      boundsBeforeMaximize.height(),
+      boundsBeforeMaximize.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3332,14 +3386,22 @@
     val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
 
     // Maximize
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
 
     // Restore
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
     // Assert last bounds before maximize removed after use
     assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      boundsBeforeMaximize.height(),
+      boundsBeforeMaximize.width(),
+      displayController
+    )
   }
 
 
@@ -3738,14 +3800,11 @@
       handlerClass: Class<out TransitionHandler>? = null
   ): WindowContainerTransaction {
     val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      if (handlerClass == null) {
-        verify(transitions).startTransition(eq(type), arg.capture(), isNull())
-      } else {
-        verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
-      }
+
+    if (handlerClass == null) {
+      verify(transitions).startTransition(eq(type), arg.capture(), isNull())
     } else {
-      verify(shellTaskOrganizer).applyTransaction(arg.capture())
+      verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
     }
     return arg.value
   }
@@ -3755,43 +3814,27 @@
   ): WindowContainerTransaction {
     val arg: ArgumentCaptor<WindowContainerTransaction> =
         ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
+    verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
         .startTransition(capture(arg), eq(currentBounds))
-    } else {
-      verify(shellTaskOrganizer).applyTransaction(capture(arg))
-    }
     return arg.value
   }
 
   private fun getLatestEnterDesktopWct(): WindowContainerTransaction {
     val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
-    } else {
-      verify(shellTaskOrganizer).applyTransaction(arg.capture())
-    }
+    verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
     return arg.value
   }
 
   private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
     val arg: ArgumentCaptor<WindowContainerTransaction> =
         ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
-    } else {
-      verify(shellTaskOrganizer).applyTransaction(capture(arg))
-    }
+    verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
     return arg.value
   }
 
   private fun getLatestExitDesktopWct(): WindowContainerTransaction {
     val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
-    } else {
-      verify(shellTaskOrganizer).applyTransaction(arg.capture())
-    }
+    verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
     return arg.value
   }
 
@@ -3799,27 +3842,15 @@
       wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
 
   private fun verifyWCTNotExecuted() {
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(transitions, never()).startTransition(anyInt(), any(), isNull())
-    } else {
-      verify(shellTaskOrganizer, never()).applyTransaction(any())
-    }
+    verify(transitions, never()).startTransition(anyInt(), any(), isNull())
   }
 
   private fun verifyExitDesktopWCTNotExecuted() {
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any())
-    } else {
-      verify(shellTaskOrganizer, never()).applyTransaction(any())
-    }
+    verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any())
   }
 
   private fun verifyEnterDesktopWCTNotExecuted() {
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any())
-    } else {
-      verify(shellTaskOrganizer, never()).applyTransaction(any())
-    }
+    verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any())
   }
 
   private fun createTransition(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
index 9b9703f..8495580 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
@@ -115,8 +115,8 @@
                 freeformTasksInZOrder = freeformTasksInZOrder)
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
-            assertThat(actualDesktop.tasksByTaskIdMap).hasSize(2)
-            assertThat(actualDesktop.getZOrderedTasks(0)).isEqualTo(2)
+            assertThat(actualDesktop?.tasksByTaskIdMap).hasSize(2)
+            assertThat(actualDesktop?.getZOrderedTasks(0)).isEqualTo(2)
         }
     }
 
@@ -138,7 +138,7 @@
                 freeformTasksInZOrder = freeformTasksInZOrder)
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
-            assertThat(actualDesktop.tasksByTaskIdMap[task.taskId]?.desktopTaskState)
+            assertThat(actualDesktop?.tasksByTaskIdMap?.get(task.taskId)?.desktopTaskState)
                 .isEqualTo(DesktopTaskState.MINIMIZED)
         }
     }
@@ -161,8 +161,8 @@
                 freeformTasksInZOrder = freeformTasksInZOrder)
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
-            assertThat(actualDesktop.tasksByTaskIdMap).isEmpty()
-            assertThat(actualDesktop.zOrderedTasksList).isEmpty()
+            assertThat(actualDesktop?.tasksByTaskIdMap).isEmpty()
+            assertThat(actualDesktop?.zOrderedTasksList).isEmpty()
         }
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 175fbd2..1839b8a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -87,6 +87,8 @@
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 import com.android.wm.shell.desktopmode.DesktopRepository
 import com.android.wm.shell.desktopmode.DesktopTasksController
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
@@ -194,7 +196,11 @@
     @Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController
     @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
     @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
+    @Mock private lateinit var motionEvent: MotionEvent
+    @Mock lateinit var displayController: DisplayController
+    @Mock lateinit var displayLayout: DisplayLayout
     private lateinit var spyContext: TestableContext
+    private lateinit var desktopModeEventLogger: DesktopModeEventLogger
 
     private val transactionFactory = Supplier<SurfaceControl.Transaction> {
         SurfaceControl.Transaction()
@@ -224,6 +230,7 @@
         shellInit = ShellInit(mockShellExecutor)
         windowDecorByTaskIdSpy.clear()
         spyContext.addMockSystemService(InputManager::class.java, mockInputManager)
+        desktopModeEventLogger = mock<DesktopModeEventLogger>()
         desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
                 spyContext,
                 mockShellExecutor,
@@ -256,7 +263,8 @@
                 mockCaptionHandleRepository,
                 Optional.of(mockActivityOrientationChangeHandler),
                 mockTaskPositionerFactory,
-                mockFocusTransitionObserver
+                mockFocusTransitionObserver,
+                desktopModeEventLogger
         )
         desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
         whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -299,6 +307,10 @@
             argumentCaptor<DesktopModeKeyguardChangeListener>()
         verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture())
         desktopModeOnKeyguardChangedListener = keyguardChangedCaptor.firstValue
+        whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+        }
     }
 
     @After
@@ -612,7 +624,11 @@
 
         maxOrRestoreListenerCaptor.value.invoke()
 
-        verify(mockDesktopTasksController).toggleDesktopTaskSize(decor.mTaskInfo)
+        verify(mockDesktopTasksController).toggleDesktopTaskSize(
+            decor.mTaskInfo,
+            ResizeTrigger.MAXIMIZE_MENU,
+            null
+        )
     }
 
     @Test
@@ -647,7 +663,9 @@
             eq(decor.mTaskInfo),
             taskSurfaceCaptor.capture(),
             eq(currentBounds),
-            eq(SnapPosition.LEFT)
+            eq(SnapPosition.LEFT),
+            eq(ResizeTrigger.SNAP_LEFT_MENU),
+            eq(null)
         )
         assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface)
     }
@@ -685,7 +703,9 @@
             eq(decor.mTaskInfo),
             taskSurfaceCaptor.capture(),
             eq(currentBounds),
-            eq(SnapPosition.LEFT)
+            eq(SnapPosition.LEFT),
+            eq(ResizeTrigger.SNAP_LEFT_MENU),
+            eq(null)
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -704,7 +724,9 @@
         onLeftSnapClickListenerCaptor.value.invoke()
 
         verify(mockDesktopTasksController, never())
-            .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT))
+            .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
+                eq(ResizeTrigger.MAXIMIZE_BUTTON),
+                eq(null))
         verify(mockToast).show()
     }
 
@@ -725,7 +747,9 @@
             eq(decor.mTaskInfo),
             taskSurfaceCaptor.capture(),
             eq(currentBounds),
-            eq(SnapPosition.RIGHT)
+            eq(SnapPosition.RIGHT),
+            eq(ResizeTrigger.SNAP_RIGHT_MENU),
+            eq(null)
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -763,7 +787,9 @@
             eq(decor.mTaskInfo),
             taskSurfaceCaptor.capture(),
             eq(currentBounds),
-            eq(SnapPosition.RIGHT)
+            eq(SnapPosition.RIGHT),
+            eq(ResizeTrigger.SNAP_RIGHT_MENU),
+            eq(null)
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -782,7 +808,9 @@
         onRightSnapClickListenerCaptor.value.invoke()
 
         verify(mockDesktopTasksController, never())
-            .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT))
+            .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
+                eq(ResizeTrigger.MAXIMIZE_BUTTON),
+                eq(null))
         verify(mockToast).show()
     }
 
@@ -1247,7 +1275,7 @@
         onClickListenerCaptor.value.onClick(view)
 
         verify(mockDesktopTasksController)
-            .toggleDesktopTaskSize(decor.mTaskInfo)
+            .toggleDesktopTaskSize(decor.mTaskInfo, ResizeTrigger.MAXIMIZE_BUTTON, null)
     }
 
     private fun createOpenTaskDecoration(
@@ -1337,7 +1365,7 @@
         whenever(
             mockDesktopModeWindowDecorFactory.create(
                 any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
-                any(), any(), any(), any(), any(), any(), any())
+                any(), any(), any(), any(), any(), any(), any(), any())
         ).thenReturn(decoration)
         decoration.mTaskInfo = task
         whenever(decoration.user).thenReturn(mockUserHandle)
@@ -1378,5 +1406,6 @@
         private const val TAG = "DesktopModeWindowDecorViewModelTests"
         private val STABLE_INSETS = Rect(0, 100, 0, 0)
         private val INITIAL_BOUNDS = Rect(0, 0, 100, 100)
+        private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 3208872..0afb6c1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -106,6 +106,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -210,6 +211,8 @@
     private MultiInstanceHelper mMockMultiInstanceHelper;
     @Mock
     private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository;
+    @Mock
+    private DesktopModeEventLogger mDesktopModeEventLogger;
     @Captor
     private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
     @Captor
@@ -1400,7 +1403,7 @@
                 mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new,
                 new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory,
                 maximizeMenuFactory, mMockHandleMenuFactory,
-                mMockMultiInstanceHelper, mMockCaptionHandleRepository);
+                mMockMultiInstanceHelper, mMockCaptionHandleRepository, mDesktopModeEventLogger);
         windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
                 mMockTouchEventListener, mMockTouchEventListener);
         windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index fb17ae9..cb7fade 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -83,6 +83,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.tests.R;
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer;
@@ -138,6 +139,8 @@
     private SurfaceSyncGroup mMockSurfaceSyncGroup;
     @Mock
     private SurfaceControl mMockTaskSurface;
+    @Mock
+    private DesktopModeEventLogger mDesktopModeEventLogger;
 
     private final List<SurfaceControl.Transaction> mMockSurfaceControlTransactions =
             new ArrayList<>();
@@ -1014,7 +1017,7 @@
                 new MockObjectSupplier<>(mMockSurfaceControlTransactions,
                         () -> mock(SurfaceControl.Transaction.class)),
                 () -> mMockWindowContainerTransaction, () -> mMockTaskSurface,
-                mMockSurfaceControlViewHostFactory);
+                mMockSurfaceControlViewHostFactory, mDesktopModeEventLogger);
     }
 
     private class MockObjectSupplier<T> implements Supplier<T> {
@@ -1054,11 +1057,12 @@
                 Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
                 Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
                 Supplier<SurfaceControl> surfaceControlSupplier,
-                SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
+                SurfaceControlViewHostFactory surfaceControlViewHostFactory,
+                DesktopModeEventLogger desktopModeEventLogger) {
             super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                     surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                     windowContainerTransactionSupplier, surfaceControlSupplier,
-                    surfaceControlViewHostFactory);
+                    surfaceControlViewHostFactory, desktopModeEventLogger);
         }
 
         @Override
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
index 2e3ee32..e3f8fbb 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
@@ -20,6 +20,8 @@
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
     android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
     android:paddingTop="@dimen/settingslib_switchbar_margin"
     android:paddingBottom="@dimen/settingslib_switchbar_margin"
     android:orientation="vertical">
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
index 3e0e184..255b2c9 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
@@ -20,6 +20,8 @@
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
     android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
     android:paddingTop="@dimen/settingslib_switchbar_margin"
     android:paddingBottom="@dimen/settingslib_switchbar_margin"
     android:orientation="vertical">
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml
new file mode 100644
index 0000000..94c6924
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:importantForAccessibility="no">
+
+    <com.android.settingslib.widget.MainSwitchBar
+        android:id="@+id/settingslib_main_switch_bar"
+        android:visibility="gone"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent" />
+
+</FrameLayout>
+
+
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
index 7c0eaea..bf34db9 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
@@ -18,7 +18,11 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_height="wrap_content"
-    android:layout_width="match_parent">
+    android:layout_width="match_parent"
+    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
 
     <TextView
         android:id="@+id/switch_text"
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml
index fa908a4..bef6e35 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml
@@ -18,10 +18,6 @@
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
-    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
-    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
-    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
     android:importantForAccessibility="no">
 
     <com.android.settingslib.widget.MainSwitchBar
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index 3394874..83858d9 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -81,7 +81,11 @@
     }
 
     private void init(Context context, AttributeSet attrs) {
-        setLayoutResource(R.layout.settingslib_main_switch_layout);
+        boolean isExpressive = SettingsThemeHelper.isExpressiveTheme(context);
+        int resId = isExpressive
+                ? R.layout.settingslib_expressive_main_switch_layout
+                : R.layout.settingslib_main_switch_layout;
+        setLayoutResource(resId);
         mSwitchChangeListeners.add(this);
         if (attrs != null) {
             final TypedArray a = context.obtainStyledAttributes(attrs,
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 8ae11ab..811b47d 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -43,7 +43,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.process.ProcessWrapper;
 import com.android.systemui.res.R;
-import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.util.NotificationChannels;
 
 import java.lang.reflect.InvocationTargetException;
@@ -454,13 +454,13 @@
     @Override
     public void onConfigurationChanged(@NonNull Configuration newConfig) {
         if (mServicesStarted) {
-            ConfigurationController configController = mSysUIComponent.getConfigurationController();
+            ConfigurationForwarder configForwarder = mSysUIComponent.getConfigurationForwarder();
             if (Trace.isEnabled()) {
                 Trace.traceBegin(
                         Trace.TRACE_TAG_APP,
-                        configController.getClass().getSimpleName() + ".onConfigurationChanged()");
+                        configForwarder.getClass().getSimpleName() + ".onConfigurationChanged()");
             }
-            configController.onConfigurationChanged(newConfig);
+            configForwarder.onConfigurationChanged(newConfig);
             Trace.endSection();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 3fe6669..17f1961 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -29,6 +29,7 @@
 import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.NotificationInsetsModule;
 import com.android.systemui.statusbar.QsFrameTranslateModule;
+import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -125,13 +126,20 @@
     BootCompleteCacheImpl provideBootCacheImpl();
 
     /**
-     * Creates a ContextComponentHelper.
+     * Creates a ConfigurationController.
      */
     @SysUISingleton
     @GlobalConfig
     ConfigurationController getConfigurationController();
 
     /**
+     * Creates a ConfigurationForwarder.
+     */
+    @SysUISingleton
+    @GlobalConfig
+    ConfigurationForwarder getConfigurationForwarder();
+
+    /**
      * Creates a ContextComponentHelper.
      */
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 21922ff..12718e8b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -17,6 +17,7 @@
 package com.android.systemui.doze;
 
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+import static android.hardware.biometrics.Flags.screenOffUnlockUdfps;
 
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_QUICK_PICKUP;
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS;
@@ -248,8 +249,8 @@
                         true /* touchscreen */,
                         false /* ignoresSetting */,
                         dozeParameters.longPressUsesProx(),
-                        false /* immediatelyReRegister */,
-                        true /* requiresAod */
+                        screenOffUnlockUdfps() /* immediatelyReRegister */,
+                        !screenOffUnlockUdfps() /* requiresAod */
                 ),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 6db91ac..4071b13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -221,7 +221,7 @@
                 { notificationScrimClippingParams.params.top },
                 // Only allow scrolling when we are fully expanded. That way, we don't intercept
                 // swipes in lockscreen (when somehow QS is receiving touches).
-                { scrollState.canScrollForward && viewModel.isQsFullyExpanded },
+                { (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing },
             )
         frame.addView(
             composeView,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index 71fa0ac..7b25939 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -77,25 +77,24 @@
     colors: TileColors,
     squishiness: () -> Float,
     accessibilityUiState: AccessibilityUiState? = null,
-    toggleClickSupported: Boolean = false,
     iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius),
-    onClick: () -> Unit = {},
-    onLongClick: () -> Unit = {},
+    toggleClick: (() -> Unit)? = null,
+    onLongClick: (() -> Unit)? = null,
 ) {
     Row(
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = tileHorizontalArrangement(),
     ) {
         // Icon
-        val longPressLabel = longPressLabel()
+        val longPressLabel = longPressLabel().takeIf { onLongClick != null }
         Box(
             modifier =
-                Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) {
+                Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) {
                     Modifier.clip(iconShape)
                         .verticalSquish(squishiness)
                         .background(colors.iconBackground, { 1f })
                         .combinedClickable(
-                            onClick = onClick,
+                            onClick = toggleClick!!,
                             onLongClick = onLongClick,
                             onLongClickLabel = longPressLabel,
                         )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index d2ec958..b581c8b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -202,14 +202,17 @@
         topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) },
     ) { innerPadding ->
         CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+            val scrollState = rememberScrollState()
+            LaunchedEffect(listState.dragInProgress) {
+                if (listState.dragInProgress) {
+                    scrollState.animateScrollTo(0)
+                }
+            }
+
             Column(
                 verticalArrangement =
                     spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
-                modifier =
-                    modifier
-                        .fillMaxSize()
-                        .verticalScroll(rememberScrollState())
-                        .padding(innerPadding),
+                modifier = modifier.fillMaxSize().verticalScroll(scrollState).padding(innerPadding),
             ) {
                 AnimatedContent(
                     targetState = listState.dragInProgress,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index 52d5261..5f28fe4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -160,19 +160,18 @@
                 )
             } else {
                 val iconShape = TileDefaults.animateIconShape(uiState.state)
+                val secondaryClick: (() -> Unit)? =
+                    { tile.onSecondaryClick() }.takeIf { uiState.handlesSecondaryClick }
+                val longClick: (() -> Unit)? =
+                    { tile.onLongClick(expandable) }.takeIf { uiState.handlesLongClick }
                 LargeTileContent(
                     label = uiState.label,
                     secondaryLabel = uiState.secondaryLabel,
                     icon = icon,
                     colors = colors,
                     iconShape = iconShape,
-                    toggleClickSupported = state.handlesSecondaryClick,
-                    onClick = {
-                        if (state.handlesSecondaryClick) {
-                            tile.onSecondaryClick()
-                        }
-                    },
-                    onLongClick = { tile.onLongClick(expandable) },
+                    toggleClick = secondaryClick,
+                    onLongClick = longClick,
                     accessibilityUiState = uiState.accessibilityUiState,
                     squishiness = squishiness,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
index aa42080..56675e4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
@@ -33,6 +33,7 @@
     val label: String,
     val secondaryLabel: String,
     val state: Int,
+    val handlesLongClick: Boolean,
     val handlesSecondaryClick: Boolean,
     val icon: Supplier<QSTile.Icon?>,
     val accessibilityUiState: AccessibilityUiState,
@@ -86,6 +87,7 @@
         label = label?.toString() ?: "",
         secondaryLabel = secondaryLabel?.toString() ?: "",
         state = if (disabledByPolicy) Tile.STATE_UNAVAILABLE else state,
+        handlesLongClick = handlesLongClick,
         handlesSecondaryClick = handlesSecondaryClick,
         icon = icon?.let { Supplier { icon } } ?: iconSupplier ?: Supplier { null },
         AccessibilityUiState(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.kt
new file mode 100644
index 0000000..111d335
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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.shade
+
+import javax.inject.Qualifier
+
+/**
+ * Qualifies classes that provide display-specific info for shade window components.
+ *
+ * The Shade window can be moved between displays with different characteristics (e.g., density,
+ * size). This annotation ensures that components within the shade window use the correct context
+ * and resources for the display they are currently on.
+ *
+ * Classes annotated with `@ShadeDisplayAware` (e.g., 'Context`, `Resources`, `LayoutInflater`,
+ * `ConfigurationController`) will be dynamically updated to reflect the current display's
+ * configuration. This ensures consistent rendering even when the shade window is moved to an
+ * external display.
+ */
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeDisplayAware
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
index 231a0b0..9fe4a54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
@@ -32,13 +32,13 @@
 interface NotificationActivityStarter {
 
     /** Called when the user clicks on the notification bubble icon. */
-    fun onNotificationBubbleIconClicked(entry: NotificationEntry?)
+    fun onNotificationBubbleIconClicked(entry: NotificationEntry)
 
     /** Called when the user clicks on the surface of a notification. */
-    fun onNotificationClicked(entry: NotificationEntry?, row: ExpandableNotificationRow?)
+    fun onNotificationClicked(entry: NotificationEntry, row: ExpandableNotificationRow)
 
     /** Called when the user clicks on a button in the notification guts which fires an intent. */
-    fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)
+    fun startNotificationGutsIntent(intent: Intent, appUid: Int, row: ExpandableNotificationRow)
 
     /**
      * Called when the user clicks "Manage" or "History" in the Shade. Prefer using
@@ -56,7 +56,7 @@
     fun startSettingsIntent(view: View, intentInfo: SettingsIntent)
 
     /** Called when the user succeed to drop notification to proper target view. */
-    fun onDragSuccess(entry: NotificationEntry?)
+    fun onDragSuccess(entry: NotificationEntry)
 
     val isCollapsingToShowActivityOverLockscreen: Boolean
         get() = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt
new file mode 100644
index 0000000..3fd46fc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.content.res.Configuration
+
+/**
+ * Used to forward a configuration change to other components.
+ *
+ * This is commonly used to propagate configs to [ConfigurationController]. Note that there could be
+ * different configuration forwarder, for example each display, window or group of classes (e.g.
+ * shade window classes).
+ */
+interface ConfigurationForwarder {
+    /** Should be called when a new configuration is received. */
+    fun onConfigurationChanged(newConfiguration: Configuration)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 93db2db..af98311 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -20,7 +20,6 @@
 import static android.service.notification.NotificationListenerService.REASON_CLICK;
 
 import static com.android.systemui.statusbar.phone.CentralSurfaces.getActivityOptions;
-import static com.android.systemui.util.kotlin.NullabilityKt.expectNotNull;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -231,8 +230,7 @@
      * @param entry notification that bubble icon was clicked
      */
     @Override
-    public void onNotificationBubbleIconClicked(NotificationEntry entry) {
-        expectNotNull(TAG, "entry", entry);
+    public void onNotificationBubbleIconClicked(@NonNull NotificationEntry entry) {
         Runnable action = () -> {
             mBubblesManagerOptional.ifPresent(bubblesManager ->
                     bubblesManager.onUserChangedBubble(entry, !entry.isBubble()));
@@ -258,9 +256,8 @@
      * @param row   row for that notification
      */
     @Override
-    public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) {
-        expectNotNull(TAG, "entry", entry);
-        expectNotNull(TAG, "row", row);
+    public void onNotificationClicked(@NonNull NotificationEntry entry,
+            @NonNull ExpandableNotificationRow row) {
         mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(),
                 mKeyguardStateController.isVisible(),
                 mNotificationShadeWindowController.getPanelExpanded());
@@ -442,8 +439,7 @@
      * @param entry notification entry that is dropped.
      */
     @Override
-    public void onDragSuccess(NotificationEntry entry) {
-        expectNotNull(TAG, "entry", entry);
+    public void onDragSuccess(@NonNull NotificationEntry entry) {
         // this method is not responsible for intent sending.
         // will focus follow operation only after drag-and-drop that notification.
         final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true);
@@ -534,10 +530,8 @@
     }
 
     @Override
-    public void startNotificationGutsIntent(final Intent intent, final int appUid,
-            ExpandableNotificationRow row) {
-        expectNotNull(TAG, "intent", intent);
-        expectNotNull(TAG, "row", row);
+    public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid,
+            @NonNull ExpandableNotificationRow row) {
         boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
index cec77c1..1bb4e8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
@@ -16,16 +16,15 @@
 
 import android.content.res.Configuration;
 
+import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 
 /**
  * Common listener for configuration or subsets of configuration changes (like density or
  * font scaling), providing easy static dependence on these events.
  */
-public interface ConfigurationController extends CallbackController<ConfigurationListener> {
-
-    /** Alert controller of a change in the configuration. */
-    void onConfigurationChanged(Configuration newConfiguration);
+public interface ConfigurationController extends CallbackController<ConfigurationListener>,
+        ConfigurationForwarder {
 
     /** Alert controller of a change in between light and dark themes. */
     void notifyThemeChanged();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index b81af86..c7bd5a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.connectivity.NetworkControllerImpl;
 import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
+import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.statusbar.policy.BatteryControllerLogger;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.BluetoothControllerImpl;
@@ -186,6 +187,13 @@
             DevicePostureControllerImpl devicePostureControllerImpl);
 
     /** */
+    @Binds
+    @SysUISingleton
+    @GlobalConfig
+    ConfigurationForwarder provideGlobalConfigurationForwarder(
+            @GlobalConfig ConfigurationController configurationController);
+
+    /** */
     @Provides
     @SysUISingleton
     @GlobalConfig
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index e89a31f..618722a 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -27,7 +27,10 @@
 import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
 import com.android.systemui.res.R
 import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureRecognizer
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 
 @Composable
 fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -48,7 +51,17 @@
                 ),
         )
     val recognizer = rememberBackGestureRecognizer(LocalContext.current.resources)
-    GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack)
+    val gestureUiState: Flow<GestureUiState> =
+        remember(recognizer) {
+            GestureFlowAdapter(recognizer).gestureStateAsFlow.map {
+                it.toGestureUiState(
+                    progressStartMark = "",
+                    progressEndMark = "",
+                    successAnimation = R.raw.trackpad_back_success,
+                )
+            }
+        }
+    GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack)
 }
 
 @Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index 7899f5b..11e1ff4 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.touchpad.tutorial.ui.composable
 
 import androidx.activity.compose.BackHandler
+import androidx.annotation.RawRes
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Box
@@ -31,23 +32,49 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInteropFilter
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.inputdevice.tutorial.ui.composable.ActionTutorialContent
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
+import kotlinx.coroutines.flow.Flow
 
-fun GestureState.toTutorialActionState(): TutorialActionState {
+sealed interface GestureUiState {
+    data object NotStarted : GestureUiState
+
+    data class Finished(@RawRes val successAnimation: Int) : GestureUiState
+
+    data class InProgress(
+        val progress: Float = 0f,
+        val progressStartMark: String = "",
+        val progressEndMark: String = "",
+    ) : GestureUiState
+}
+
+fun GestureState.toGestureUiState(
+    progressStartMark: String,
+    progressEndMark: String,
+    successAnimation: Int,
+): GestureUiState {
+    return when (this) {
+        GestureState.NotStarted -> NotStarted
+        is GestureState.InProgress ->
+            GestureUiState.InProgress(this.progress, progressStartMark, progressEndMark)
+        is GestureState.Finished -> GestureUiState.Finished(successAnimation)
+    }
+}
+
+fun GestureUiState.toTutorialActionState(): TutorialActionState {
     return when (this) {
         NotStarted -> TutorialActionState.NotStarted
         // progress is disabled for now as views are not ready to handle varying progress
-        is InProgress -> TutorialActionState.InProgress(0f)
-        Finished -> TutorialActionState.Finished
+        is GestureUiState.InProgress -> TutorialActionState.InProgress(progress = 0f)
+        is Finished -> TutorialActionState.Finished
     }
 }
 
@@ -55,15 +82,13 @@
 fun GestureTutorialScreen(
     screenConfig: TutorialScreenConfig,
     gestureRecognizer: GestureRecognizer,
+    gestureUiStateFlow: Flow<GestureUiState>,
     onDoneButtonClicked: () -> Unit,
     onBack: () -> Unit,
 ) {
     BackHandler(onBack = onBack)
-    var gestureState: GestureState by remember { mutableStateOf(NotStarted) }
     var easterEggTriggered by remember { mutableStateOf(false) }
-    LaunchedEffect(gestureRecognizer) {
-        gestureRecognizer.addGestureStateCallback { gestureState = it }
-    }
+    val gestureState by gestureUiStateFlow.collectAsStateWithLifecycle(NotStarted)
     val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered = true }
     val gestureHandler =
         remember(gestureRecognizer) { TouchpadGestureHandler(gestureRecognizer, easterEggMonitor) }
@@ -84,7 +109,7 @@
 @Composable
 private fun TouchpadGesturesHandlingBox(
     gestureHandler: TouchpadGestureHandler,
-    gestureState: GestureState,
+    gestureState: GestureUiState,
     easterEggTriggered: Boolean,
     resetEasterEggFlag: () -> Unit,
     modifier: Modifier = Modifier,
@@ -110,7 +135,7 @@
                 .pointerInteropFilter(
                     onTouchEvent = { event ->
                         // FINISHED is the final state so we don't need to process touches anymore
-                        if (gestureState == Finished) {
+                        if (gestureState is Finished) {
                             false
                         } else {
                             gestureHandler.onMotionEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index 3ddf760..05871ce 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -25,8 +25,11 @@
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
 import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
 import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
 import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 
 @Composable
 fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -47,7 +50,17 @@
                 ),
         )
     val recognizer = rememberHomeGestureRecognizer(LocalContext.current.resources)
-    GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack)
+    val gestureUiState: Flow<GestureUiState> =
+        remember(recognizer) {
+            GestureFlowAdapter(recognizer).gestureStateAsFlow.map {
+                it.toGestureUiState(
+                    progressStartMark = "",
+                    progressEndMark = "",
+                    successAnimation = R.raw.trackpad_home_success,
+                )
+            }
+        }
+    GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack)
 }
 
 @Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index 30a21bf..4fd1644 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -25,8 +25,11 @@
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
 import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
 import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
 import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 
 @Composable
 fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -47,7 +50,17 @@
                 ),
         )
     val recognizer = rememberRecentAppsGestureRecognizer(LocalContext.current.resources)
-    GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack)
+    val gestureUiState: Flow<GestureUiState> =
+        remember(recognizer) {
+            GestureFlowAdapter(recognizer).gestureStateAsFlow.map {
+                it.toGestureUiState(
+                    progressStartMark = "",
+                    progressEndMark = "",
+                    successAnimation = R.raw.trackpad_recent_apps_success,
+                )
+            }
+        }
+    GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack)
 }
 
 @Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
index 80f8003..024048c 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
@@ -33,6 +33,10 @@
         gestureStateChangedCallback = callback
     }
 
+    override fun clearGestureStateCallback() {
+        gestureStateChangedCallback = {}
+    }
+
     override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
         val gestureState = distanceTracker.processEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt
new file mode 100644
index 0000000..23e31b0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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.touchpad.tutorial.ui.gesture
+
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+class GestureFlowAdapter(gestureRecognizer: GestureRecognizer) {
+
+    val gestureStateAsFlow: Flow<GestureState> = conflatedCallbackFlow {
+        val callback: (GestureState) -> Unit = { trySend(it) }
+        gestureRecognizer.addGestureStateCallback(callback)
+        awaitClose { gestureRecognizer.clearGestureStateCallback() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
index d146268..68a2ef9 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
@@ -22,6 +22,8 @@
 /** Based on passed [MotionEvent]s recognizes different states of gesture and notifies callback. */
 interface GestureRecognizer : Consumer<MotionEvent> {
     fun addGestureStateCallback(callback: (GestureState) -> Unit)
+
+    fun clearGestureStateCallback()
 }
 
 fun isThreeFingerTouchpadSwipe(event: MotionEvent) = isNFingerTouchpadSwipe(event, fingerCount = 3)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
index 2b84a4c..b804b9a 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
@@ -29,6 +29,10 @@
         gestureStateChangedCallback = callback
     }
 
+    override fun clearGestureStateCallback() {
+        gestureStateChangedCallback = {}
+    }
+
     override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
         val gestureState = distanceTracker.processEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
index 69b7c5e..7d484ee 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
@@ -38,6 +38,10 @@
         gestureStateChangedCallback = callback
     }
 
+    override fun clearGestureStateCallback() {
+        gestureStateChangedCallback = {}
+    }
+
     override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
         val gestureState = distanceTracker.processEvent(event)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 3d1a0d0..96f4a60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -481,6 +481,25 @@
         verify(mAuthController).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat());
     }
 
+    @Test
+    @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS)
+    public void udfpsLongPress_triggeredWhenDoze() {
+        // GIVEN device is DOZE
+        when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
+
+        // WHEN udfps long-press is triggered
+        mTriggers.onSensor(DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS, 100, 100,
+                new float[]{0, 1, 2, 3, 4});
+
+        // THEN the pulse is NOT dropped
+        verify(mDozeLog, never()).tracePulseDropped(anyString(), any());
+
+        // WHEN the screen state is OFF
+        mTriggers.onScreenState(Display.STATE_OFF);
+
+        // THEN aod interrupt never be sent
+        verify(mAuthController, never()).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat());
+    }
 
     @Test
     public void udfpsLongPress_dozeState_notRegistered() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
index 0b5f8d5..723c0d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
@@ -74,22 +74,31 @@
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.wmshell.BubblesManager
 import java.util.Optional
-import junit.framework.Assert
 import kotlin.test.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runCurrent
+import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers
 import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 /** Tests for [NotificationGutsManager] with the scene container enabled. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
@@ -99,7 +108,7 @@
         NotificationChannel(
             TEST_CHANNEL_ID,
             TEST_CHANNEL_ID,
-            NotificationManager.IMPORTANCE_DEFAULT
+            NotificationManager.IMPORTANCE_DEFAULT,
         )
 
     private val kosmos = testKosmos()
@@ -146,7 +155,7 @@
         MockitoAnnotations.initMocks(this)
         allowTestableLooperAsMainThread()
         helper = NotificationTestHelper(mContext, mDependency)
-        Mockito.`when`(accessibilityManager.isTouchExplorationEnabled).thenReturn(false)
+        whenever(accessibilityManager.isTouchExplorationEnabled).thenReturn(false)
         windowRootViewVisibilityInteractor =
             WindowRootViewVisibilityInteractor(
                 testScope.backgroundScope,
@@ -185,12 +194,12 @@
                 deviceProvisionedController,
                 metricsLogger,
                 headsUpManager,
-                activityStarter
+                activityStarter,
             )
         gutsManager.setUpWithPresenter(
             presenter,
             notificationListContainer,
-            onSettingsClickListener
+            onSettingsClickListener,
         )
         gutsManager.setNotificationActivityStarter(notificationActivityStarter)
         gutsManager.start()
@@ -198,49 +207,31 @@
 
     @Test
     fun testOpenAndCloseGuts() {
-        val guts = Mockito.spy(NotificationGuts(mContext))
-        Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock
-            ->
+        val guts = spy(NotificationGuts(mContext))
+        whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock ->
             handler.post((invocation.arguments[0] as Runnable))
             null
         }
 
         // Test doesn't support animation since the guts view is not attached.
-        Mockito.doNothing()
-            .`when`(guts)
-            .openControls(
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.any(Runnable::class.java)
-            )
+        doNothing()
+            .whenever(guts)
+            .openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>())
         val realRow = createTestNotificationRow()
         val menuItem = createTestMenuItem(realRow)
-        val row = Mockito.spy(realRow)
-        Mockito.`when`(row!!.windowToken).thenReturn(Binder())
-        Mockito.`when`(row.guts).thenReturn(guts)
+        val row = spy(realRow)
+        whenever(row!!.windowToken).thenReturn(Binder())
+        whenever(row.guts).thenReturn(guts)
         Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
         assertEquals(View.INVISIBLE.toLong(), guts.visibility.toLong())
         executor.runAllReady()
-        verify(guts)
-            .openControls(
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.any(Runnable::class.java)
-            )
+        verify(guts).openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>())
         verify(headsUpManager).setGutsShown(realRow!!.entry, true)
         assertEquals(View.VISIBLE.toLong(), guts.visibility.toLong())
         gutsManager.closeAndSaveGuts(false, false, true, 0, 0, false)
         verify(guts)
-            .closeControls(
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean()
-            )
-        verify(row, Mockito.times(1)).setGutsView(ArgumentMatchers.any())
+            .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>())
+        verify(row, times(1)).setGutsView(any())
         executor.runAllReady()
         verify(headsUpManager).setGutsShown(realRow.entry, false)
     }
@@ -250,7 +241,7 @@
         // First, start out lockscreen or shade as not visible
         setIsLockscreenOrShadeVisible(false)
         testScope.testScheduler.runCurrent()
-        val guts = Mockito.mock(NotificationGuts::class.java)
+        val guts = mock<NotificationGuts>()
         gutsManager.exposedGuts = guts
 
         // WHEN the lockscreen or shade becomes visible
@@ -258,15 +249,9 @@
         testScope.testScheduler.runCurrent()
 
         // THEN the guts are not closed
-        verify(guts, Mockito.never()).removeCallbacks(ArgumentMatchers.any())
-        verify(guts, Mockito.never())
-            .closeControls(
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean()
-            )
+        verify(guts, never()).removeCallbacks(any())
+        verify(guts, never())
+            .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>())
     }
 
     @Test
@@ -274,7 +259,7 @@
         // First, start out lockscreen or shade as visible
         setIsLockscreenOrShadeVisible(true)
         testScope.testScheduler.runCurrent()
-        val guts = Mockito.mock(NotificationGuts::class.java)
+        val guts = mock<NotificationGuts>()
         gutsManager.exposedGuts = guts
 
         // WHEN the lockscreen or shade is no longer visible
@@ -282,14 +267,14 @@
         testScope.testScheduler.runCurrent()
 
         // THEN the guts are closed
-        verify(guts).removeCallbacks(ArgumentMatchers.any())
+        verify(guts).removeCallbacks(anyOrNull())
         verify(guts)
             .closeControls(
-                /* leavebehinds= */ ArgumentMatchers.eq(true),
-                /* controls= */ ArgumentMatchers.eq(true),
-                /* x= */ ArgumentMatchers.anyInt(),
-                /* y= */ ArgumentMatchers.anyInt(),
-                /* force= */ ArgumentMatchers.eq(true)
+                /* leavebehinds= */ eq(true),
+                /* controls= */ eq(true),
+                /* x= */ any<Int>(),
+                /* y= */ any<Int>(),
+                /* force= */ eq(true),
             )
     }
 
@@ -304,95 +289,68 @@
         testScope.testScheduler.runCurrent()
 
         // THEN the list container is reset
-        verify(notificationListContainer)
-            .resetExposedMenuView(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyBoolean())
+        verify(notificationListContainer).resetExposedMenuView(any<Boolean>(), any<Boolean>())
     }
 
     @Test
     fun testChangeDensityOrFontScale() {
-        val guts = Mockito.spy(NotificationGuts(mContext))
-        Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock
-            ->
+        val guts = spy(NotificationGuts(mContext))
+        whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock ->
             handler.post((invocation.arguments[0] as Runnable))
             null
         }
 
         // Test doesn't support animation since the guts view is not attached.
-        Mockito.doNothing()
-            .`when`(guts)
-            .openControls(
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.any(Runnable::class.java)
-            )
+        doNothing()
+            .whenever(guts)
+            .openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>())
         val realRow = createTestNotificationRow()
         val menuItem = createTestMenuItem(realRow)
-        val row = Mockito.spy(realRow)
-        Mockito.`when`(row!!.windowToken).thenReturn(Binder())
-        Mockito.`when`(row.guts).thenReturn(guts)
-        Mockito.doNothing().`when`(row).ensureGutsInflated()
+        val row = spy(realRow)
+        whenever(row!!.windowToken).thenReturn(Binder())
+        whenever(row.guts).thenReturn(guts)
+        doNothing().whenever(row).ensureGutsInflated()
         val realEntry = realRow!!.entry
-        val entry = Mockito.spy(realEntry)
-        Mockito.`when`(entry.row).thenReturn(row)
-        Mockito.`when`(entry.getGuts()).thenReturn(guts)
+        val entry = spy(realEntry)
+        whenever(entry.row).thenReturn(row)
+        whenever(entry.getGuts()).thenReturn(guts)
         Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
         executor.runAllReady()
-        verify(guts)
-            .openControls(
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.any(Runnable::class.java)
-            )
+        verify(guts).openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>())
 
         // called once by mGutsManager.bindGuts() in mGutsManager.openGuts()
-        verify(row).setGutsView(ArgumentMatchers.any())
+        verify(row).setGutsView(any())
         row.onDensityOrFontScaleChanged()
         gutsManager.onDensityOrFontScaleChanged(entry)
         executor.runAllReady()
         gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false)
         verify(guts)
-            .closeControls(
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean()
-            )
+            .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>())
 
         // called again by mGutsManager.bindGuts(), in mGutsManager.onDensityOrFontScaleChanged()
-        verify(row, Mockito.times(2)).setGutsView(ArgumentMatchers.any())
+        verify(row, times(2)).setGutsView(any())
     }
 
     @Test
     fun testAppOpsSettingsIntent_camera() {
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_CAMERA)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action)
     }
 
     @Test
     fun testAppOpsSettingsIntent_mic() {
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_RECORD_AUDIO)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action)
     }
 
     @Test
@@ -400,30 +358,22 @@
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_CAMERA)
         ops.add(AppOpsManager.OP_RECORD_AUDIO)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action)
     }
 
     @Test
     fun testAppOpsSettingsIntent_overlay() {
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION, captor.lastValue.action)
     }
 
     @Test
@@ -432,15 +382,11 @@
         ops.add(AppOpsManager.OP_CAMERA)
         ops.add(AppOpsManager.OP_RECORD_AUDIO)
         ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action)
     }
 
     @Test
@@ -448,15 +394,11 @@
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_CAMERA)
         ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action)
     }
 
     @Test
@@ -464,112 +406,108 @@
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_RECORD_AUDIO)
         ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action)
     }
 
     @Test
     @Throws(Exception::class)
     fun testInitializeNotificationInfoView_highPriority() {
-        val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
-        val row = Mockito.spy(helper.createRow())
+        val notificationInfoView = mock<NotificationInfo>()
+        val row = spy(helper.createRow())
         val entry = row.entry
         NotificationEntryHelper.modifyRanking(entry)
             .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
             .setImportance(NotificationManager.IMPORTANCE_HIGH)
             .build()
-        Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
-        Mockito.`when`(highPriorityProvider.isHighPriority(entry)).thenReturn(true)
+        whenever(row.getIsNonblockable()).thenReturn(false)
+        whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true)
         val statusBarNotification = entry.sbn
         gutsManager.initializeNotificationInfo(row, notificationInfoView)
         verify(notificationInfoView)
             .bindNotification(
-                ArgumentMatchers.any(PackageManager::class.java),
-                ArgumentMatchers.any(INotificationManager::class.java),
-                ArgumentMatchers.eq(onUserInteractionCallback),
-                ArgumentMatchers.eq(channelEditorDialogController),
-                ArgumentMatchers.eq(statusBarNotification.packageName),
-                ArgumentMatchers.any(NotificationChannel::class.java),
-                ArgumentMatchers.eq(entry),
-                ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
-                ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
-                ArgumentMatchers.any(UiEventLogger::class.java),
-                ArgumentMatchers.eq(true),
-                ArgumentMatchers.eq(false),
-                ArgumentMatchers.eq(true), /* wasShownHighPriority */
-                ArgumentMatchers.eq(assistantFeedbackController),
-                ArgumentMatchers.any(MetricsLogger::class.java)
+                any<PackageManager>(),
+                any<INotificationManager>(),
+                eq(onUserInteractionCallback),
+                eq(channelEditorDialogController),
+                eq(statusBarNotification.packageName),
+                any<NotificationChannel>(),
+                eq(entry),
+                any<NotificationInfo.OnSettingsClickListener>(),
+                any<NotificationInfo.OnAppSettingsClickListener>(),
+                any<UiEventLogger>(),
+                eq(true),
+                eq(false),
+                eq(true), /* wasShownHighPriority */
+                eq(assistantFeedbackController),
+                any<MetricsLogger>(),
             )
     }
 
     @Test
     @Throws(Exception::class)
     fun testInitializeNotificationInfoView_PassesAlongProvisionedState() {
-        val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
-        val row = Mockito.spy(helper.createRow())
+        val notificationInfoView = mock<NotificationInfo>()
+        val row = spy(helper.createRow())
         NotificationEntryHelper.modifyRanking(row.entry)
             .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
             .build()
-        Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
+        whenever(row.getIsNonblockable()).thenReturn(false)
         val statusBarNotification = row.entry.sbn
         val entry = row.entry
         gutsManager.initializeNotificationInfo(row, notificationInfoView)
         verify(notificationInfoView)
             .bindNotification(
-                ArgumentMatchers.any(PackageManager::class.java),
-                ArgumentMatchers.any(INotificationManager::class.java),
-                ArgumentMatchers.eq(onUserInteractionCallback),
-                ArgumentMatchers.eq(channelEditorDialogController),
-                ArgumentMatchers.eq(statusBarNotification.packageName),
-                ArgumentMatchers.any(NotificationChannel::class.java),
-                ArgumentMatchers.eq(entry),
-                ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
-                ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
-                ArgumentMatchers.any(UiEventLogger::class.java),
-                ArgumentMatchers.eq(true),
-                ArgumentMatchers.eq(false),
-                ArgumentMatchers.eq(false), /* wasShownHighPriority */
-                ArgumentMatchers.eq(assistantFeedbackController),
-                ArgumentMatchers.any(MetricsLogger::class.java)
+                any<PackageManager>(),
+                any<INotificationManager>(),
+                eq(onUserInteractionCallback),
+                eq(channelEditorDialogController),
+                eq(statusBarNotification.packageName),
+                any<NotificationChannel>(),
+                eq(entry),
+                any<NotificationInfo.OnSettingsClickListener>(),
+                any<NotificationInfo.OnAppSettingsClickListener>(),
+                any<UiEventLogger>(),
+                eq(true),
+                eq(false),
+                eq(false), /* wasShownHighPriority */
+                eq(assistantFeedbackController),
+                any<MetricsLogger>(),
             )
     }
 
     @Test
     @Throws(Exception::class)
     fun testInitializeNotificationInfoView_withInitialAction() {
-        val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
-        val row = Mockito.spy(helper.createRow())
+        val notificationInfoView = mock<NotificationInfo>()
+        val row = spy(helper.createRow())
         NotificationEntryHelper.modifyRanking(row.entry)
             .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
             .build()
-        Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
+        whenever(row.getIsNonblockable()).thenReturn(false)
         val statusBarNotification = row.entry.sbn
         val entry = row.entry
         gutsManager.initializeNotificationInfo(row, notificationInfoView)
         verify(notificationInfoView)
             .bindNotification(
-                ArgumentMatchers.any(PackageManager::class.java),
-                ArgumentMatchers.any(INotificationManager::class.java),
-                ArgumentMatchers.eq(onUserInteractionCallback),
-                ArgumentMatchers.eq(channelEditorDialogController),
-                ArgumentMatchers.eq(statusBarNotification.packageName),
-                ArgumentMatchers.any(NotificationChannel::class.java),
-                ArgumentMatchers.eq(entry),
-                ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
-                ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
-                ArgumentMatchers.any(UiEventLogger::class.java),
-                ArgumentMatchers.eq(true),
-                ArgumentMatchers.eq(false),
-                ArgumentMatchers.eq(false), /* wasShownHighPriority */
-                ArgumentMatchers.eq(assistantFeedbackController),
-                ArgumentMatchers.any(MetricsLogger::class.java)
+                any<PackageManager>(),
+                any<INotificationManager>(),
+                eq(onUserInteractionCallback),
+                eq(channelEditorDialogController),
+                eq(statusBarNotification.packageName),
+                any<NotificationChannel>(),
+                eq(entry),
+                any<NotificationInfo.OnSettingsClickListener>(),
+                any<NotificationInfo.OnAppSettingsClickListener>(),
+                any<UiEventLogger>(),
+                eq(true),
+                eq(false),
+                eq(false), /* wasShownHighPriority */
+                eq(assistantFeedbackController),
+                any<MetricsLogger>(),
             )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
index 6be13be..3219127 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
@@ -23,7 +23,7 @@
         listeners -= listener
     }
 
-    override fun onConfigurationChanged(newConfiguration: Configuration?) {
+    override fun onConfigurationChanged(newConfiguration: Configuration) {
         listeners.forEach { it.onConfigChanged(newConfiguration) }
     }
 
@@ -36,7 +36,7 @@
     }
 
     fun notifyConfigurationChanged() {
-        onConfigurationChanged(newConfiguration = null)
+        onConfigurationChanged(newConfiguration = Configuration())
     }
 
     fun notifyLayoutDirectionChanged(isRtl: Boolean) {
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index c6855b4..4ac567c 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -285,7 +285,11 @@
 
         val displayRect = getDisplayRect(wmHelper)
 
-        val endX = if (isLeft) displayRect.left else displayRect.right
+        val endX = if (isLeft) {
+            displayRect.left + SNAP_RESIZE_DRAG_INSET
+        } else {
+            displayRect.right - SNAP_RESIZE_DRAG_INSET
+        }
         val endY = displayRect.centerY() / 2
 
         // drag the window to snap resize
@@ -391,6 +395,7 @@
 
     private companion object {
         val TIMEOUT: Duration = Duration.ofSeconds(3)
+        const val SNAP_RESIZE_DRAG_INSET: Int = 5 // inset to avoid dragging to display edge
         const val CAPTION: String = "desktop_mode_caption"
         const val MAXIMIZE_BUTTON_VIEW: String = "maximize_button_view"
         const val MAXIMIZE_MENU: String = "maximize_menu"
diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
similarity index 96%
rename from tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
rename to tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
index 6f3deab..2692e12 100644
--- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
@@ -40,7 +40,6 @@
 import android.tools.traces.monitors.PerfettoTraceMonitor;
 import android.tools.traces.protolog.ProtoLogTrace;
 import android.tracing.perfetto.DataSource;
-import android.util.proto.ProtoInputStream;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -74,7 +73,7 @@
 @SuppressWarnings("ConstantConditions")
 @Presubmit
 @RunWith(JUnit4.class)
-public class PerfettoProtoLogImplTest {
+public class ProcessedPerfettoProtoLogImplTest {
     private static final String TEST_PROTOLOG_DATASOURCE_NAME = "test.android.protolog";
     private static final String MOCK_VIEWER_CONFIG_FILE = "my/mock/viewer/config/file.pb";
     private final File mTracingDirectory = InstrumentationRegistry.getInstrumentation()
@@ -100,7 +99,7 @@
 
     private static ProtoLogViewerConfigReader sReader;
 
-    public PerfettoProtoLogImplTest() throws IOException {
+    public ProcessedPerfettoProtoLogImplTest() throws IOException {
     }
 
     @BeforeClass
@@ -151,7 +150,8 @@
         ViewerConfigInputStreamProvider viewerConfigInputStreamProvider = Mockito.mock(
                 ViewerConfigInputStreamProvider.class);
         Mockito.when(viewerConfigInputStreamProvider.getInputStream())
-                .thenAnswer(it -> new ProtoInputStream(sViewerConfigBuilder.build().toByteArray()));
+                .thenAnswer(it -> new AutoClosableProtoInputStream(
+                        sViewerConfigBuilder.build().toByteArray()));
 
         sCacheUpdater = () -> {};
         sReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
@@ -165,21 +165,16 @@
                     throw new RuntimeException(
                             "Unexpected viewer config file path provided");
                 }
-                return new ProtoInputStream(sViewerConfigBuilder.build().toByteArray());
+                return new AutoClosableProtoInputStream(sViewerConfigBuilder.build().toByteArray());
             });
         };
         sProtoLogConfigurationService =
                 new ProtoLogConfigurationServiceImpl(dataSourceBuilder, tracer);
 
-        if (android.tracing.Flags.clientSideProtoLogging()) {
-            sProtoLog = new PerfettoProtoLogImpl(
-                    MOCK_VIEWER_CONFIG_FILE, sReader, () -> sCacheUpdater.run(),
-                    TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService);
-        } else {
-            sProtoLog = new PerfettoProtoLogImpl(
-                    viewerConfigInputStreamProvider, sReader, () -> sCacheUpdater.run(),
-                    TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService);
-        }
+        sProtoLog = new ProcessedPerfettoProtoLogImpl(
+                MOCK_VIEWER_CONFIG_FILE, viewerConfigInputStreamProvider, sReader,
+                () -> sCacheUpdater.run(), TestProtoLogGroup.values(), dataSourceBuilder,
+                sProtoLogConfigurationService);
 
         busyWaitForDataSourceRegistration(TEST_PROTOLOG_DATASOURCE_NAME);
     }
@@ -398,18 +393,17 @@
     }
 
     @Test
-    public void log_logcatEnabledNoMessage() {
+    public void log_logcatEnabledNoMessageThrows() {
         when(sReader.getViewerString(anyLong())).thenReturn(null);
         PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog);
         TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
-        implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
-                new Object[]{5});
-
-        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
-                LogLevel.INFO), eq("UNKNOWN MESSAGE args = (5)"));
-        verify(sReader).getViewerString(eq(1234L));
+        var assertion = assertThrows(RuntimeException.class, () ->
+                implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                    new Object[]{5}));
+        Truth.assertThat(assertion).hasMessageThat()
+                .contains("Failed to decode message for logcat");
     }
 
     @Test
@@ -539,16 +533,12 @@
         PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
                 .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME)
                 .build();
-        long before;
-        long after;
         try {
             traceMonitor.start();
-            before = SystemClock.elapsedRealtimeNanos();
             sProtoLog.log(
                     LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash,
                     0b01100100,
                     new Object[]{"test", 1, 0.1, true});
-            after = SystemClock.elapsedRealtimeNanos();
         } finally {
             traceMonitor.stop(mWriter);
         }
@@ -606,7 +596,8 @@
         Truth.assertThat(stacktrace).doesNotContain(DataSource.class.getSimpleName() + ".java");
         Truth.assertThat(stacktrace)
                 .doesNotContain(ProtoLogImpl.class.getSimpleName() + ".java");
-        Truth.assertThat(stacktrace).contains(PerfettoProtoLogImplTest.class.getSimpleName());
+        Truth.assertThat(stacktrace)
+                .contains(ProcessedPerfettoProtoLogImplTest.class.getSimpleName());
         Truth.assertThat(stacktrace).contains("stackTraceTrimmed");
     }
 
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
index 8ecddaa..3d1e208 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
@@ -47,12 +47,12 @@
     }
 
     @Test
-    public void throwOnRegisteringDuplicateGroup() {
-        final var assertion = assertThrows(RuntimeException.class,
-                () -> ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2));
+    public void deduplicatesRegisteringDuplicateGroup() {
+        ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2);
 
-        Truth.assertThat(assertion).hasMessageThat().contains("" + TEST_GROUP_1.getId());
-        Truth.assertThat(assertion).hasMessageThat().contains("duplicate");
+        final var instance = ProtoLog.getSingleInstance();
+        Truth.assertThat(instance.getRegisteredGroups())
+                .containsExactly(TEST_GROUP_1, TEST_GROUP_2);
     }
 
     @Test
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
index d78ced1..9e029a8 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
@@ -19,9 +19,12 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
+import android.os.Build;
 import android.platform.test.annotations.Presubmit;
-import android.util.proto.ProtoInputStream;
 
+import com.google.common.truth.Truth;
+
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,6 +32,8 @@
 
 import perfetto.protos.ProtologCommon;
 
+import java.io.File;
+
 @Presubmit
 @RunWith(JUnit4.class)
 public class ProtoLogViewerConfigReaderTest {
@@ -83,7 +88,7 @@
                 ).build().toByteArray();
 
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider =
-            () -> new ProtoInputStream(TEST_VIEWER_CONFIG);
+            () -> new AutoClosableProtoInputStream(TEST_VIEWER_CONFIG);
 
     private ProtoLogViewerConfigReader mConfig;
 
@@ -123,6 +128,31 @@
     }
 
     @Test
+    public void viewerConfigIsOnDevice() {
+        Assume.assumeFalse(Build.FINGERPRINT.contains("robolectric"));
+
+        final String[] viewerConfigPaths;
+        if (android.tracing.Flags.perfettoProtologTracing()) {
+            viewerConfigPaths = new String[] {
+                    "/system_ext/etc/wmshell.protolog.pb",
+                    "/system/etc/core.protolog.pb",
+            };
+        } else {
+            viewerConfigPaths = new String[] {
+                    "/system_ext/etc/wmshell.protolog.json.gz",
+                    "/system/etc/protolog.conf.json.gz",
+            };
+        }
+
+        for (final var viewerConfigPath : viewerConfigPaths) {
+            File f = new File(viewerConfigPath);
+
+            Truth.assertWithMessage(f.getAbsolutePath() + " exists").that(f.exists()).isTrue();
+        }
+
+    }
+
+    @Test
     public void loadUnloadAndReloadViewerConfig() {
         loadViewerConfig();
         unloadViewerConfig();