Snap for 10412050 from d3a79b43853023f8a922cf05eb64fc43835eb3bf to studio-hedgehog-release

Change-Id: I3d5753e115459d51778b3f72443db4eacf9262ae
diff --git a/adt-ui/src/main/java/com/android/tools/adtui/categorytable/CategoryTable.kt b/adt-ui/src/main/java/com/android/tools/adtui/categorytable/CategoryTable.kt
index bdb8a36..5a643a6 100644
--- a/adt-ui/src/main/java/com/android/tools/adtui/categorytable/CategoryTable.kt
+++ b/adt-ui/src/main/java/com/android/tools/adtui/categorytable/CategoryTable.kt
@@ -171,8 +171,8 @@
 
     addMouseListener(
       object : MouseAdapter() {
-        override fun mouseClicked(e: MouseEvent) {
-          mouseClickedOnRow(e)
+        override fun mousePressed(e: MouseEvent) {
+          mousePressedOnRow(e)
         }
       }
     )
@@ -229,7 +229,7 @@
 
   internal fun indexOf(key: RowKey<T>): Int = rowComponents.indexOfFirst { it.rowKey == key }
 
-  private fun mouseClickedOnRow(e: MouseEvent) {
+  private fun mousePressedOnRow(e: MouseEvent) {
     if (SwingUtilities.isLeftMouseButton(e)) {
       rowComponents
         .firstOrNull { it.isVisible && e.y < it.y + it.height }
diff --git a/adt-ui/src/main/java/com/android/tools/adtui/categorytable/Column.kt b/adt-ui/src/main/java/com/android/tools/adtui/categorytable/Column.kt
index 8d523a9..7a4b301 100644
--- a/adt-ui/src/main/java/com/android/tools/adtui/categorytable/Column.kt
+++ b/adt-ui/src/main/java/com/android/tools/adtui/categorytable/Column.kt
@@ -15,6 +15,7 @@
  */
 package com.android.tools.adtui.categorytable
 
+import com.android.tools.adtui.event.DelegateMouseEventHandler
 import com.intellij.ui.components.JBLabel
 import javax.swing.JComponent
 
@@ -44,6 +45,13 @@
   fun createUi(rowValue: T): U
 
   /**
+   * If the UI for this column has MouseListeners, this delegate should be installed as an
+   * additional MouseListener if the standard mouse behavior for the row is still desired (i.e.,
+   * selecting the row).
+   */
+  fun installMouseDelegate(component: U, mouseDelegate: DelegateMouseEventHandler) {}
+
+  /**
    * Updates the UI based on the current value. Updating the UI only in response to this method
    * helps to ensure that the overall state of the table remains in sync.
    */
@@ -78,6 +86,9 @@
 
   override fun createUi(rowValue: T) = JBLabel()
 
+  // KT-39603
+  override fun installMouseDelegate(component: JBLabel, mouseDelegate: DelegateMouseEventHandler) {}
+
   override fun updateValue(rowValue: T, component: JBLabel, value: String) {
     component.text = value
   }
diff --git a/adt-ui/src/main/java/com/android/tools/adtui/categorytable/RowComponent.kt b/adt-ui/src/main/java/com/android/tools/adtui/categorytable/RowComponent.kt
index 8beef9e..c0e0b34 100644
--- a/adt-ui/src/main/java/com/android/tools/adtui/categorytable/RowComponent.kt
+++ b/adt-ui/src/main/java/com/android/tools/adtui/categorytable/RowComponent.kt
@@ -15,6 +15,7 @@
  */
 package com.android.tools.adtui.categorytable
 
+import com.android.tools.adtui.event.DelegateMouseEventHandler
 import com.intellij.openapi.actionSystem.DataKey
 import com.intellij.openapi.actionSystem.DataProvider
 import com.intellij.ui.components.JBLabel
@@ -128,9 +129,11 @@
   initialValue: T,
   primaryKey: Any
 ) : RowComponent<T>(), DataProvider {
+  private val mouseDelegate = DelegateMouseEventHandler.delegateTo(this)
+
   /** The components of this row, in model order. */
   val componentList: List<ColumnComponent<T, *, *>> =
-    columns.map { ColumnComponent(it, initialValue) }
+    columns.map { ColumnComponent(it, initialValue, mouseDelegate) }
 
   init {
     layout = ValueRowLayout(header)
@@ -192,9 +195,11 @@
    */
   internal class ColumnComponent<T, C, U : JComponent>(
     val column: Column<T, C, U>,
-    initialValue: T
+    initialValue: T,
+    mouseDelegate: DelegateMouseEventHandler,
   ) {
-    val component = column.createUi(initialValue)
+    val component =
+      column.createUi(initialValue).also { column.installMouseDelegate(it, mouseDelegate) }
     fun updateValue(rowValue: T) {
       column.updateValue(rowValue, component, column.attribute.value(rowValue))
     }
diff --git a/adt-ui/src/main/java/com/android/tools/adtui/compose/IssueNotificationAction.kt b/adt-ui/src/main/java/com/android/tools/adtui/compose/IssueNotificationAction.kt
index e20c27d..a48fe5e 100644
--- a/adt-ui/src/main/java/com/android/tools/adtui/compose/IssueNotificationAction.kt
+++ b/adt-ui/src/main/java/com/android/tools/adtui/compose/IssueNotificationAction.kt
@@ -180,6 +180,10 @@
     return JBUI.insets(1)
   }
 
+  open fun shouldHide(status: ComposeStatus, dataContext: DataContext) : Boolean {
+    return status.icon == null && StringUtil.isEmpty(status.title)
+  }
+
   override fun createCustomComponent(presentation: Presentation, place: String): JComponent = IssueNotificationActionButton(this, presentation, place)
 
   override fun updateCustomComponent(component: JComponent, presentation: Presentation) {
@@ -193,7 +197,7 @@
     val presentation = e.presentation
     createStatusInfo(project, e.dataContext)?.let {
       presentation.apply {
-        if (it.icon == null && StringUtil.isEmpty(it.title)) {
+        if (shouldHide(it, e.dataContext)) {
           isEnabledAndVisible = false
           return@let
         }
diff --git a/adt-ui/src/main/java/com/android/tools/adtui/workbench/WorkBench.java b/adt-ui/src/main/java/com/android/tools/adtui/workbench/WorkBench.java
index 5939513..c657850 100644
--- a/adt-ui/src/main/java/com/android/tools/adtui/workbench/WorkBench.java
+++ b/adt-ui/src/main/java/com/android/tools/adtui/workbench/WorkBench.java
@@ -160,8 +160,8 @@
     addToolsToModel(minimizedWindows);
     if (!isDisposed) {
       myWorkBenchManager.register(this);
+      KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("focusOwner", myMyPropertyChangeListener);
     }
-    KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("focusOwner", myMyPropertyChangeListener);
   }
 
   @Override
diff --git a/android-common/src/com/android/tools/idea/flags/StudioFlags.java b/android-common/src/com/android/tools/idea/flags/StudioFlags.java
index e8854b3..eb49a90 100644
--- a/android-common/src/com/android/tools/idea/flags/StudioFlags.java
+++ b/android-common/src/com/android/tools/idea/flags/StudioFlags.java
@@ -1176,9 +1176,9 @@
   //region Compose
   private static final FlagGroup COMPOSE = new FlagGroup(FLAGS, "compose", "Compose");
 
-  public static final Flag<Boolean> COMPOSE_PREVIEW_LITE_MODE = Flag.create(
-    COMPOSE, "preview.compose.lite.mode", "Enable Compose Preview Lite Mode",
-    "If enabled, Preview Lite Mode will be enabled.",
+  public static final Flag<Boolean> COMPOSE_PREVIEW_ESSENTIALS_MODE = Flag.create(
+    COMPOSE, "preview.essentials.mode", "Enable Compose Preview Essentials Mode",
+    "If enabled, Compose Preview Essentials Mode will be enabled.",
     true);
 
   public static final Flag<Boolean> COMPOSE_PREVIEW_DOUBLE_RENDER = Flag.create(
diff --git a/android/integration/testSrc/com/android/tools/idea/LanguageHighlightingTest.kt b/android/integration/testSrc/com/android/tools/idea/LanguageHighlightingTest.kt
index 0cfb420..8f3f18a 100644
--- a/android/integration/testSrc/com/android/tools/idea/LanguageHighlightingTest.kt
+++ b/android/integration/testSrc/com/android/tools/idea/LanguageHighlightingTest.kt
@@ -32,7 +32,7 @@
   val system: AndroidSystem = AndroidSystem.standard()
 
   @Test
-  fun basicShowUsages() {
+  fun kotlinHighlighting() {
     val project = AndroidProject("tools/adt/idea/android/integration/testData/languagehighlighting")
 
     // Create a maven repo and set it up in the installation and environment
diff --git a/android/rendering/testSrc/com/android/tools/idea/rendering/RenderServiceTest.java b/android/rendering/testSrc/com/android/tools/idea/rendering/RenderServiceTest.java
index f5c4fd5..55da9b4 100644
--- a/android/rendering/testSrc/com/android/tools/idea/rendering/RenderServiceTest.java
+++ b/android/rendering/testSrc/com/android/tools/idea/rendering/RenderServiceTest.java
@@ -19,6 +19,7 @@
 import com.android.tools.rendering.RenderAsyncActionExecutor;
 import com.android.tools.rendering.RenderService;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
 import junit.framework.TestCase;
 
 import java.util.concurrent.CountDownLatch;
@@ -88,4 +89,30 @@
     assertEquals(renderActionCounter + 1, renderActionExecutor.getExecutedRenderActionCount());
     assertTrue(called.get());
   }
+
+  public void testRenderThreadCheck() throws Throwable {
+    assertFalse(RenderService.isRenderThread());
+    AtomicReference<Throwable> exceptionReference = new AtomicReference<>(null);
+    RenderService.getRenderAsyncActionExecutor().runAsyncAction(() -> {
+      assertTrue(RenderService.isRenderThread());
+      Thread thread = new Thread(() -> {
+        assertTrue(RenderService.isRenderThread());
+      });
+
+      thread.setUncaughtExceptionHandler((t, e) -> {
+          exceptionReference.set(e);
+      });
+      thread.start();
+      try {
+        thread.join();
+      }
+      catch (InterruptedException e) {
+        throw new RuntimeException(e);
+      }
+    }).get();
+
+    Throwable exception = exceptionReference.get();
+    if (exception != null) throw exception;
+    assertFalse(RenderService.isRenderThread());
+  }
 }
\ No newline at end of file
diff --git a/android/src/com/android/tools/idea/avdmanager/Category.java b/android/src/com/android/tools/idea/avdmanager/Category.java
index e7f006e..441879c 100644
--- a/android/src/com/android/tools/idea/avdmanager/Category.java
+++ b/android/src/com/android/tools/idea/avdmanager/Category.java
@@ -22,19 +22,17 @@
 import org.jetbrains.annotations.NotNull;
 
 enum Category {
-  PHONE("Phone", "Pixel Fold", definition ->
+  PHONE("Phone", "pixel_fold", definition ->
     !definition.getIsDeprecated() && definition.getTagId() == null && !hasTabletScreen(definition)),
 
-  TABLET("Tablet", "Pixel Tablet", definition ->
+  TABLET("Tablet", "pixel_tablet", definition ->
     !definition.getIsDeprecated() && definition.getTagId() == null && hasTabletScreen(definition)),
 
-  WEAR_OS("Wear OS", "Wear OS Square", definition -> !definition.getIsDeprecated() && HardwareConfigHelper.isWear(definition)),
-  DESKTOP("Desktop", "Medium Desktop", definition -> !definition.getIsDeprecated() && HardwareConfigHelper.isDesktop(definition)),
+  WEAR_OS("Wear OS", "wearos_square", definition -> !definition.getIsDeprecated() && HardwareConfigHelper.isWear(definition)),
+  DESKTOP("Desktop", "desktop_medium", definition -> !definition.getIsDeprecated() && HardwareConfigHelper.isDesktop(definition)),
+  TV("TV", "tv_1080p", definition -> !definition.getIsDeprecated() && (HardwareConfigHelper.isTv(definition) || hasTvScreen(definition))),
 
-  TV("TV", "Television (1080p)", definition ->
-    !definition.getIsDeprecated() && (HardwareConfigHelper.isTv(definition) || hasTvScreen(definition))),
-
-  AUTOMOTIVE("Automotive", "Automotive (1024p landscape)", definition ->
+  AUTOMOTIVE("Automotive", "automotive_1024p_landscape", definition ->
     !definition.getIsDeprecated() && HardwareConfigHelper.isAutomotive(definition)),
 
   LEGACY("Legacy", "Nexus S", Device::getIsDeprecated);
@@ -43,7 +41,7 @@
   private final String myName;
 
   @NotNull
-  private final String myDefaultDefinitionName;
+  private final String myDefaultDefinitionId;
 
   @NotNull
   private final Predicate<Device> myPredicate;
@@ -57,15 +55,15 @@
     return definition.getDefaultHardware().getScreen().getDiagonalLength() >= Device.MINIMUM_TV_SIZE;
   }
 
-  Category(@NotNull String name, @NotNull String defaultDefinitionName, @NotNull Predicate<Device> predicate) {
+  Category(@NotNull String name, @NotNull String defaultDefinitionId, @NotNull Predicate<Device> predicate) {
     myName = name;
-    myDefaultDefinitionName = defaultDefinitionName;
+    myDefaultDefinitionId = defaultDefinitionId;
     myPredicate = predicate;
   }
 
   @NotNull
-  final String getDefaultDefinitionName() {
-    return myDefaultDefinitionName;
+  final String getDefaultDefinitionId() {
+    return myDefaultDefinitionId;
   }
 
   @NotNull
diff --git a/android/src/com/android/tools/idea/avdmanager/DeviceDefinitionList.java b/android/src/com/android/tools/idea/avdmanager/DeviceDefinitionList.java
index 0fb940b..a1c88d8 100644
--- a/android/src/com/android/tools/idea/avdmanager/DeviceDefinitionList.java
+++ b/android/src/com/android/tools/idea/avdmanager/DeviceDefinitionList.java
@@ -17,6 +17,7 @@
 
 import com.android.sdklib.devices.Device;
 import com.android.tools.adtui.common.ColoredIconGenerator;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
@@ -97,10 +98,21 @@
   private final List<DeviceDefinitionSelectionListener> myListeners = new ArrayList<>();
   private final List<DeviceCategorySelectionListener> myCategoryListeners = new ArrayList<>();
   private Collection<Device> myDevices;
+
+  @NotNull
+  private final DeviceSupplier mySupplier;
+
   private Device myDefaultDevice;
 
-  public DeviceDefinitionList() {
+  @SuppressWarnings("unused")
+  DeviceDefinitionList() {
+    this(new DeviceSupplier());
+  }
+
+  @VisibleForTesting
+  DeviceDefinitionList(@NotNull DeviceSupplier supplier) {
     super(new BorderLayout());
+    mySupplier = supplier;
 
     refreshDeviceProfiles();
     setDefaultDevices();
@@ -143,10 +155,10 @@
 
   @NotNull
   private Device getDefaultDefinition(@NotNull Category category) {
-    var definition = category.getDefaultDefinitionName();
+    var id = category.getDefaultDefinitionId();
 
     return myCategoryToDefinitionMultimap.get(category).stream()
-      .filter(d -> d.getDisplayName().equals(definition))
+      .filter(definition -> definition.getId().equals(id))
       .findFirst()
       .orElseThrow();
   }
@@ -270,7 +282,7 @@
   }
 
   private void refreshDeviceProfiles() {
-    myDevices = new DeviceSupplier().get();
+    myDevices = mySupplier.get();
 
     myCategoryToDefinitionMultimap = myDevices.stream()
       .collect(Multimaps.toMultimap(Category::valueOfDefinition,
diff --git a/android/src/com/android/tools/idea/editors/liveedit/ui/LiveEditNotificationAction.kt b/android/src/com/android/tools/idea/editors/liveedit/ui/LiveEditNotificationAction.kt
index a32e36a..90d371b 100644
--- a/android/src/com/android/tools/idea/editors/liveedit/ui/LiveEditNotificationAction.kt
+++ b/android/src/com/android/tools/idea/editors/liveedit/ui/LiveEditNotificationAction.kt
@@ -16,6 +16,7 @@
 package com.android.tools.idea.editors.liveedit.ui
 
 import com.android.ddmlib.IDevice
+import com.android.tools.adtui.compose.ComposeStatus
 import com.android.tools.adtui.compose.InformationPopup
 import com.android.tools.adtui.compose.InformationPopupImpl
 import com.android.tools.adtui.compose.IssueNotificationAction
@@ -37,6 +38,7 @@
 import com.intellij.openapi.actionSystem.DataProvider
 import com.intellij.openapi.actionSystem.DefaultActionGroup
 import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
+import com.intellij.openapi.actionSystem.PlatformDataKeys
 import com.intellij.openapi.actionSystem.RightAlignedToolbarAction
 import com.intellij.openapi.actionSystem.Separator
 import com.intellij.openapi.actionSystem.ex.ActionUtil
@@ -83,7 +85,7 @@
   dataContext: DataContext,
 ): InformationPopup? {
   return getStatusInfo(project, dataContext).let { status ->
-    if (status == LiveEditStatus.Disabled) {
+    if (shouldHideImpl(status, dataContext)) {
       return@let null
     }
 
@@ -181,11 +183,19 @@
     return JBUI.insets(2)
   }
 
+  override fun shouldHide(status: ComposeStatus, dataContext: DataContext): Boolean {
+    return shouldHideImpl(status, dataContext)
+  }
+
   override fun getActionUpdateThread(): ActionUpdateThread {
     return ActionUpdateThread.EDT
   }
 }
 
+private fun shouldHideImpl(status: ComposeStatus, dataContext: DataContext): Boolean {
+  return status == LiveEditStatus.Disabled && dataContext.getData(PlatformDataKeys.TOOL_WINDOW)?.id != RUNNING_DEVICES_TOOL_WINDOW_ID
+}
+
 /**
  * [DefaultActionGroup] that shows the notification chip and the [RedeployAction] button when applicable.
  */
diff --git a/android/src/com/android/tools/idea/rendering/StudioRenderSecurityManager.kt b/android/src/com/android/tools/idea/rendering/StudioRenderSecurityManager.kt
index a67d6b0..87f59cc 100644
--- a/android/src/com/android/tools/idea/rendering/StudioRenderSecurityManager.kt
+++ b/android/src/com/android/tools/idea/rendering/StudioRenderSecurityManager.kt
@@ -16,14 +16,21 @@
 package com.android.tools.idea.rendering
 
 import com.android.tools.adtui.webp.WebpNativeLibHelper
+import com.android.tools.rendering.RenderService
 import com.android.tools.rendering.security.RenderSecurityException
 import com.android.tools.rendering.security.RenderSecurityManager
+import com.android.tools.rendering.security.RenderSecurityManagerDefaults
 import java.io.File
 
 /** Studio-specific [RenderSecurityManager]. */
-class StudioRenderSecurityManager(
-  sdkPath: String?, projectPath: String?, restrictReads: Boolean
-) : RenderSecurityManager(sdkPath, projectPath, restrictReads) {
+class StudioRenderSecurityManager(sdkPath: String?, projectPath: String?, restrictReads: Boolean) :
+  RenderSecurityManager(
+    sdkPath,
+    projectPath,
+    restrictReads,
+    RenderSecurityManagerDefaults.getDefaultAllowedPaths(),
+    RenderService::isRenderThread
+  ) {
   override fun checkLink(lib: String) {
     // Allow linking with relative paths
     // Needed to for example load the "fontmanager" library from layout lib (from the
@@ -39,4 +46,4 @@
       throw RenderSecurityException.create("Link", lib)
     }
   }
-}
\ No newline at end of file
+}
diff --git a/android/src/com/android/tools/idea/run/deployment/liveedit/LiveEditStatus.kt b/android/src/com/android/tools/idea/run/deployment/liveedit/LiveEditStatus.kt
index aa286bd..867bb4f 100644
--- a/android/src/com/android/tools/idea/run/deployment/liveedit/LiveEditStatus.kt
+++ b/android/src/com/android/tools/idea/run/deployment/liveedit/LiveEditStatus.kt
@@ -123,7 +123,7 @@
     }
   }
 
-  object Disabled : LiveEditStatus(null, "", "", DISABLED)
+  object Disabled : LiveEditStatus(AllIcons.General.Warning, "Live Edit disabled", "Live Edit is disabled.", DISABLED)
 
   object UnrecoverableError :
     LiveEditStatus(
diff --git a/android/src/com/android/tools/idea/stats/AndroidStudioEventLogger.kt b/android/src/com/android/tools/idea/stats/AndroidStudioEventLogger.kt
index 13195fb..50509a3 100644
--- a/android/src/com/android/tools/idea/stats/AndroidStudioEventLogger.kt
+++ b/android/src/com/android/tools/idea/stats/AndroidStudioEventLogger.kt
@@ -18,7 +18,11 @@
 import com.android.tools.analytics.UsageTracker
 import com.android.tools.analytics.withProjectId
 import com.google.wireless.android.sdk.stats.AndroidStudioEvent
+import com.google.wireless.android.sdk.stats.AndroidStudioEvent.EventKind.DEBUGGER_EVENT
 import com.google.wireless.android.sdk.stats.DebuggerEvent
+import com.google.wireless.android.sdk.stats.DebuggerEvent.FramesViewUpdated
+import com.google.wireless.android.sdk.stats.DebuggerEvent.FramesViewUpdated.FileTypeInfo
+import com.google.wireless.android.sdk.stats.DebuggerEvent.Type.FRAMES_VIEW_UPDATED
 import com.google.wireless.android.sdk.stats.FileType
 import com.google.wireless.android.sdk.stats.FileUsage
 import com.google.wireless.android.sdk.stats.KotlinGradlePerformance
@@ -31,8 +35,10 @@
 import com.intellij.internal.statistic.eventLog.EventLogConfiguration
 import com.intellij.internal.statistic.eventLog.EventLogGroup
 import com.intellij.internal.statistic.eventLog.StatisticsEventLogger
+import com.intellij.internal.statistic.eventLog.events.EventFields
 import com.intellij.openapi.project.ProjectManager
 import com.intellij.openapi.project.getProjectCacheFileName
+import com.intellij.xdebugger.impl.XDebuggerActionsCollector
 import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics
 import org.jetbrains.kotlin.statistics.metrics.StringMetrics
 import java.util.Locale
@@ -52,6 +58,7 @@
       "run.configuration.exec" -> logRunConfigurationExec(eventId, data)
       "vfs" -> logVfsEvent(eventId, data)
       "debugger.breakpoints.usage" -> logDebuggerBreakpointsUsage(eventId, data)
+      "xdebugger.actions" -> logDebuggerEvent(eventId, data)
     }
     return CompletableFuture.completedFuture(null)
   }
@@ -243,6 +250,43 @@
   }
 
 
+  private fun logDebuggerEvent(eventId: String, data: Map<String, Any>) {
+    val event = when (eventId) {
+      XDebuggerActionsCollector.EVENT_FRAMES_UPDATED -> {
+        @Suppress("UnstableApiUsage")
+        val durationMs = data[EventFields.DurationMs.name] as? Long ?: return
+        val totalFrames = data[XDebuggerActionsCollector.TOTAL_FRAMES] as? Int ?: return
+
+        @Suppress("UNCHECKED_CAST")
+        val fileTypes = data[XDebuggerActionsCollector.FILE_TYPES] as? List<String> ?: return
+
+        @Suppress("UNCHECKED_CAST")
+        val framesPerFileType = data[XDebuggerActionsCollector.FRAMES_PER_TYPE] as? List<Int> ?: return
+
+        val fileTypeInfos = fileTypes.zip(framesPerFileType).map { (type, frames) ->
+          FileTypeInfo.newBuilder()
+            .setFileType(type)
+            .setNumFrames(frames)
+            .build()
+        }
+        DebuggerEvent.newBuilder()
+          .setType(FRAMES_VIEW_UPDATED)
+          .setFramesViewUpdated(
+            FramesViewUpdated.newBuilder()
+              .setDurationMs(durationMs)
+              .setTotalFrames(totalFrames)
+              .addAllFileTypeInfos(fileTypeInfos)
+          )
+      }
+
+      else -> return
+    }
+    UsageTracker.log(
+      AndroidStudioEvent.newBuilder()
+        .setKind(DEBUGGER_EVENT)
+        .setDebuggerEvent(event))
+  }
+
   /**
    * Adds the associated project from the IntelliJ anonymization project id to the builder
    */
diff --git a/android/src/org/jetbrains/android/uipreview/AndroidEditorSettings.java b/android/src/org/jetbrains/android/uipreview/AndroidEditorSettings.java
index 502cc02..3f66f31d 100644
--- a/android/src/org/jetbrains/android/uipreview/AndroidEditorSettings.java
+++ b/android/src/org/jetbrains/android/uipreview/AndroidEditorSettings.java
@@ -106,7 +106,7 @@
     private EditorMode myPreferredComposableEditorMode;
     private EditorMode myPreferredKotlinEditorMode;
     private double myMagnifySensitivity = DEFAULT_MAGNIFY_SENSITIVITY;
-    private boolean myComposePreviewLiteModeEnabled = false;
+    private boolean myComposePreviewEssentialsModeEnabled = false;
 
     public EditorMode getPreferredEditorMode() {
       return myPreferredEditorMode;
@@ -148,12 +148,12 @@
       myMagnifySensitivity = magnifySensitivity;
     }
 
-    public boolean isComposePreviewLiteModeEnabled() {
-      return myComposePreviewLiteModeEnabled;
+    public boolean isComposePreviewEssentialsModeEnabled() {
+      return myComposePreviewEssentialsModeEnabled;
     }
 
-    public void setComposePreviewLiteModeEnabled(boolean composePreviewLiteModeEnabled) {
-      myComposePreviewLiteModeEnabled = composePreviewLiteModeEnabled;
+    public void setComposePreviewEssentialsModeEnabled(boolean composePreviewEssentialsModeEnabled) {
+      myComposePreviewEssentialsModeEnabled = composePreviewEssentialsModeEnabled;
     }
   }
 }
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewBundle.properties b/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewBundle.properties
index ad9b681..38b7aa2 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewBundle.properties
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewBundle.properties
@@ -8,31 +8,31 @@
   <li><code>updateTransition</code></li>\
   <li><code>rememberInfiniteTransition</code></li>\
   </ul></html>
-action.animation.inspector.lite.mode.description=Animation Preview is not available in Compose Preview Essentials Mode
+action.animation.inspector.essentials.mode.description=Animation Preview is not available in Compose Preview Essentials Mode
 action.stop.animation.inspector.title=Stop Animation Preview
 action.stop.animation.inspector.description=Closes the Animation Preview
 action.run.title=Run Preview
 action.run.description=Run this preview in isolation on the emulator or running device
-action.run.lite.mode.description=Run Preview is not available in Compose Preview Essentials Mode
+action.run.essentials.mode.description=Run Preview is not available in Compose Preview Essentials Mode
 action.run.description.test.files=Running the preview is not supported in test files
 action.group.switch.title=Switch Preview Group
 action.interactive.title=Start Interactive Mode
 action.interactive.description=Live preview to test gestures
-action.interactive.lite.mode.description=Interactive Mode is not available in Compose Preview Essentials Mode
+action.interactive.essentials.mode.description=Interactive Mode is not available in Compose Preview Essentials Mode
 action.jump.to.definition=Jump to Definition
 action.stop.interactive.title=Stop Interactive Mode
 action.stop.interactive.description=Stops interactive mode
 action.scene.mode.colorblind.dropdown.title=Color Blind Modes
 action.scene.view.control.title=View Control
 action.scene.view.control.description=Control the views and layout of preview elements
-action.scene.view.control.lite.mode.description=Some actions are not available in Compose Preview Essentials Mode
+action.scene.view.control.essentials.mode.description=Some actions are not available in Compose Preview Essentials Mode
 action.scene.view.control.show.inspection.tooltip=Show Inspection Tooltips
 action.scene.view.control.start.filter.preview.mode=Filter Previews
 action.scene.view.control.no.matched.result=no result
 action.scene.view.control.one.matched.result=1 result
 action.scene.view.control.multiple.matched.results={0} results
 action.uicheck.description=Check this preview for common screen size and accessibility issues
-action.uicheck.lite.mode.description=UI Check is not available in Compose Preview Essentials Mode
+action.uicheck.essentials.mode.description=UI Check is not available in Compose Preview Essentials Mode
 action.uicheck.title=Start UI Check Mode
 action.stop.uicheck.description=Stops UI Check mode
 action.stop.uicheck.title=Stop UI Check
@@ -43,7 +43,7 @@
 action.preview.fast.refresh.enable.title=Enable Live Updates
 action.preview.fast.refresh.disable.title=Disable Live Updates
 action.preview.fast.refresh.toggle.description=Toggles the state of the Preview Live Edit refresh
-action.preview.fast.refresh.disabled.in.lite.mode.description=Preview Live Updates are disabled in Essentials Mode
+action.preview.fast.refresh.disabled.in.essentials.mode.description=Preview Live Updates are disabled in Essentials Mode
 action.open.issues.panel.title=Open Issues Panel
 action.view.problems=View Problems
 action.zoom.to.selection=Zoom to Selection
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewRepresentationProvider.kt b/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewRepresentationProvider.kt
index 643a270..a00431a 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewRepresentationProvider.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewRepresentationProvider.kt
@@ -30,7 +30,7 @@
 import com.android.tools.idea.compose.preview.actions.StopInteractivePreviewAction
 import com.android.tools.idea.compose.preview.actions.StopUiCheckPreviewAction
 import com.android.tools.idea.compose.preview.actions.visibleOnlyInComposeStaticPreview
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.editors.sourcecode.isKotlinFileType
 import com.android.tools.idea.flags.StudioFlags
 import com.android.tools.idea.preview.representation.CommonRepresentationEditorFileType
@@ -74,7 +74,7 @@
             layoutManagers = PREVIEW_LAYOUT_MANAGER_OPTIONS
           ) {
             !isAnyPreviewRefreshing(it.dataContext) &&
-              !ComposePreviewLiteModeManager.isLiteModeEnabled
+              !ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
           }
           .visibleOnlyInComposeStaticPreview(),
         StudioFlags.COMPOSE_DEBUG_BOUNDS.ifEnabled { ShowDebugBoundaries() },
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewTroubleInfoCollector.kt b/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewTroubleInfoCollector.kt
index ee69a59..ab8c56f 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewTroubleInfoCollector.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewTroubleInfoCollector.kt
@@ -15,12 +15,12 @@
  */
 package com.android.tools.idea.compose.preview
 
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.intellij.openapi.project.Project
 import com.intellij.troubleshooting.TroubleInfoCollector
 
 /** [TroubleInfoCollector] to collect information related to Compose Preview. */
 class ComposePreviewTroubleInfoCollector : TroubleInfoCollector {
   override fun collectInfo(project: Project) =
-    "Compose Preview Essentials Mode enabled: ${ComposePreviewLiteModeManager.isLiteModeEnabled}"
+    "Compose Preview Essentials Mode enabled: ${ComposePreviewEssentialsModeManager.isEssentialsModeEnabled}"
 }
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewViewImpl.kt b/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewViewImpl.kt
index 90a39ab..1af1003 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewViewImpl.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewViewImpl.kt
@@ -28,9 +28,9 @@
 import com.android.tools.idea.common.surface.DesignSurface
 import com.android.tools.idea.common.surface.GuiInputHandler
 import com.android.tools.idea.common.surface.handleLayoutlibNativeCrash
-import com.android.tools.idea.compose.preview.lite.ComposeEssentialsMode
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
-import com.android.tools.idea.compose.preview.lite.EssentialsModeWrapperPanel
+import com.android.tools.idea.compose.preview.essentials.ComposeEssentialsMode
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
+import com.android.tools.idea.compose.preview.essentials.EssentialsModeWrapperPanel
 import com.android.tools.idea.editors.build.ProjectBuildStatusManager
 import com.android.tools.idea.editors.build.ProjectStatus
 import com.android.tools.idea.editors.notifications.NotificationPanel
@@ -145,9 +145,9 @@
       (PreviewDisplaySettings, LayoutlibSceneManager) -> LayoutlibSceneManager
   ): List<ComposePreviewElement> {
     return mainSurface.updatePreviewsAndRefresh(
-      // Don't reuse models when in lite mode to avoid briefly showing an unexpected/mixed
+      // Don't reuse models when in essentials mode to avoid briefly showing an unexpected/mixed
       // state of the old and new preview.
-      tryReusingModels = !ComposePreviewLiteModeManager.isLiteModeEnabled,
+      tryReusingModels = !ComposePreviewEssentialsModeManager.isEssentialsModeEnabled,
       reinflate,
       previewElements,
       Logger.getInstance(ComposePreviewView::class.java),
@@ -365,7 +365,7 @@
     set(value) {
       // Avoid repeated values.
       if (value == field) return
-      // If lite mode is enabled,disabled or updated - components should be rearranged.
+      // If essentials mode is enabled,disabled or updated - components should be rearranged.
       // Remove components from its existing places.
       if (field == null) {
         content.remove(mainSurface)
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/Filtering.kt b/compose-designer/src/com/android/tools/idea/compose/preview/Filtering.kt
index 3c00934..d89d515 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/Filtering.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/Filtering.kt
@@ -60,7 +60,7 @@
 ) : PreviewElementProvider<ComposePreviewElementInstance> {
   /**
    * The Composable [ComposePreviewElementInstance] to filter. If no [ComposePreviewElementInstance]
-   * is defined by that intsance, then this filter will return all the available previews.
+   * is defined by that instance, then this filter will return all the available previews.
    */
   @Volatile var instance: ComposePreviewElementInstance? = null
 
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/Preview.kt b/compose-designer/src/com/android/tools/idea/compose/preview/Preview.kt
index 03f41db..6be7351 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/Preview.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/Preview.kt
@@ -31,10 +31,10 @@
 import com.android.tools.idea.compose.preview.PreviewGroup.Companion.ALL_PREVIEW_GROUP
 import com.android.tools.idea.compose.preview.animation.ComposePreviewAnimationManager
 import com.android.tools.idea.compose.preview.designinfo.hasDesignInfoProviders
+import com.android.tools.idea.compose.preview.essentials.ComposeEssentialsMode
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.fast.FastPreviewSurface
 import com.android.tools.idea.compose.preview.fast.requestFastPreviewRefreshAndTrack
-import com.android.tools.idea.compose.preview.lite.ComposeEssentialsMode
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
 import com.android.tools.idea.compose.preview.navigation.ComposePreviewNavigationHandler
 import com.android.tools.idea.compose.preview.scene.ComposeSceneComponentProvider
 import com.android.tools.idea.compose.preview.scene.ComposeScreenViewProvider
@@ -408,21 +408,21 @@
 
   /**
    * Updates the [composeWorkBench]'s [ComposeEssentialsMode] according to the state of Android
-   * Studio Essentials Mode or Compose Preview Lite Mode.
+   * Studio (and/or Compose Preview) Essentials Mode.
    *
    * @param sourceEventType type of the event that triggered the update
    */
   private fun updateEssentialsMode(
     sourceEventType: ComposePreviewLiteModeEvent.ComposePreviewLiteModeEventType? = null
   ) {
-    val liteModeIsEnabled = ComposePreviewLiteModeManager.isLiteModeEnabled
-    val composeEssentialsModeIsSet = composeWorkBench.essentialsMode != null
+    val essentialsModeIsEnabled = ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
+    val composePreviewViewEssentialsModeIsSet = composeWorkBench.essentialsMode != null
     // Only update essentials mode if needed
-    if (liteModeIsEnabled == composeEssentialsModeIsSet) return
+    if (essentialsModeIsEnabled == composePreviewViewEssentialsModeIsSet) return
 
-    if (composeEssentialsModeIsSet) {
+    if (composePreviewViewEssentialsModeIsSet) {
       composeWorkBench.essentialsMode = null
-      singlePreviewElementInstance = null // Remove filter applied by lite mode.
+      singlePreviewElementInstance = null // Remove filter applied by essentials mode.
     } else {
       composeWorkBench.essentialsMode = ComposeEssentialsMode(composeWorkBench.mainSurface)
     }
@@ -832,12 +832,12 @@
           }
 
           // If Fast Preview is enabled, prefetch the daemon for the current configuration.
-          // This should not happen when lite mode is enabled.
+          // This should not happen when essentials mode is enabled.
           if (
             module != null &&
               !module.isDisposed &&
               FastPreviewManager.getInstance(project).isEnabled &&
-              !ComposePreviewLiteModeManager.isLiteModeEnabled
+              !ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
           ) {
             FastPreviewManager.getInstance(project).preStartDaemon(module)
           }
@@ -1016,7 +1016,7 @@
               !EssentialsMode.isEnabled() &&
                 interactiveModeFlow.value.isStoppingOrDisabled() &&
                 !animationInspection.get() &&
-                !ComposePreviewLiteModeManager.isLiteModeEnabled
+                !ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
             )
               requestRefresh()
           }
@@ -1026,12 +1026,12 @@
 
   /**
    * Whether fast preview is available. In addition to checking its normal availability from
-   * [FastPreviewManager], we also verify that lite mode is not enabled, because fast preview should
-   * not be available in this case.
+   * [FastPreviewManager], we also verify that essentials mode is not enabled, because fast preview
+   * should not be available in this case.
    */
   private fun isFastPreviewAvailable() =
     FastPreviewManager.getInstance(project).isAvailable &&
-      !ComposePreviewLiteModeManager.isLiteModeEnabled
+      !ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
 
   override fun onActivate() {
     lifecycleManager.activate()
@@ -1195,9 +1195,10 @@
       )
     }
     // Some Composables (e.g. Popup) delay their content placement and wrap them into a coroutine
-    // controlled by the Compose clock. For that reason, we need to call executeCallbacksAsync()
-    // once, to make sure the queued behaviors are triggered and displayed in static preview.
-    surface.sceneManagers.forEach { it.executeCallbacksAsync() }
+    // controlled by the Compose clock. For that reason, we need to call
+    // executeCallbacksAndRequestRender() once, to make sure the queued behaviors are triggered
+    // and displayed in static preview.
+    surface.sceneManagers.forEach { it.executeCallbacksAndRequestRender(null) }
   }
 
   /**
@@ -1215,7 +1216,9 @@
           .setComposePreviewLiteModeEvent(
             ComposePreviewLiteModeEvent.newBuilder()
               .setType(eventType)
-              .setIsComposePreviewLiteMode(ComposePreviewLiteModeManager.isLiteModeEnabled)
+              .setIsComposePreviewLiteMode(
+                ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
+              )
           )
       )
     }
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/actions/AnimationInspectorAction.kt b/compose-designer/src/com/android/tools/idea/compose/preview/actions/AnimationInspectorAction.kt
index cb1fe97..b79680d 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/actions/AnimationInspectorAction.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/actions/AnimationInspectorAction.kt
@@ -17,7 +17,7 @@
 
 import com.android.tools.idea.compose.preview.COMPOSE_PREVIEW_ELEMENT_INSTANCE
 import com.android.tools.idea.compose.preview.COMPOSE_PREVIEW_MANAGER
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.message
 import com.intellij.openapi.actionSystem.AnActionEvent
 import com.intellij.openapi.actionSystem.DataContext
@@ -43,13 +43,14 @@
   override fun updateButton(e: AnActionEvent) {
     super.updateButton(e)
     e.presentation.apply {
-      val isLiteModeEnabled = ComposePreviewLiteModeManager.isLiteModeEnabled
-      isEnabled = !isLiteModeEnabled
+      val isEssentialsModeEnabled = ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
+      isEnabled = !isEssentialsModeEnabled
       // Only display the animation inspector icon if there are animations to be inspected.
       isVisible = getPreviewElement()?.hasAnimations == true
-      text = if (isLiteModeEnabled) null else message("action.animation.inspector.title")
+      text = if (isEssentialsModeEnabled) null else message("action.animation.inspector.title")
       description =
-        if (isLiteModeEnabled) message("action.animation.inspector.lite.mode.description")
+        if (isEssentialsModeEnabled)
+          message("action.animation.inspector.essentials.mode.description")
         else message("action.animation.inspector.description")
     }
   }
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/actions/ComposeViewControlAction.kt b/compose-designer/src/com/android/tools/idea/compose/preview/actions/ComposeViewControlAction.kt
index 23602e8..bf37cf7 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/actions/ComposeViewControlAction.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/actions/ComposeViewControlAction.kt
@@ -21,9 +21,9 @@
 import com.android.tools.adtui.actions.ZoomOutAction
 import com.android.tools.idea.actions.DESIGN_SURFACE
 import com.android.tools.idea.compose.preview.analytics.PreviewCanvasTracker
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.isAnyPreviewRefreshing
 import com.android.tools.idea.compose.preview.isPreviewFilterEnabled
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
 import com.android.tools.idea.compose.preview.message
 import com.android.tools.idea.flags.StudioFlags
 import com.android.tools.idea.uibuilder.surface.LayoutManagerSwitcher
@@ -57,15 +57,18 @@
     e.presentation.isEnabled = !isAnyPreviewRefreshing(e.dataContext)
     e.presentation.isVisible = !isPreviewFilterEnabled(e.dataContext)
     e.presentation.description =
-      if (ComposePreviewLiteModeManager.isLiteModeEnabled)
-        message("action.scene.view.control.lite.mode.description")
+      if (ComposePreviewEssentialsModeManager.isEssentialsModeEnabled)
+        message("action.scene.view.control.essentials.mode.description")
       else message("action.scene.view.control.description")
   }
 
   @VisibleForTesting
   public override fun updateActions(context: DataContext): Boolean {
     removeAll()
-    if (StudioFlags.COMPOSE_VIEW_FILTER.get() && !ComposePreviewLiteModeManager.isLiteModeEnabled) {
+    if (
+      StudioFlags.COMPOSE_VIEW_FILTER.get() &&
+        !ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
+    ) {
       DESIGN_SURFACE.getData(context)?.let { surface ->
         add(ComposeShowFilterAction(surface))
         addSeparator()
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/actions/DeployToDeviceAction.kt b/compose-designer/src/com/android/tools/idea/compose/preview/actions/DeployToDeviceAction.kt
index 6228455..e0a9eb3 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/actions/DeployToDeviceAction.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/actions/DeployToDeviceAction.kt
@@ -18,7 +18,7 @@
 import com.android.tools.compose.COMPOSE_PREVIEW_ACTIVITY_FQN
 import com.android.tools.idea.compose.preview.COMPOSE_PREVIEW_ELEMENT_INSTANCE
 import com.android.tools.idea.compose.preview.ComposePreviewElement
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.message
 import com.android.tools.idea.compose.preview.previewProviderClassAndIndex
 import com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfiguration
@@ -60,14 +60,14 @@
     val isTestFile =
       previewElement()?.previewBodyPsi?.let { isTestFile(it.project, it.virtualFile) } ?: false
     e.presentation.apply {
-      val isLiteModeEnabled = ComposePreviewLiteModeManager.isLiteModeEnabled
-      isEnabled = !isTestFile && !isLiteModeEnabled
+      val isEssentialsModeEnabled = ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
+      isEnabled = !isTestFile && !isEssentialsModeEnabled
       isVisible = true
-      text = if (isLiteModeEnabled) null else message("action.run.title")
+      text = if (isEssentialsModeEnabled) null else message("action.run.title")
       description =
         if (isTestFile) message("action.run.description.test.files")
         else {
-          if (isLiteModeEnabled) message("action.run.lite.mode.description")
+          if (isEssentialsModeEnabled) message("action.run.essentials.mode.description")
           else message("action.run.description")
         }
     }
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/actions/EnableInteractiveAction.kt b/compose-designer/src/com/android/tools/idea/compose/preview/actions/EnableInteractiveAction.kt
index 7c29bfc..d9d5f23 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/actions/EnableInteractiveAction.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/actions/EnableInteractiveAction.kt
@@ -17,7 +17,7 @@
 
 import com.android.tools.idea.compose.preview.COMPOSE_PREVIEW_ELEMENT_INSTANCE
 import com.android.tools.idea.compose.preview.COMPOSE_PREVIEW_MANAGER
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.message
 import com.android.tools.idea.concurrency.AndroidCoroutineScope
 import com.intellij.openapi.actionSystem.AnActionEvent
@@ -41,12 +41,12 @@
 
   override fun updateButton(e: AnActionEvent) {
     super.updateButton(e)
-    val isLiteModeEnabled = ComposePreviewLiteModeManager.isLiteModeEnabled
+    val isEssentialsModeEnabled = ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
     e.presentation.isVisible = true
-    e.presentation.isEnabled = !isLiteModeEnabled
-    e.presentation.text = if (isLiteModeEnabled) null else message("action.interactive.title")
+    e.presentation.isEnabled = !isEssentialsModeEnabled
+    e.presentation.text = if (isEssentialsModeEnabled) null else message("action.interactive.title")
     e.presentation.description =
-      if (isLiteModeEnabled) message("action.interactive.lite.mode.description")
+      if (isEssentialsModeEnabled) message("action.interactive.essentials.mode.description")
       else message("action.interactive.description")
   }
 
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/actions/EnableUiCheckAction.kt b/compose-designer/src/com/android/tools/idea/compose/preview/actions/EnableUiCheckAction.kt
index 6a80d6f..d04677a 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/actions/EnableUiCheckAction.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/actions/EnableUiCheckAction.kt
@@ -18,7 +18,7 @@
 import com.android.tools.idea.common.error.IssuePanelService
 import com.android.tools.idea.compose.preview.COMPOSE_PREVIEW_ELEMENT_INSTANCE
 import com.android.tools.idea.compose.preview.COMPOSE_PREVIEW_MANAGER
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.message
 import com.android.tools.idea.flags.StudioFlags
 import com.intellij.openapi.actionSystem.ActionUpdateThread
@@ -37,12 +37,12 @@
   override fun updateButton(e: AnActionEvent) {
     super.updateButton(e)
     val isUiCheckModeEnabled = StudioFlags.NELE_COMPOSE_UI_CHECK_MODE.get()
-    val isLiteModeEnabled = ComposePreviewLiteModeManager.isLiteModeEnabled
+    val isEssentialsModeEnabled = ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
     e.presentation.isVisible = isUiCheckModeEnabled
-    e.presentation.isEnabled = isUiCheckModeEnabled && !isLiteModeEnabled
-    e.presentation.text = if (isLiteModeEnabled) null else message("action.uicheck.title")
+    e.presentation.isEnabled = isUiCheckModeEnabled && !isEssentialsModeEnabled
+    e.presentation.text = if (isEssentialsModeEnabled) null else message("action.uicheck.title")
     e.presentation.description =
-      if (isLiteModeEnabled) message("action.uicheck.lite.mode.description")
+      if (isEssentialsModeEnabled) message("action.uicheck.essentials.mode.description")
       else message("action.uicheck.description")
   }
 
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/actions/SwitchSurfaceLayoutManagerAction.kt b/compose-designer/src/com/android/tools/idea/compose/preview/actions/SwitchSurfaceLayoutManagerAction.kt
index 0d1dfcb..83c2f77 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/actions/SwitchSurfaceLayoutManagerAction.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/actions/SwitchSurfaceLayoutManagerAction.kt
@@ -18,7 +18,7 @@
 import com.android.tools.adtui.actions.DropDownAction
 import com.android.tools.idea.common.actions.ActionButtonWithToolTipDescription
 import com.android.tools.idea.common.surface.DesignSurface.SceneViewAlignment
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.message
 import com.android.tools.idea.uibuilder.surface.LayoutManagerSwitcher
 import com.android.tools.idea.uibuilder.surface.layout.SurfaceLayoutManager
@@ -72,8 +72,8 @@
     }
 
     override fun isSelected(e: AnActionEvent): Boolean {
-      if (ComposePreviewLiteModeManager.isLiteModeEnabled) {
-        // In Lite Mode, the standard layout manager actions should be disabled, as Gallery
+      if (ComposePreviewEssentialsModeManager.isEssentialsModeEnabled) {
+        // In Essentials Mode, the standard layout manager actions should be disabled, as Gallery
         // will be selected by default and a special action will be created for it.
         return false
       }
@@ -92,15 +92,16 @@
     // We will only add the actions and be visible if there are more than one option
     if (layoutManagers.size > 1) {
       layoutManagers.forEach { add(SetSurfaceLayoutManagerAction(it)) }
-      if (ComposePreviewLiteModeManager.isLiteModeEnabled) {
-        // When lite mode is enabled, we add another action to the list, so users have a visual
-        // indication that they're not in neither Grid nor List view. When/if Gallery becomes a
-        // mode in the future that's independent of Lite Mode, we need to refactor this code.
+      if (ComposePreviewEssentialsModeManager.isEssentialsModeEnabled) {
+        // When essentials mode is enabled, we add another action to the list, so users have a
+        // visual indication that they're not in neither Grid nor List view. When/if Gallery
+        // becomes a mode in the future that's independent of Essentials Mode, we need to refactor
+        // this code.
         val galleryAction =
           object : ToggleAction(message("gallery.mode.title")) {
             override fun getActionUpdateThread() = ActionUpdateThread.EDT
             override fun isSelected(e: AnActionEvent) =
-              ComposePreviewLiteModeManager.isLiteModeEnabled
+              ComposePreviewEssentialsModeManager.isEssentialsModeEnabled
             override fun setSelected(e: AnActionEvent, state: Boolean) {}
             override fun update(e: AnActionEvent) {
               super.update(e)
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/actions/ToggleFastPreviewAction.kt b/compose-designer/src/com/android/tools/idea/compose/preview/actions/ToggleFastPreviewAction.kt
index bd144c8..6410268 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/actions/ToggleFastPreviewAction.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/actions/ToggleFastPreviewAction.kt
@@ -15,9 +15,9 @@
  */
 package com.android.tools.idea.compose.preview.actions
 
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.fast.FastPreviewSurface
 import com.android.tools.idea.compose.preview.findComposePreviewManagersForContext
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
 import com.android.tools.idea.compose.preview.message
 import com.android.tools.idea.editors.fast.ManualDisabledReason
 import com.android.tools.idea.editors.fast.fastPreviewManager
@@ -52,9 +52,9 @@
       presentation.isEnabledAndVisible = false
       return
     }
-    if (ComposePreviewLiteModeManager.isLiteModeEnabled) {
+    if (ComposePreviewEssentialsModeManager.isEssentialsModeEnabled) {
       presentation.description =
-        message("action.preview.fast.refresh.disabled.in.lite.mode.description")
+        message("action.preview.fast.refresh.disabled.in.essentials.mode.description")
       presentation.isEnabled = false
     } else {
       presentation.description = message("action.preview.fast.refresh.toggle.description")
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/analytics/MultiPreviewUsageTracker.kt b/compose-designer/src/com/android/tools/idea/compose/preview/analytics/MultiPreviewUsageTracker.kt
index 5d3f3bc..24e6987 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/analytics/MultiPreviewUsageTracker.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/analytics/MultiPreviewUsageTracker.kt
@@ -17,7 +17,7 @@
 
 import com.android.tools.idea.common.analytics.DesignerUsageTrackerManager
 import com.android.tools.idea.common.analytics.setApplicationId
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.preview.PreviewNode
 import com.google.common.cache.Cache
 import com.google.common.cache.CacheBuilder
@@ -91,7 +91,7 @@
           .filter { !it.nodeInfo.isUseless() && !it.nodeInfo.isPreviewType() }
           .map { it.nodeInfo.build() }
       )
-      .setIsComposePreviewLiteMode(ComposePreviewLiteModeManager.isLiteModeEnabled)
+      .setIsComposePreviewLiteMode(ComposePreviewEssentialsModeManager.isEssentialsModeEnabled)
 
   fun build(): ComposeMultiPreviewEvent {
     return eventBuilder.build()
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/lite/ComposeEssentialsMode.kt b/compose-designer/src/com/android/tools/idea/compose/preview/essentials/ComposeEssentialsMode.kt
similarity index 97%
rename from compose-designer/src/com/android/tools/idea/compose/preview/lite/ComposeEssentialsMode.kt
rename to compose-designer/src/com/android/tools/idea/compose/preview/essentials/ComposeEssentialsMode.kt
index a698578..5ffeb33 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/lite/ComposeEssentialsMode.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/essentials/ComposeEssentialsMode.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tools.idea.compose.preview.lite
+package com.android.tools.idea.compose.preview.essentials
 
 import com.android.tools.idea.compose.preview.findComposePreviewManagersForContext
 import com.intellij.openapi.actionSystem.DataContext
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/lite/ComposePreviewLiteModeManager.kt b/compose-designer/src/com/android/tools/idea/compose/preview/essentials/ComposePreviewEssentialsModeManager.kt
similarity index 71%
rename from compose-designer/src/com/android/tools/idea/compose/preview/lite/ComposePreviewLiteModeManager.kt
rename to compose-designer/src/com/android/tools/idea/compose/preview/essentials/ComposePreviewEssentialsModeManager.kt
index 643a795..171cb53 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/lite/ComposePreviewLiteModeManager.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/essentials/ComposePreviewEssentialsModeManager.kt
@@ -13,21 +13,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tools.idea.compose.preview.lite
+package com.android.tools.idea.compose.preview.essentials
 
 import com.android.tools.idea.flags.StudioFlags
 import com.android.tools.idea.modes.essentials.EssentialsMode
 import org.jetbrains.android.uipreview.AndroidEditorSettings
 
 /**
- * Service to handle and query the state of Compose Preview Lite Mode. The state can be set via
- * settings.
+ * Service to handle and query the state of Compose Preview Essentials Mode. The state can be
+ * changed via settings panel.
  */
-object ComposePreviewLiteModeManager {
+object ComposePreviewEssentialsModeManager {
 
-  val isLiteModeEnabled: Boolean
+  val isEssentialsModeEnabled: Boolean
     get() =
-      StudioFlags.COMPOSE_PREVIEW_LITE_MODE.get() &&
-        (AndroidEditorSettings.getInstance().globalState.isComposePreviewLiteModeEnabled ||
+      StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.get() &&
+        (AndroidEditorSettings.getInstance().globalState.isComposePreviewEssentialsModeEnabled ||
           EssentialsMode.isEnabled())
 }
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/lite/GalleryTabs.kt b/compose-designer/src/com/android/tools/idea/compose/preview/essentials/GalleryTabs.kt
similarity index 99%
rename from compose-designer/src/com/android/tools/idea/compose/preview/lite/GalleryTabs.kt
rename to compose-designer/src/com/android/tools/idea/compose/preview/essentials/GalleryTabs.kt
index a1bb90a..603ae5b 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/lite/GalleryTabs.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/essentials/GalleryTabs.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tools.idea.compose.preview.lite
+package com.android.tools.idea.compose.preview.essentials
 
 import com.android.tools.adtui.util.ActionToolbarUtil
 import com.android.tools.idea.compose.preview.Colors
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/lite/PreviewElementKey.kt b/compose-designer/src/com/android/tools/idea/compose/preview/essentials/PreviewElementKey.kt
similarity index 93%
rename from compose-designer/src/com/android/tools/idea/compose/preview/lite/PreviewElementKey.kt
rename to compose-designer/src/com/android/tools/idea/compose/preview/essentials/PreviewElementKey.kt
index 9b792c7..af15cce 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/lite/PreviewElementKey.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/essentials/PreviewElementKey.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tools.idea.compose.preview.lite
+package com.android.tools.idea.compose.preview.essentials
 
 import com.android.tools.idea.compose.preview.ComposePreviewElementInstance
 
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunConfigurationProducer.kt b/compose-designer/src/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunConfigurationProducer.kt
index 1e08458..474bc32 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunConfigurationProducer.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunConfigurationProducer.kt
@@ -17,8 +17,8 @@
 
 import com.android.tools.compose.COMPOSE_PREVIEW_ACTIVITY_FQN
 import com.android.tools.compose.COMPOSE_PREVIEW_PARAMETER_ANNOTATION_FQN
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.isValidComposePreview
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
 import com.android.tools.idea.kotlin.fqNameMatches
 import com.android.tools.idea.kotlin.getClassName
 import com.android.tools.idea.projectsystem.getHolderModule
@@ -65,7 +65,7 @@
     context: ConfigurationContext,
     sourceElement: Ref<PsiElement>
   ): Boolean {
-    if (ComposePreviewLiteModeManager.isLiteModeEnabled) return false
+    if (ComposePreviewEssentialsModeManager.isEssentialsModeEnabled) return false
     val module = context.module ?: context.location?.module ?: return false
     configuration.setLaunchActivity(COMPOSE_PREVIEW_ACTIVITY_FQN, true)
     context.containingComposePreviewFunction()?.let { ktNamedFunction ->
@@ -104,7 +104,7 @@
     configuration: ComposePreviewRunConfiguration,
     context: ConfigurationContext
   ): Boolean {
-    if (ComposePreviewLiteModeManager.isLiteModeEnabled) return false
+    if (ComposePreviewEssentialsModeManager.isEssentialsModeEnabled) return false
     context.containingComposePreviewFunction()?.let {
       val createdFromContext = configuration.composableMethodFqn == it.composePreviewFunctionFqn()
       if (createdFromContext) {
diff --git a/compose-designer/src/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunLineMarkerContributor.kt b/compose-designer/src/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunLineMarkerContributor.kt
index afc19a8..1ef2c1b 100644
--- a/compose-designer/src/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunLineMarkerContributor.kt
+++ b/compose-designer/src/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunLineMarkerContributor.kt
@@ -15,8 +15,8 @@
  */
 package com.android.tools.idea.compose.preview.runconfiguration
 
+import com.android.tools.idea.compose.preview.essentials.ComposePreviewEssentialsModeManager
 import com.android.tools.idea.compose.preview.isValidComposePreview
-import com.android.tools.idea.compose.preview.lite.ComposePreviewLiteModeManager
 import com.android.tools.idea.compose.preview.message
 import com.intellij.execution.lineMarker.ExecutorAction
 import com.intellij.execution.lineMarker.RunLineMarkerContributor
@@ -38,7 +38,7 @@
 class ComposePreviewRunLineMarkerContributor : RunLineMarkerContributor() {
 
   override fun getInfo(element: PsiElement): Info? {
-    if (ComposePreviewLiteModeManager.isLiteModeEnabled) return null
+    if (ComposePreviewEssentialsModeManager.isEssentialsModeEnabled) return null
     // Marker should be in a single LeafPsiElement. We choose the identifier and return null for
     // other elements within the function.
     if (element !is LeafPsiElement) return null
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/gradle/preview/RenderErrorTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/gradle/preview/RenderErrorTest.kt
index 00b49dd..71588d7 100644
--- a/compose-designer/testSrc/com/android/tools/idea/compose/gradle/preview/RenderErrorTest.kt
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/gradle/preview/RenderErrorTest.kt
@@ -205,6 +205,7 @@
     assertThat(offsets.sorted(), `is`(listOf(1667, 1817)))
   }
 
+  @Ignore("b/289364358")
   @Test
   fun testVisualLintErrors() {
     val issueModel = VisualLintService.getInstance(project).issueModel
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/gradle/preview/TestComposePreview.kt b/compose-designer/testSrc/com/android/tools/idea/compose/gradle/preview/TestComposePreview.kt
index fdde159..c7e62ca 100644
--- a/compose-designer/testSrc/com/android/tools/idea/compose/gradle/preview/TestComposePreview.kt
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/gradle/preview/TestComposePreview.kt
@@ -21,7 +21,7 @@
 import com.android.tools.idea.compose.preview.ComposePreviewView
 import com.android.tools.idea.compose.preview.NopComposePreviewManager
 import com.android.tools.idea.compose.preview.createMainDesignSurfaceBuilder
-import com.android.tools.idea.compose.preview.lite.ComposeEssentialsMode
+import com.android.tools.idea.compose.preview.essentials.ComposeEssentialsMode
 import com.android.tools.idea.compose.preview.navigation.ComposePreviewNavigationHandler
 import com.android.tools.idea.compose.preview.scene.ComposeSceneComponentProvider
 import com.android.tools.idea.compose.preview.scene.ComposeScreenViewProvider
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/preview/ComposePreviewRepresentationTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/preview/ComposePreviewRepresentationTest.kt
index adf71c9..59af698 100644
--- a/compose-designer/testSrc/com/android/tools/idea/compose/preview/ComposePreviewRepresentationTest.kt
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/preview/ComposePreviewRepresentationTest.kt
@@ -21,7 +21,7 @@
 import com.android.tools.idea.common.surface.DesignSurfaceListener
 import com.android.tools.idea.compose.ComposeProjectRule
 import com.android.tools.idea.compose.pickers.preview.property.referenceDeviceIds
-import com.android.tools.idea.compose.preview.lite.ComposeEssentialsMode
+import com.android.tools.idea.compose.preview.essentials.ComposeEssentialsMode
 import com.android.tools.idea.compose.preview.navigation.ComposePreviewNavigationHandler
 import com.android.tools.idea.concurrency.AndroidDispatchers.workerThread
 import com.android.tools.idea.editors.build.ProjectStatus
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/preview/analytics/MultiPreviewUsageTrackerTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/preview/analytics/MultiPreviewUsageTrackerTest.kt
index 5fe1529..0805390 100644
--- a/compose-designer/testSrc/com/android/tools/idea/compose/preview/analytics/MultiPreviewUsageTrackerTest.kt
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/preview/analytics/MultiPreviewUsageTrackerTest.kt
@@ -327,8 +327,8 @@
   }
 
   @Test
-  fun testLogEvent_LiteMode() {
-    StudioFlags.COMPOSE_PREVIEW_LITE_MODE.override(true)
+  fun testLogEvent_EssentialsMode() {
+    StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.override(true)
     fun logAndGetMultiPreviewEvent() =
       MultiPreviewUsageTracker.getInstance(null)
         .logEvent(MultiPreviewEvent(listOf(), ""))
@@ -336,13 +336,13 @@
 
     try {
       val settings = AndroidEditorSettings.getInstance().globalState
-      settings.isComposePreviewLiteModeEnabled = false
+      settings.isComposePreviewEssentialsModeEnabled = false
       assertFalse(logAndGetMultiPreviewEvent().isComposePreviewLiteMode)
 
-      settings.isComposePreviewLiteModeEnabled = true
+      settings.isComposePreviewEssentialsModeEnabled = true
       assertTrue(logAndGetMultiPreviewEvent().isComposePreviewLiteMode)
     } finally {
-      StudioFlags.COMPOSE_PREVIEW_LITE_MODE.clearOverride()
+      StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.clearOverride()
     }
   }
 
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/ComposeEssentialsModeTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/ComposeEssentialsModeTest.kt
similarity index 97%
rename from compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/ComposeEssentialsModeTest.kt
rename to compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/ComposeEssentialsModeTest.kt
index 17fd087..9c7e604 100644
--- a/compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/ComposeEssentialsModeTest.kt
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/ComposeEssentialsModeTest.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tools.idea.compose.preview.lite
+package com.android.tools.idea.compose.preview.essentials
 
 import com.android.tools.adtui.TreeWalker
 import com.android.tools.idea.compose.preview.COMPOSE_PREVIEW_MANAGER
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/ComposePreviewEssentialsModeManagerTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/ComposePreviewEssentialsModeManagerTest.kt
new file mode 100644
index 0000000..0850a0c
--- /dev/null
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/ComposePreviewEssentialsModeManagerTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.tools.idea.compose.preview.essentials
+
+import com.android.flags.junit.FlagRule
+import com.android.tools.idea.flags.StudioFlags
+import com.android.tools.idea.modes.essentials.EssentialsMode
+import com.android.tools.idea.testing.AndroidProjectRule
+import org.jetbrains.android.uipreview.AndroidEditorSettings
+import org.jetbrains.android.uipreview.AndroidEditorSettings.GlobalState
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class ComposePreviewEssentialsModeManagerTest {
+
+  @get:Rule val essentialsModeFlagRule = FlagRule(StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE)
+  @get:Rule val projectRule = AndroidProjectRule.inMemory()
+
+  private lateinit var settings: GlobalState
+
+  @Before
+  fun setUp() {
+    settings = AndroidEditorSettings.getInstance().globalState
+  }
+
+  @Test
+  fun essentialsModeIsOnlyEnabledIfFlagIsEnabled() {
+    StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.override(false)
+    assertFalse(ComposePreviewEssentialsModeManager.isEssentialsModeEnabled)
+
+    settings.isComposePreviewEssentialsModeEnabled = true
+    // Even with the essentials mode enabled in the settings panel, we shouldn't be in
+    // essentials mode if the flag is disabled.
+    assertFalse(ComposePreviewEssentialsModeManager.isEssentialsModeEnabled)
+  }
+
+  @Test
+  fun essentialsModeIsControlledViaSettingsIfFlagIsEnabled() {
+    StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.override(true)
+    settings.isComposePreviewEssentialsModeEnabled = false
+    assertFalse(ComposePreviewEssentialsModeManager.isEssentialsModeEnabled)
+
+    settings.isComposePreviewEssentialsModeEnabled = true
+    assertTrue(ComposePreviewEssentialsModeManager.isEssentialsModeEnabled)
+  }
+
+  @Test
+  fun previewEssentialsModeIsEnabledIfStudioEssentialsModeIsEnabled() {
+    StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.override(true)
+    try {
+      settings.isComposePreviewEssentialsModeEnabled = false
+      assertFalse(ComposePreviewEssentialsModeManager.isEssentialsModeEnabled)
+
+      // Enable Android Studio essentials mode. Note that preview essentials mode is still disabled
+      // in settings.
+      EssentialsMode.setEnabled(true, projectRule.project)
+      assertTrue(ComposePreviewEssentialsModeManager.isEssentialsModeEnabled)
+    } finally {
+      EssentialsMode.setEnabled(false, projectRule.project)
+    }
+  }
+}
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/GalleryTabsTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/GalleryTabsTest.kt
similarity index 99%
rename from compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/GalleryTabsTest.kt
rename to compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/GalleryTabsTest.kt
index baad209..9963452 100644
--- a/compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/GalleryTabsTest.kt
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/preview/essentials/GalleryTabsTest.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tools.idea.compose.preview.lite
+package com.android.tools.idea.compose.preview.essentials
 
 import com.android.tools.adtui.TreeWalker
 import com.android.tools.adtui.swing.FakeUi
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/ComposePreviewLiteModeManagerTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/ComposePreviewLiteModeManagerTest.kt
deleted file mode 100644
index a130f4b..0000000
--- a/compose-designer/testSrc/com/android/tools/idea/compose/preview/lite/ComposePreviewLiteModeManagerTest.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2023 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.tools.idea.compose.preview.lite
-
-import com.android.flags.junit.FlagRule
-import com.android.tools.idea.flags.StudioFlags
-import com.android.tools.idea.modes.essentials.EssentialsMode
-import com.android.tools.idea.testing.AndroidProjectRule
-import org.jetbrains.android.uipreview.AndroidEditorSettings
-import org.jetbrains.android.uipreview.AndroidEditorSettings.GlobalState
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-class ComposePreviewLiteModeManagerTest {
-
-  @get:Rule val liteModeFlagRule = FlagRule(StudioFlags.COMPOSE_PREVIEW_LITE_MODE)
-  @get:Rule val projectRule = AndroidProjectRule.inMemory()
-
-  private lateinit var settings: GlobalState
-
-  @Before
-  fun setUp() {
-    settings = AndroidEditorSettings.getInstance().globalState
-  }
-
-  @Test
-  fun liteModeIsOnlyEnabledIfFlagIsEnabled() {
-    StudioFlags.COMPOSE_PREVIEW_LITE_MODE.override(false)
-    assertFalse(ComposePreviewLiteModeManager.isLiteModeEnabled)
-
-    settings.isComposePreviewLiteModeEnabled = true
-    // Even with the lite mode enabled in the settings panel, we shouldn't be in
-    // lite mode if the flag is disabled.
-    assertFalse(ComposePreviewLiteModeManager.isLiteModeEnabled)
-  }
-
-  @Test
-  fun liteModeIsControlledViaSettingsIfFlagIsEnabled() {
-    StudioFlags.COMPOSE_PREVIEW_LITE_MODE.override(true)
-    settings.isComposePreviewLiteModeEnabled = false
-    assertFalse(ComposePreviewLiteModeManager.isLiteModeEnabled)
-
-    settings.isComposePreviewLiteModeEnabled = true
-    assertTrue(ComposePreviewLiteModeManager.isLiteModeEnabled)
-  }
-
-  @Test
-  fun liteModeIsEnabledIfEssentialsModeIsEnabled() {
-    StudioFlags.COMPOSE_PREVIEW_LITE_MODE.override(true)
-    try {
-      settings.isComposePreviewLiteModeEnabled = false
-      assertFalse(ComposePreviewLiteModeManager.isLiteModeEnabled)
-
-      // Enable Android Studio essentials mode. Note that preview lite mode is still disabled in
-      // settings.
-      EssentialsMode.setEnabled(true, projectRule.project)
-      assertTrue(ComposePreviewLiteModeManager.isLiteModeEnabled)
-    } finally {
-      EssentialsMode.setEnabled(false, projectRule.project)
-    }
-  }
-}
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunConfigurationProducerTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunConfigurationProducerTest.kt
index de90c828..ac0f302 100644
--- a/compose-designer/testSrc/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunConfigurationProducerTest.kt
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunConfigurationProducerTest.kt
@@ -60,13 +60,13 @@
           .trimIndent()
       )
     composableFunction = PsiTreeUtil.findChildrenOfType(file, KtNamedFunction::class.java).first()
-    StudioFlags.COMPOSE_PREVIEW_LITE_MODE.override(true)
+    StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.override(true)
   }
 
   override fun tearDown() {
     super.tearDown()
-    StudioFlags.COMPOSE_PREVIEW_LITE_MODE.clearOverride()
-    AndroidEditorSettings.getInstance().globalState.isComposePreviewLiteModeEnabled = false
+    StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.clearOverride()
+    AndroidEditorSettings.getInstance().globalState.isComposePreviewEssentialsModeEnabled = false
   }
 
   override fun configureAdditionalModules(
@@ -103,13 +103,13 @@
     }
   }
 
-  fun testSetupConfigurationFromContextWhenLiteModeIsEnabled() {
-    AndroidEditorSettings.getInstance().globalState.isComposePreviewLiteModeEnabled = true
+  fun testSetupConfigurationFromContextWhenEssentialsModeIsEnabled() {
+    AndroidEditorSettings.getInstance().globalState.isComposePreviewEssentialsModeEnabled = true
 
     val context = ConfigurationContext(composableFunction)
     val runConfiguration = newComposePreviewRunConfiguration()
     val producer = ComposePreviewRunConfigurationProducer()
-    // We shouldn't be able to create a configuration from context when lite mode is enabled.
+    // We shouldn't be able to create a configuration from context when essentials mode is enabled.
     assertFalse(
       producer.setupConfigurationFromContext(runConfiguration, context, Ref(context.psiLocation))
     )
@@ -296,15 +296,16 @@
     assertTrue(producer.isConfigurationFromContext(runConfiguration, context))
   }
 
-  fun testIsConfigurationFromContextWhenLiteModeIsEnabled() {
-    AndroidEditorSettings.getInstance().globalState.isComposePreviewLiteModeEnabled = true
+  fun testIsConfigurationFromContextWhenEssentialsModeIsEnabled() {
+    AndroidEditorSettings.getInstance().globalState.isComposePreviewEssentialsModeEnabled = true
     val producer = ComposePreviewRunConfigurationProducer()
     val context = ConfigurationContext(composableFunction)
     val runConfiguration = newComposePreviewRunConfiguration()
 
     runConfiguration.name = "Preview1"
     runConfiguration.composableMethodFqn = "TestKt.Preview1"
-    // Configuration shouldn't match when Lite Mode is enabled, even if both name and FQN match.
+    // Configuration shouldn't match when Compose Preview Essentials Mode is enabled,
+    // even if both name and FQN match.
     assertFalse(producer.isConfigurationFromContext(runConfiguration, context))
   }
 
diff --git a/compose-designer/testSrc/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunLineMarkerContributorTest.kt b/compose-designer/testSrc/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunLineMarkerContributorTest.kt
index be1d401..6d724e9 100644
--- a/compose-designer/testSrc/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunLineMarkerContributorTest.kt
+++ b/compose-designer/testSrc/com/android/tools/idea/compose/preview/runconfiguration/ComposePreviewRunLineMarkerContributorTest.kt
@@ -48,13 +48,13 @@
     super.setUp()
     myFixture.stubComposableAnnotation()
     myFixture.stubPreviewAnnotation()
-    StudioFlags.COMPOSE_PREVIEW_LITE_MODE.override(true)
+    StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.override(true)
   }
 
   override fun tearDown() {
     super.tearDown()
-    StudioFlags.COMPOSE_PREVIEW_LITE_MODE.clearOverride()
-    AndroidEditorSettings.getInstance().globalState.isComposePreviewLiteModeEnabled = false
+    StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.clearOverride()
+    AndroidEditorSettings.getInstance().globalState.isComposePreviewEssentialsModeEnabled = false
   }
 
   override fun configureAdditionalModules(
@@ -92,8 +92,8 @@
     assertNotNull(contributor.getInfo(functionIdentifier))
   }
 
-  fun testGetInfoWhenLiteModeIsEnabled() {
-    AndroidEditorSettings.getInstance().globalState.isComposePreviewLiteModeEnabled = true
+  fun testGetInfoWhenEssentialsModeIsEnabled() {
+    AndroidEditorSettings.getInstance().globalState.isComposePreviewEssentialsModeEnabled = true
     val file =
       myFixture.addFileToProjectAndInvalidate(
         "src/Test.kt",
@@ -112,7 +112,7 @@
 
     val functionIdentifier = file.findFunctionIdentifier("Preview1")
     // Although the function is a valid preview, a run line marker should not be created, because
-    // Lite Mode is enabled
+    // Essentials Mode is enabled
     assertNull(contributor.getInfo(functionIdentifier))
   }
 
diff --git a/designer/src/com/android/tools/idea/common/error/IssuePanelService.kt b/designer/src/com/android/tools/idea/common/error/IssuePanelService.kt
index c34322d..aac3202 100644
--- a/designer/src/com/android/tools/idea/common/error/IssuePanelService.kt
+++ b/designer/src/com/android/tools/idea/common/error/IssuePanelService.kt
@@ -183,7 +183,7 @@
           setSharedIssuePanelVisibility(false)
           return
         }
-        if (isComposeFile(newFile) || isSupportedDesignerFileType(newFile)) {
+        if (isSupportedDesignerFileType(newFile)) {
           addIssuePanel()
           return
         }
@@ -359,9 +359,6 @@
 
     fileToTabName[file]?.let { return it }
 
-    if (isComposeFile(file)) {
-      return "Compose"
-    }
     val name = getTabNameOfSupportedDesignerFile(file)
     if (name != null) {
       return name
@@ -378,7 +375,7 @@
    */
   private fun isDesignEditor(editor: FileEditor): Boolean {
     val virtualFile = editor.file ?: return false
-    return isComposeFile(virtualFile) || isSupportedDesignerFileType(virtualFile) || editor.getDesignSurface() != null
+    return isSupportedDesignerFileType(virtualFile) || editor.getDesignSurface() != null
   }
 
   private fun isComposeFile(file: VirtualFile): Boolean {
@@ -400,6 +397,7 @@
   private fun getTabNameOfSupportedDesignerFile(file: VirtualFile): String? {
     val psiFile = file.toPsiFile(project) ?: return null
     return when {
+      isComposeFile(file) -> "Compose"
       LayoutFileType.isResourceTypeOf(psiFile) -> "Layout and Qualifiers"
       PreferenceScreenFileType.isResourceTypeOf(psiFile) -> "Preference"
       MenuFileType.isResourceTypeOf(psiFile) -> "Menu"
diff --git a/designer/src/com/android/tools/idea/uibuilder/handlers/BuiltinViewHandlerProvider.kt b/designer/src/com/android/tools/idea/uibuilder/handlers/BuiltinViewHandlerProvider.kt
index 82e3400..0e07d88 100644
--- a/designer/src/com/android/tools/idea/uibuilder/handlers/BuiltinViewHandlerProvider.kt
+++ b/designer/src/com/android/tools/idea/uibuilder/handlers/BuiltinViewHandlerProvider.kt
@@ -227,6 +227,7 @@
           PreferenceTags.PREFERENCE_SCREEN -> PreferenceScreenHandler()
           PreferenceTags.RINGTONE_PREFERENCE -> RingtonePreferenceHandler()
           PreferenceTags.SWITCH_PREFERENCE -> SwitchPreferenceHandler()
+          PreferenceTags.SWITCH_PREFERENCE_COMPAT -> SwitchPreferenceHandler()
           else -> null
       }
 }
diff --git a/designer/src/com/android/tools/idea/uibuilder/handlers/motion/MotionLayoutComponentHelper.java b/designer/src/com/android/tools/idea/uibuilder/handlers/motion/MotionLayoutComponentHelper.java
index a622f499..1b6a0cf 100644
--- a/designer/src/com/android/tools/idea/uibuilder/handlers/motion/MotionLayoutComponentHelper.java
+++ b/designer/src/com/android/tools/idea/uibuilder/handlers/motion/MotionLayoutComponentHelper.java
@@ -30,6 +30,7 @@
 import com.android.tools.res.ids.ResourceIdManager;
 import com.android.utils.Pair;
 import com.intellij.util.ArrayUtil;
+import com.intellij.util.concurrency.EdtExecutorService;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Arrays;
@@ -158,6 +159,23 @@
     return -1;
   }
 
+  private void whenDesignToolAvailable(@NotNull Runnable runnable) {
+    if (myDesignTool != null) {
+      runnable.run();
+      return;
+    }
+
+    if (myFuture == null) {
+      return;
+    }
+
+    myFuture.thenRunAsync(() -> {
+      if (myDesignTool == null) {
+        runnable.run();
+      }
+    }, EdtExecutorService.getInstance());
+  }
+
   private boolean isMyDesignToolAvailable() {
     if (myDesignTool == null) {
       if (myFuture != null) {
@@ -397,9 +415,6 @@
   }
 
   private boolean setTransitionPosition(float position) {
-    if (!isMyDesignToolAvailable()) {
-      return false;
-    }
     if (myCallSetTransitionPosition == null) {
       try {
         myCallSetTransitionPosition = myDesignTool.getClass().getMethod("setToolPosition", float.class);
@@ -438,18 +453,17 @@
     return true;
   }
 
-  public boolean setProgress(float value) {
-    if (!isMyDesignToolAvailable()) {
-      return false;
-    }
-    NlModel model = myMotionLayoutComponent.getModel();
-    //model.notifyModified(NlModel.ChangeType.EDIT);
-    if (!setTransitionPosition(value)) {
-      return false;
-    }
-    model.notifyLiveUpdate(false);
-    refresh(myMotionLayoutComponent);
-    return true;
+  public void setProgress(float value) {
+    whenDesignToolAvailable(() -> {
+      NlModel model = myMotionLayoutComponent.getModel();
+
+      if (!setTransitionPosition(value)) {
+        return;
+      }
+
+      model.notifyLiveUpdate(false);
+      refresh(myMotionLayoutComponent);
+    });
   }
 
   public void setTransition(String start, String end) {
@@ -497,46 +511,45 @@
   }
 
   public void setState(String state) {
-    if (!isMyDesignToolAvailable()) {
-      return;
-    }
-    if (myCallSetState == null) {
-      try {
-        myCallSetState = myDesignTool.getClass().getMethod("setState", String.class);
-      }
-      catch (NoSuchMethodException e) {
-        if (DEBUG) {
-          e.printStackTrace();
+    whenDesignToolAvailable(() -> {
+      if (myCallSetState == null) {
+        try {
+          myCallSetState = myDesignTool.getClass().getMethod("setState", String.class);
         }
-        myCallSetState = null;
+        catch (NoSuchMethodException e) {
+          if (DEBUG) {
+            e.printStackTrace();
+          }
+          myCallSetState = null;
+          return;
+        }
+      }
+      if (myCallSetState != null) {
+        try {
+          RenderService.getRenderAsyncActionExecutor().runAsyncAction(() -> {
+            try {
+              myCallSetState.invoke(myDesignTool, state);
+            }
+            catch (ClassCastException | IllegalAccessException | InvocationTargetException e) {
+              myCallSetState = null;
+              if (DEBUG) {
+                e.printStackTrace();
+              }
+            }
+          });
+        }
+        catch (Exception e) {
+          if (DEBUG) {
+            e.printStackTrace();
+          }
+        }
+      }
+      if (myCallSetState == null) {
         return;
       }
-    }
-    if (myCallSetState != null) {
-      try {
-        RenderService.getRenderAsyncActionExecutor().runAsyncAction(() -> {
-          try {
-            myCallSetState.invoke(myDesignTool, state);
-          }
-          catch (ClassCastException | IllegalAccessException | InvocationTargetException e) {
-            myCallSetState = null;
-            if (DEBUG) {
-              e.printStackTrace();
-            }
-          }
-        });
-      }
-      catch (Exception e) {
-        if (DEBUG) {
-          e.printStackTrace();
-        }
-      }
-    }
-    if (myCallSetState == null) {
-      return;
-    }
-    NlModel model = myMotionLayoutComponent.getModel();
-    model.notifyLiveUpdate(false);
+      NlModel model = myMotionLayoutComponent.getModel();
+      model.notifyLiveUpdate(false);
+    });
   }
 
   public void disableAutoTransition(boolean disable) {
diff --git a/designer/src/com/android/tools/idea/uibuilder/handlers/preference/PreferenceCategoryHandler.java b/designer/src/com/android/tools/idea/uibuilder/handlers/preference/PreferenceCategoryHandler.java
index 37801e8..f0bc2e4 100644
--- a/designer/src/com/android/tools/idea/uibuilder/handlers/preference/PreferenceCategoryHandler.java
+++ b/designer/src/com/android/tools/idea/uibuilder/handlers/preference/PreferenceCategoryHandler.java
@@ -22,6 +22,7 @@
 import com.android.tools.idea.uibuilder.api.*;
 import com.android.tools.idea.common.model.NlComponent;
 import com.android.tools.idea.common.scene.SceneComponent;
+import com.google.common.base.Strings;
 import org.intellij.lang.annotations.Language;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -72,4 +73,11 @@
 
     return true;
   }
+
+  @NotNull
+  @Override
+  public String getTitle(@NotNull NlComponent component) {
+    String title = component.getAttribute(AUTO_URI, ATTR_TITLE);
+    return title != null ? title : Strings.nullToEmpty(component.getAndroidAttribute(ATTR_TITLE));
+  }
 }
diff --git a/designer/src/com/android/tools/idea/uibuilder/handlers/preference/PreferenceHandler.java b/designer/src/com/android/tools/idea/uibuilder/handlers/preference/PreferenceHandler.java
index 4c21b13..61113f6 100644
--- a/designer/src/com/android/tools/idea/uibuilder/handlers/preference/PreferenceHandler.java
+++ b/designer/src/com/android/tools/idea/uibuilder/handlers/preference/PreferenceHandler.java
@@ -80,6 +80,7 @@
   @NotNull
   @Override
   public String getTitle(@NotNull NlComponent component) {
-    return Strings.nullToEmpty(component.getAndroidAttribute(ATTR_TITLE));
+    String title = component.getAttribute(AUTO_URI, ATTR_TITLE);
+    return title != null ? title : Strings.nullToEmpty(component.getAndroidAttribute(ATTR_TITLE));
   }
 }
diff --git a/designer/src/com/android/tools/idea/uibuilder/handlers/preference/SwitchPreferenceHandler.java b/designer/src/com/android/tools/idea/uibuilder/handlers/preference/SwitchPreferenceHandler.java
index d3411ef..505eba0 100644
--- a/designer/src/com/android/tools/idea/uibuilder/handlers/preference/SwitchPreferenceHandler.java
+++ b/designer/src/com/android/tools/idea/uibuilder/handlers/preference/SwitchPreferenceHandler.java
@@ -29,7 +29,6 @@
 
 import static com.android.SdkConstants.ATTR_TITLE;
 import static com.android.SdkConstants.PreferenceAttributes.*;
-import static com.android.SdkConstants.PreferenceTags.SWITCH_PREFERENCE;
 
 public final class SwitchPreferenceHandler extends PreferenceHandler {
   @Language("XML")
@@ -80,7 +79,7 @@
 
     if (type == InsertType.CREATE) {
       NlWriteCommandActionUtil.run(newChild, "Set SwitchPreference", () -> {
-        newChild.setAndroidAttribute(ATTR_KEY, generateKey(newChild, SWITCH_PREFERENCE, "switch_preference_"));
+        newChild.setAndroidAttribute(ATTR_KEY, generateKey(newChild, newChild.getTagName(), "switch_preference_"));
       });
     }
     return true;
diff --git a/designer/src/com/android/tools/idea/uibuilder/options/NlOptionsConfigurable.kt b/designer/src/com/android/tools/idea/uibuilder/options/NlOptionsConfigurable.kt
index dc43c14..c7a3fcb 100644
--- a/designer/src/com/android/tools/idea/uibuilder/options/NlOptionsConfigurable.kt
+++ b/designer/src/com/android/tools/idea/uibuilder/options/NlOptionsConfigurable.kt
@@ -146,7 +146,7 @@
       }
 
       group("Compose Preview") {
-        if (StudioFlags.COMPOSE_PREVIEW_LITE_MODE.get()) {
+        if (StudioFlags.COMPOSE_PREVIEW_ESSENTIALS_MODE.get()) {
           buttonsGroup("Resource Usage:") {
             if (EssentialsMode.isEnabled()) {
               row {
@@ -161,8 +161,8 @@
             row {
               defaultModeRadioButton =
                 radioButton("Default")
-                  .bindSelected({ !state.isComposePreviewLiteModeEnabled }) {
-                    state.isComposePreviewLiteModeEnabled = !it
+                  .bindSelected({ !state.isComposePreviewEssentialsModeEnabled }) {
+                    state.isComposePreviewEssentialsModeEnabled = !it
                   }
             }.enabled(!EssentialsMode.isEnabled())
             indent {
@@ -175,13 +175,14 @@
               }
             }.enabled(!EssentialsMode.isEnabled())
             row {
-              val liteModeHint = "Preview will preserve resources by inflating previews on demand, and disabling live updates and " +
-                                 "preview modes. <a href=\"https://developer.android.com/jetpack/compose/tooling/previews\">Learn more</a>"
+              val essentialsModeHint =
+                "Preview will preserve resources by inflating previews on demand, and disabling live updates and preview modes. " +
+                "<a href=\"https://developer.android.com/jetpack/compose/tooling/previews\">Learn more</a>"
 
               radioButton("Essentials")
-                .comment(liteModeHint)
-                .bindSelected(state::isComposePreviewLiteModeEnabled) {
-                  state.isComposePreviewLiteModeEnabled = it
+                .comment(essentialsModeHint)
+                .bindSelected(state::isComposePreviewEssentialsModeEnabled) {
+                  state.isComposePreviewEssentialsModeEnabled = it
                 }
             }.enabled(!EssentialsMode.isEnabled())
           }
diff --git a/designer/testSrc/com/android/tools/idea/uibuilder/structure/NlTreeCellRendererTest.kt b/designer/testSrc/com/android/tools/idea/uibuilder/structure/NlTreeCellRendererTest.kt
index 1000216..dc19aef 100644
--- a/designer/testSrc/com/android/tools/idea/uibuilder/structure/NlTreeCellRendererTest.kt
+++ b/designer/testSrc/com/android/tools/idea/uibuilder/structure/NlTreeCellRendererTest.kt
@@ -125,4 +125,54 @@
     assertThat(labels[0].text).isEqualTo("SwitchPreferenceCompat")
     assertThat(labels[0].icon).isSameAs(StudioIcons.LayoutEditor.Palette.VIEW)
   }
+
+  @Test
+  fun displayPreferenceAndroidTitle() {
+    val badgeHandler = mock(NlTreeBadgeHandler::class.java)
+    val panel = mock(NlVisibilityGutterPanel::class.java)
+    val tree = NlComponentTree(projectRule.project, null, panel)
+    UIUtil.putClientProperty(tree, ExpandableItemsHandler.EXPANDED_RENDERER, true)
+    val button = MockNlComponent.create(
+      ApplicationManager.getApplication().runReadAction<XmlTag> {
+        XmlTagUtil.createTag(projectRule.project, "<SwitchPreferenceCompat android:title=\"Title\"></SwitchPreferenceCompat>")
+          .apply { putUserData(ModuleUtilCore.KEY_MODULE, projectRule.module) }
+      }
+    )
+    tree.setUI(object : BasicTreeUI() {
+      override fun getLeftChildIndent() = 0
+      override fun getRightChildIndent() = 0
+      override fun getPathForRow(tree: JTree?, row: Int) = TreePath(arrayOf(button))
+    })
+    val renderer = NlTreeCellRenderer(badgeHandler)
+    val component = renderer.getTreeCellRendererComponent(tree, button, selected = false, expanded = false, leaf = true, row = 3,
+                                                          hasFocus = false) as JComponent
+    val labels = UIUtil.findComponentsOfType(component, JLabel::class.java)
+    assertThat(labels[0].text).isEqualTo("Title")
+    assertThat(labels[0].icon).isSameAs(StudioIcons.LayoutEditor.Palette.VIEW)
+  }
+
+  @Test
+  fun displayPreferenceAppTitle() {
+    val badgeHandler = mock(NlTreeBadgeHandler::class.java)
+    val panel = mock(NlVisibilityGutterPanel::class.java)
+    val tree = NlComponentTree(projectRule.project, null, panel)
+    UIUtil.putClientProperty(tree, ExpandableItemsHandler.EXPANDED_RENDERER, true)
+    val button = MockNlComponent.create(
+      ApplicationManager.getApplication().runReadAction<XmlTag> {
+        XmlTagUtil.createTag(projectRule.project, "<SwitchPreferenceCompat app:title=\"Title\"></SwitchPreferenceCompat>")
+          .apply { putUserData(ModuleUtilCore.KEY_MODULE, projectRule.module) }
+      }
+    )
+    tree.setUI(object : BasicTreeUI() {
+      override fun getLeftChildIndent() = 0
+      override fun getRightChildIndent() = 0
+      override fun getPathForRow(tree: JTree?, row: Int) = TreePath(arrayOf(button))
+    })
+    val renderer = NlTreeCellRenderer(badgeHandler)
+    val component = renderer.getTreeCellRendererComponent(tree, button, selected = false, expanded = false, leaf = true, row = 3,
+                                                          hasFocus = false) as JComponent
+    val labels = UIUtil.findComponentsOfType(component, JLabel::class.java)
+    assertThat(labels[0].text).isEqualTo("Title")
+    assertThat(labels[0].icon).isSameAs(StudioIcons.LayoutEditor.Palette.VIEW)
+  }
 }
\ No newline at end of file
diff --git a/device-manager-v2/src/com/android/tools/idea/devicemanagerv2/DeviceManagerPanel.kt b/device-manager-v2/src/com/android/tools/idea/devicemanagerv2/DeviceManagerPanel.kt
index e185587..ed6c383 100644
--- a/device-manager-v2/src/com/android/tools/idea/devicemanagerv2/DeviceManagerPanel.kt
+++ b/device-manager-v2/src/com/android/tools/idea/devicemanagerv2/DeviceManagerPanel.kt
@@ -28,7 +28,7 @@
 import com.android.tools.adtui.actions.DropDownAction
 import com.android.tools.adtui.categorytable.CategoryTable
 import com.android.tools.adtui.categorytable.IconButton
-import com.android.tools.adtui.categorytable.RowKey
+import com.android.tools.adtui.categorytable.RowKey.ValueRowKey
 import com.android.tools.adtui.util.ActionToolbarUtil
 import com.android.tools.idea.concurrency.AndroidCoroutineScope
 import com.android.tools.idea.concurrency.AndroidDispatchers.uiThread
@@ -178,6 +178,17 @@
 
     panelScope.launch(uiDispatcher) { trackDevices() }
     panelScope.launch(uiDispatcher) { trackDeviceTemplates() }
+
+    // Keep the device details synced with the selected row.
+    panelScope.launch(uiDispatcher) {
+      deviceTable.selection.asFlow().collect { selectedRows ->
+        if (deviceDetailsPanelRow != null) {
+          (selectedRows.singleOrNull() as? ValueRowKey)?.let { selectedRow ->
+            deviceTable.values.find { it.key() == selectedRow.key }?.let { showDeviceDetails(it) }
+          }
+        }
+      }
+    }
   }
 
   private suspend fun trackDevices() {
@@ -228,8 +239,8 @@
                 if (deviceTable.addOrUpdateRow(it, beforeKey = handle.sourceTemplate)) {
                   handle.sourceTemplate?.let {
                     if (templateInstantiationCount.add(it, 1) == 0) {
-                      if (deviceTable.selection.selectedKeys().contains(RowKey.ValueRowKey(it))) {
-                        deviceTable.selection.selectRow(RowKey.ValueRowKey(handle))
+                      if (deviceTable.selection.selectedKeys().contains(ValueRowKey(it))) {
+                        deviceTable.selection.selectRow(ValueRowKey(handle))
                       }
                       deviceTable.setRowVisibleByKey(it, false)
                     }
diff --git a/device-manager-v2/src/com/android/tools/idea/devicemanagerv2/DeviceTableColumns.kt b/device-manager-v2/src/com/android/tools/idea/devicemanagerv2/DeviceTableColumns.kt
index 5911c9f..58602e6 100644
--- a/device-manager-v2/src/com/android/tools/idea/devicemanagerv2/DeviceTableColumns.kt
+++ b/device-manager-v2/src/com/android/tools/idea/devicemanagerv2/DeviceTableColumns.kt
@@ -22,6 +22,7 @@
 import com.android.tools.adtui.categorytable.Column
 import com.android.tools.adtui.categorytable.IconLabel
 import com.android.tools.adtui.categorytable.LabelColumn
+import com.android.tools.adtui.event.DelegateMouseEventHandler
 import com.google.common.collect.Ordering
 import com.intellij.openapi.project.Project
 import com.intellij.util.ui.JBDimension
@@ -136,6 +137,14 @@
       }
     }
 
+    override fun installMouseDelegate(
+      component: ActionButtonsPanel,
+      mouseDelegate: DelegateMouseEventHandler
+    ) {
+      // Install the mouse handler on each child of the panel
+      component.components.forEach { mouseDelegate.installListenerOn(it) }
+    }
+
     // TODO: Precomputing this is a hack... can we base it on the panel after it has been
     // constructed?
     override val widthConstraint =
diff --git a/device-manager-v2/testSrc/com/android/tools/idea/devicemanagerv2/DeviceManagerPanelTest.kt b/device-manager-v2/testSrc/com/android/tools/idea/devicemanagerv2/DeviceManagerPanelTest.kt
index e56f107..3589b61 100644
--- a/device-manager-v2/testSrc/com/android/tools/idea/devicemanagerv2/DeviceManagerPanelTest.kt
+++ b/device-manager-v2/testSrc/com/android/tools/idea/devicemanagerv2/DeviceManagerPanelTest.kt
@@ -21,16 +21,16 @@
 import com.android.sdklib.deviceprovisioner.DeviceState
 import com.android.sdklib.deviceprovisioner.DeviceTemplate
 import com.android.tools.adtui.categorytable.CategoryTable
-import com.android.tools.adtui.categorytable.RowKey
+import com.android.tools.adtui.categorytable.IconButton
+import com.android.tools.adtui.categorytable.RowKey.ValueRowKey
+import com.android.tools.adtui.swing.FakeUi
 import com.google.common.truth.Truth.assertThat
 import com.intellij.openapi.project.Project
 import com.intellij.testFramework.ProjectRule
 import icons.StudioIcons
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.consumeAsFlow
 import kotlinx.coroutines.flow.stateIn
@@ -60,7 +60,7 @@
     deviceHandles.send(listOf(pixel5Emulator, pixel6))
     deviceTemplates.send(listOf(pixel5Template))
 
-    deviceTable.selection.selectRow(RowKey.ValueRowKey(pixel5Template))
+    deviceTable.selection.selectRow(ValueRowKey(pixel5Template))
 
     assertThat(deviceTable.values).hasSize(3)
     val originalValues = deviceTable.values.map { it.key() }
@@ -74,7 +74,7 @@
       .inOrder()
 
     assertThat(deviceTable.selection.selectedKeys())
-      .containsExactly(RowKey.ValueRowKey<DeviceRowData>(pixel5Handle))
+      .containsExactly(ValueRowKey<DeviceRowData>(pixel5Handle))
   }
 
   @Test
@@ -108,6 +108,54 @@
     assertThat(deviceTable.visibleKeys()).containsExactly(pixel4, pixel5Template, pixel6)
   }
 
+  @Test
+  fun detailsTracksSelection() = runTestWithFixture {
+    val pixel4 = createHandle("Pixel 4")
+    val pixel5 = createHandle("Pixel 5")
+
+    deviceHandles.send(listOf(pixel4, pixel5))
+
+    val pixel4Row = DeviceRowData.create(pixel4, emptyList())
+    val pixel5Row = DeviceRowData.create(pixel5, emptyList())
+
+    ViewDetailsAction().actionPerformed(actionEvent(dataContext(panel, deviceRowData = pixel4Row)))
+
+    assertThat(panel.deviceDetailsPanelRow).isEqualTo(pixel4Row)
+
+    deviceTable.selection.selectRow(ValueRowKey(pixel5))
+
+    assertThat(panel.deviceDetailsPanelRow).isEqualTo(pixel5Row)
+
+    deviceTable.selection.clear()
+
+    // Shouldn't change anything
+    assertThat(panel.deviceDetailsPanelRow).isEqualTo(pixel5Row)
+  }
+
+  @Test
+  fun clickRow() = runTestWithFixture {
+    val pixel4 = createHandle("Pixel 4")
+    val pixel5 = createHandle("Pixel 5")
+
+    deviceHandles.send(listOf(pixel4, pixel5))
+
+    panel.setBounds(0, 0, 800, 400)
+
+    val fakeUi = FakeUi(panel, createFakeWindow = true)
+    fakeUi.layout()
+
+    assertThat(deviceTable.selection.selectedKeys()).isEmpty()
+
+    val runButton =
+      checkNotNull(fakeUi.findComponent<IconButton> { it.baseIcon == StudioIcons.Avd.RUN })
+    assertThat(runButton.isEnabled).isTrue()
+
+    fakeUi.clickOn(runButton)
+
+    assertThat(deviceTable.selection.selectedKeys())
+      .containsExactly(ValueRowKey<DeviceRowData>(pixel4))
+  }
+
   fun <T : Any> CategoryTable<T>.visibleKeys() =
     values.mapNotNull { primaryKey(it).takeIf { isRowVisibleByKey(it) } }
 
@@ -141,23 +189,14 @@
     val deviceTable = panel.deviceTable
 
     fun createHandle(name: String, sourceTemplate: DeviceTemplate? = null) =
-      FakeDeviceHandle(scope.createChildScope(isSupervisor = true), name, sourceTemplate)
-    fun createTemplate(name: String) = FakeDeviceTemplate(name)
-  }
-
-  private class FakeDeviceHandle(
-    override val scope: CoroutineScope,
-    val name: String,
-    override val sourceTemplate: DeviceTemplate?,
-  ) : DeviceHandle {
-    override val stateFlow =
-      MutableStateFlow<DeviceState>(
-        DeviceState.Disconnected(
-          DeviceProperties.build {
-            model = name
-            icon = StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_PHONE
-          }
-        )
+      FakeDeviceHandle(
+        scope.createChildScope(isSupervisor = true),
+        sourceTemplate,
+        DeviceProperties.build {
+          model = name
+          icon = StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_PHONE
+        }
       )
+    fun createTemplate(name: String) = FakeDeviceTemplate(name)
   }
 }
diff --git a/device-manager-v2/testSrc/com/android/tools/idea/devicemanagerv2/FakeDeviceHandle.kt b/device-manager-v2/testSrc/com/android/tools/idea/devicemanagerv2/FakeDeviceHandle.kt
index 75323e1..71ae0a4 100644
--- a/device-manager-v2/testSrc/com/android/tools/idea/devicemanagerv2/FakeDeviceHandle.kt
+++ b/device-manager-v2/testSrc/com/android/tools/idea/devicemanagerv2/FakeDeviceHandle.kt
@@ -20,6 +20,7 @@
 import com.android.sdklib.deviceprovisioner.DeviceHandle
 import com.android.sdklib.deviceprovisioner.DeviceProperties
 import com.android.sdklib.deviceprovisioner.DeviceState
+import com.android.sdklib.deviceprovisioner.DeviceTemplate
 import com.android.sdklib.deviceprovisioner.RepairDeviceAction
 import com.android.sdklib.deviceprovisioner.TestDefaultDeviceActionPresentation
 import icons.StudioIcons
@@ -29,13 +30,12 @@
 
 internal class FakeDeviceHandle(
   override val scope: CoroutineScope,
+  override val sourceTemplate: DeviceTemplate? = null,
+  initialProperties: DeviceProperties =
+    DeviceProperties.build { icon = StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_PHONE },
 ) : DeviceHandle {
   override val stateFlow =
-    MutableStateFlow<DeviceState>(
-      DeviceState.Disconnected(
-        DeviceProperties.build { icon = StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_PHONE }
-      )
-    )
+    MutableStateFlow<DeviceState>(DeviceState.Disconnected(initialProperties))
   override val activationAction = FakeActivationAction()
   override val deactivationAction = FakeDeactivationAction()
   override val repairDeviceAction = FakeRepairDeviceAction()
diff --git a/intellij.android.avdmanager.tests/src/com/android/tools/idea/avdmanager/Definitions.java b/intellij.android.avdmanager.tests/src/com/android/tools/idea/avdmanager/Definitions.java
index 1484104..b104633 100644
--- a/intellij.android.avdmanager.tests/src/com/android/tools/idea/avdmanager/Definitions.java
+++ b/intellij.android.avdmanager.tests/src/com/android/tools/idea/avdmanager/Definitions.java
@@ -28,54 +28,56 @@
 
   @NotNull
   static Device mockPhone() {
-    return mockDefinition(null, mockHardware(0));
+    return mockDefinition(null, mockHardware(0), "Pixel Fold", "pixel_fold");
   }
 
   @NotNull
   static Device mockTablet() {
-    return mockDefinition(null, mockHardware(10.055));
+    return mockDefinition(null, mockHardware(10.055), "Pixel Tablet", "pixel_tablet");
   }
 
   @NotNull
   static Device mockWearOsDefinition() {
-    return mockDefinition("android-wear", mockHardware(0));
+    return mockDefinition("android-wear", mockHardware(0), "Wear OS Square", "wearos_square");
   }
 
   @NotNull
   static Device mockDesktop() {
-    return mockDefinition("android-desktop", null);
+    return mockDefinition("android-desktop", null, "Medium Desktop", "desktop_medium");
   }
 
   @NotNull
   static Device mockTv() {
-    return mockDefinition("android-tv", mockHardware(0));
+    return mockDefinition("android-tv", mockHardware(0), "Television (1080p)", "tv_1080p");
   }
 
   @NotNull
   static Device mockAutomotiveDefinition() {
-    return mockDefinition("android-automotive", mockHardware(0));
+    return mockDefinition("android-automotive", mockHardware(0), "Automotive (1024p landscape)", "automotive_1024p_landscape");
   }
 
   @NotNull
   static Device mockLegacyDefinition() {
-    var definition = mockDefinition(null, null);
+    var definition = mockDefinition(null, null, "Nexus S", "Nexus S");
     Mockito.when(definition.getIsDeprecated()).thenReturn(true);
 
     return definition;
   }
 
   @NotNull
-  private static Device mockDefinition(@Nullable String id, @Nullable Hardware hardware) {
+  static Device mockDefinition(@Nullable String tagId, @Nullable Hardware hardware, @NotNull String name, @NotNull String id) {
     var definition = Mockito.mock(Device.class);
 
-    Mockito.when(definition.getTagId()).thenReturn(id);
+    Mockito.when(definition.getTagId()).thenReturn(tagId);
     Mockito.when(definition.getDefaultHardware()).thenReturn(hardware);
+    Mockito.when(definition.getDisplayName()).thenReturn(name);
+    Mockito.when(definition.getId()).thenReturn(id);
 
     return definition;
   }
 
   @NotNull
-  private static Hardware mockHardware(double length) {
+  static Hardware mockHardware(double length) {
     var screen = Mockito.mock(Screen.class);
     Mockito.when(screen.getDiagonalLength()).thenReturn(length);
 
diff --git a/intellij.android.avdmanager.tests/src/com/android/tools/idea/avdmanager/DeviceDefinitionListTest.java b/intellij.android.avdmanager.tests/src/com/android/tools/idea/avdmanager/DeviceDefinitionListTest.java
new file mode 100644
index 0000000..d7f35dd
--- /dev/null
+++ b/intellij.android.avdmanager.tests/src/com/android/tools/idea/avdmanager/DeviceDefinitionListTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.tools.idea.avdmanager;
+
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class DeviceDefinitionListTest {
+  @Test
+  public void deviceDefinitionList() {
+    // Arrange
+    var definitions = List.of(Definitions.mockPhone(),
+                              Definitions.mockTablet(),
+                              Definitions.mockWearOsDefinition(),
+                              Definitions.mockDesktop(),
+                              Definitions.mockDefinition("android-tv", Definitions.mockHardware(0), "Android TV (1080p)", "tv_1080p"),
+                              Definitions.mockAutomotiveDefinition(),
+                              Definitions.mockLegacyDefinition());
+
+    var supplier = Mockito.mock(DeviceSupplier.class);
+    Mockito.when(supplier.get()).thenReturn(definitions);
+
+    // Act
+    new DeviceDefinitionList(supplier);
+  }
+}
diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/FullTraceSummaryDetailsView.kt b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/FullTraceSummaryDetailsView.kt
index 593d4e5..7cc2786 100644
--- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/FullTraceSummaryDetailsView.kt
+++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/FullTraceSummaryDetailsView.kt
@@ -21,7 +21,8 @@
 import com.android.tools.profilers.StudioProfilersView
 import com.android.tools.profilers.StringFormattingUtils.formatLongValueWithCommas
 import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.POWER_RAIL_TOTAL_VALUE_IN_RANGE_TOOLTIP_MSG
-import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computeCumulativeEnergyUsageInRange
+import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computeCumulativeEnergyInRange
+import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computePowerUsageRange
 import com.android.tools.profilers.cpu.systemtrace.PowerRailTrackModel.Companion.POWER_RAIL_UNIT
 import com.google.common.annotations.VisibleForTesting
 import com.intellij.util.ui.JBUI
@@ -39,6 +40,9 @@
   @get: VisibleForTesting
   val energyUsedLabel = JLabel()
 
+  @get: VisibleForTesting
+  var powerRailTable: PowerRailTable? = null
+
   init {
     addRowToCommonSection("Time Range", timeRangeLabel)
     addRowToCommonSection("Duration", durationLabel)
@@ -47,8 +51,9 @@
     cpuCapture.systemTraceData?.powerRailCounters?.let { powerRailCounters ->
       if (powerRailCounters.isNotEmpty()) {
         addRowToCommonSectionWithInfoIcon("Total Energy Used in Range", energyUsedLabel, POWER_RAIL_TOTAL_VALUE_IN_RANGE_TOOLTIP_MSG)
-        addSection(
-          PowerRailTable(profilersView.studioProfilers, powerRailCounters, tabModel.selectionRange, tabModel.captureRange).component)
+        powerRailTable = PowerRailTable(profilersView.studioProfilers, powerRailCounters, tabModel.selectionRange,
+                                            tabModel.captureRange)
+        addSection(powerRailTable!!.component)
       }
     }
     // Add a collapsible Help Text section containing Navigation and Analysis instructions (initially collapsed)
@@ -70,7 +75,8 @@
     val cpuCapture = tabModel.dataSeries[0]
     val powerRailCounters = cpuCapture.systemTraceData?.powerRailCounters
     powerRailCounters?.forEach {
-      totalEnergyUws += computeCumulativeEnergyUsageInRange(it.value.cumulativeData, selectionRange)
+      val powerUsageRange = computePowerUsageRange(it.value.cumulativeData, selectionRange)
+      totalEnergyUws += computeCumulativeEnergyInRange(powerUsageRange)
     }
 
     energyUsedLabel.text = "${formatLongValueWithCommas(totalEnergyUws)} $POWER_RAIL_UNIT"
diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/PowerRailTable.kt b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/PowerRailTable.kt
index 462a1f4..e1d208f 100644
--- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/PowerRailTable.kt
+++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/PowerRailTable.kt
@@ -24,7 +24,9 @@
 import com.android.tools.profilers.StudioProfilers
 import com.android.tools.profilers.StringFormattingUtils.formatLongValueWithCommas
 import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.POWER_RAIL_TOTAL_VALUE_IN_RANGE_TOOLTIP_MSG
-import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computeCumulativeEnergyUsageInRange
+import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computeAveragePowerInRange
+import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computeCumulativeEnergyInRange
+import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computePowerUsageRange
 import com.android.tools.profilers.cpu.systemtrace.PowerCounterData
 import com.android.tools.profilers.cpu.systemtrace.PowerRailTrackModel.Companion.POWER_RAIL_UNIT
 import com.google.common.annotations.VisibleForTesting
@@ -33,22 +35,22 @@
 import icons.StudioIcons
 import java.awt.Component
 import java.awt.FlowLayout
+import java.text.DecimalFormat
 import javax.swing.JComponent
 import javax.swing.JLabel
 import javax.swing.JPanel
 import javax.swing.JTable
-import javax.swing.SwingConstants
 import javax.swing.table.AbstractTableModel
 import javax.swing.table.TableCellRenderer
 import javax.swing.table.TableRowSorter
 
 /**
- * UI component for a table presenting the power consumption per power rail in the user-selected range.
+ * UI component for a table presenting the average power and cumulative energy consumption per power rail in the user-selected range.
  * Takes a map of power rail names to their respective [PowerCounterData] and generates [PowerRailRow]s.
  * The table will look like this:
- * | Power Rail |      Cumulative consumption (µWs) |
- * | Foo        |                       123,456,789 |
- * | Bar        |                       987,654,321 |
+ * | Power Rail | Average power (mW) | Cumulative consumption (µWs) |
+ * | Foo        | 20.0               | 123,456,789                  |
+ * | Bar        | 50.0               | 987,654,321                  |
  *
  * @param powerRailCounters map of [String] to [PowerCounterData], where [PowerCounterData] includes the cumulative power series data
  * @param range a range to query power data on
@@ -69,14 +71,24 @@
       autoCreateRowSorter = true
       showVerticalLines = true
       showHorizontalLines = false
-      columnModel.getColumn(Column.RAIL_NAME.ordinal).cellRenderer = CustomBorderTableCellRenderer().apply {
-        horizontalAlignment = SwingConstants.LEFT
+
+      columnModel.getColumn(Column.RAIL_NAME.ordinal).cellRenderer = CustomBorderTableCellRenderer()
+      val avgPowerColumnRenderer = object : CustomBorderTableCellRenderer() {
+        override fun getTableCellRendererComponent(table: JTable?,
+                                                   value: Any?,
+                                                   isSelected: Boolean,
+                                                   hasFocus: Boolean,
+                                                   row: Int,
+                                                   column: Int): Component {
+          val decimalFormat = DecimalFormat("#.##")
+          val newValue = decimalFormat.format(value)
+          return super.getTableCellRendererComponent(table, newValue, isSelected, hasFocus, row, column)
+        }
       }
+      columnModel.getColumn(Column.AVERAGE.ordinal).cellRenderer = avgPowerColumnRenderer
 
       // The following is an override of the non-header cells of the Cumulative column's rendering behavior.
-      // This override will fulfill the following desired properties for the cumulative values:
-      // (1) Formatting with commas to be more readable
-      // (2) Right justification
+      // This override formats the cumulative values with commas to be more readable.
       val cumulativeColumnRenderer = object : CustomBorderTableCellRenderer() {
         override fun getTableCellRendererComponent(table: JTable?,
                                                    value: Any?,
@@ -84,37 +96,37 @@
                                                    hasFocus: Boolean,
                                                    row: Int,
                                                    column: Int): Component {
-          val newValue = if (value is Long) formatLongValueWithCommas(value) else value
+          val newValue = if (value is Long && column == Column.CONSUMPTION.ordinal) formatLongValueWithCommas(value) else value
           return super.getTableCellRendererComponent(table, newValue, isSelected, hasFocus, row, column)
         }
-
-        override fun getHorizontalAlignment(): Int {
-          return SwingConstants.RIGHT
-        }
       }
       columnModel.getColumn(Column.CONSUMPTION.ordinal).cellRenderer = cumulativeColumnRenderer
     }
 
     // The following is an override of the default table header rendering behavior.
-    // This override will enable the Cumulative column header to have the following desires properties:
-    // (1) Right justification
-    // (2) Containing an info icon
-    // (3) Containing a tooltip warning user about the data's accuracy
+    // This override will create a custom JLabel component to host the Cumulative column header with the following properties:
+    // (1) Contains an info icon
+    // (2) Contains a tooltip warning user about the data's accuracy
     table.tableHeader.columnModel.getColumn(
       Column.CONSUMPTION.ordinal).headerRenderer = TableCellRenderer { table, value, isSelected, hasFocus, row, column ->
       val headerComponent: Component = table.tableHeader.defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus,
                                                                                                        row, column)
-      JPanel().apply {
-        // By default, table header for a JBTable is left justified. By wrapping the new header rendering into a panel
-        // with a FlowLayout with RIGHT positioning, we can right justify the header content.
-        layout = FlowLayout(FlowLayout.RIGHT)
+      JLabel().apply {
+        // All other table headers are left justified by default, so we left justify this custom table header as well.
+        layout = FlowLayout(FlowLayout.LEFT)
+        border = JBUI.Borders.empty()
 
+        if (headerComponent is JComponent) {
+          // This customer header component does not have the default margin that other table header's get, so we add it.
+          headerComponent.border = JBUI.Borders.emptyLeft(2)
+        }
         // Add the header label/text.
         add(headerComponent)
 
         // Add the info icon.
         val iconComp = JLabel()
         iconComp.icon = StudioIcons.Common.INFO_INLINE
+        iconComp.border = JBUI.Borders.empty()
         add(iconComp)
 
         // Add the info tooltip regarding the data's accuracy.
@@ -187,7 +199,14 @@
 
     private fun computePowerSummary() {
       dataRows = powerRailCounters.map {
-        PowerRailRow(it.key, computeCumulativeEnergyUsageInRange(it.value.cumulativeData, selectionRange))
+        val powerUsageRange = computePowerUsageRange(it.value.cumulativeData, selectionRange)
+        // When the range selection only contains one or zero data points, it is possible for the
+        // upper bound's timestamp (x) to be less than or equal to the lower bound's timestamp (x).
+        // Thus, in this case, the cumulative value should be 0 as there is no positive difference
+        // in start and end power values.
+        val consumption = computeCumulativeEnergyInRange(powerUsageRange)
+        val average = computeAveragePowerInRange(powerUsageRange)
+        PowerRailRow(it.key, consumption, average)
       }
       fireTableDataChanged()
     }
@@ -209,6 +228,11 @@
         return data.name
       }
     },
+    AVERAGE("Average power (mW)", java.lang.Double::class.java) {
+      override fun getValueFrom(data: PowerRailRow): Double {
+        return data.average
+      }
+    },
     CONSUMPTION("Cumulative consumption ($POWER_RAIL_UNIT)", java.lang.Long::class.java) {
       override fun getValueFrom(data: PowerRailRow): Long {
         return data.consumption
@@ -224,5 +248,6 @@
  */
 private data class PowerRailRow(
   val name: String,
-  val consumption: Long
+  val consumption: Long,
+  val average: Double
 )
\ No newline at end of file
diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/PowerRailTableUtils.kt b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/PowerRailTableUtils.kt
index d2ec792..ba712d1 100644
--- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/PowerRailTableUtils.kt
+++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/PowerRailTableUtils.kt
@@ -18,19 +18,56 @@
 import com.android.tools.adtui.model.Range
 import com.android.tools.adtui.model.SeriesData
 import com.google.common.annotations.VisibleForTesting
+import java.util.concurrent.TimeUnit
 
 object PowerRailTableUtils {
+  private fun isPowerRangeInvalid(lowerBoundTs: Long, upperBoundTs: Long) = lowerBoundTs >= upperBoundTs
+
+  fun computeCumulativeEnergyInRange(powerUsageRange: PowerUsageRange) : Long {
+    val lowerBound = powerUsageRange.lowerBound
+    val upperBound = powerUsageRange.upperBound
+    val lowerBoundTs = lowerBound.x
+    val upperBoundTs = upperBound.x
+    return if (isPowerRangeInvalid(lowerBoundTs, upperBoundTs)) {
+      0
+    }
+    else {
+      upperBound.value - lowerBound.value
+    }
+  }
+
+  fun computeAveragePowerInRange(powerUsageRange: PowerUsageRange) : Double {
+    val lowerBound = powerUsageRange.lowerBound
+    val upperBound = powerUsageRange.upperBound
+    val lowerBoundTs = lowerBound.x
+    val upperBoundTs = upperBound.x
+    val durationMs = TimeUnit.MICROSECONDS.toMillis(upperBoundTs - lowerBoundTs)
+    return if (isPowerRangeInvalid(lowerBoundTs, upperBoundTs) || durationMs == 0L) {
+      0.0
+    }
+    else {
+      val cumulativeEnergy = computeCumulativeEnergyInRange(powerUsageRange)
+      // The time is in micro-seconds, so this converts it to milliseconds.
+      cumulativeEnergy / durationMs.toDouble()
+    }
+  }
+
+  data class PowerUsageRange(
+    val lowerBound: SeriesData<Long>,
+    val upperBound: SeriesData<Long>
+  )
+
   /**
-   * Computes and returns the cumulative energy used in the passed-in range.
+   * Computes and returns the cumulative power used in the passed-in range.
    */
-  fun computeCumulativeEnergyUsageInRange(cumulativeData: List<SeriesData<Long>>, selectionRange: Range): Long {
+  fun computePowerUsageRange(cumulativeData: List<SeriesData<Long>>, selectionRange: Range): PowerUsageRange {
     val lowerBound = getLowerBoundDataInRange(cumulativeData, selectionRange.min)
     val upperBound = getUpperBoundDataInRange(cumulativeData, selectionRange.max)
     // When the range selection only contains one or zero data points, it is possible for the
     // upper bound's timestamp (x) to be less than or equal to the lower bound's timestamp (x).
     // Thus, in this case, the cumulative value should be 0 as there is no positive difference
-    // in start and end energy values.
-    return if (upperBound.x > lowerBound.x) (upperBound.value - lowerBound.value) else 0
+    // in start and end power values.
+    return PowerUsageRange(lowerBound, upperBound)
   }
 
   /**
diff --git a/profilers-ui/testSrc/com/android/tools/profilers/cpu/analysis/FullTraceSummaryDetailsViewTest.kt b/profilers-ui/testSrc/com/android/tools/profilers/cpu/analysis/FullTraceSummaryDetailsViewTest.kt
index fbe5a46..f2376bf 100644
--- a/profilers-ui/testSrc/com/android/tools/profilers/cpu/analysis/FullTraceSummaryDetailsViewTest.kt
+++ b/profilers-ui/testSrc/com/android/tools/profilers/cpu/analysis/FullTraceSummaryDetailsViewTest.kt
@@ -17,6 +17,7 @@
 
 import com.android.tools.adtui.model.FakeTimer
 import com.android.tools.adtui.model.Range
+import com.android.tools.adtui.ui.HideablePanel
 import com.android.tools.idea.transport.faketransport.FakeGrpcServer
 import com.android.tools.idea.transport.faketransport.FakeTransportService
 import com.android.tools.profilers.FakeIdeProfilerComponents
@@ -28,7 +29,6 @@
 import com.android.tools.profilers.cpu.CpuCapture
 import com.android.tools.profilers.cpu.systemtrace.CounterModel
 import com.android.tools.profilers.cpu.systemtrace.ProcessModel
-import com.android.tools.profilers.cpu.systemtrace.SystemTraceCpuCapture
 import com.android.tools.profilers.cpu.systemtrace.SystemTraceCpuCaptureBuilder
 import com.android.tools.profilers.cpu.systemtrace.SystemTraceCpuCaptureBuilderTest
 import com.android.tools.profilers.cpu.systemtrace.ThreadModel
@@ -153,5 +153,17 @@
     assertThat(view.commonSection.componentCount).isEqualTo(6)
     // Three components expected: the common section, the power table section, and the help text section.
     assertThat(view.components.size).isEqualTo(3)
+
+    // Assert that the power rail table section is a {@link HideablePanel}
+    assertThat(view.components[1]).isInstanceOf(HideablePanel::class.java)
+
+    // Assert that the power rail table has the correct dimensions and column headers.
+    assertThat(view.powerRailTable).isNotNull()
+    assertThat(view.powerRailTable!!.table.columnCount).isEqualTo(3)
+    // Note: header is not included in the row count.
+    assertThat(view.powerRailTable!!.table.rowCount).isEqualTo(1)
+    assertThat(view.powerRailTable!!.table.tableHeader.columnModel.getColumn(0).headerValue).isEqualTo("Rail name")
+    assertThat(view.powerRailTable!!.table.tableHeader.columnModel.getColumn(1).headerValue).isEqualTo("Average power (mW)")
+    assertThat(view.powerRailTable!!.table.tableHeader.columnModel.getColumn(2).headerValue).isEqualTo("Cumulative consumption (µWs)")
   }
 }
\ No newline at end of file
diff --git a/profilers-ui/testSrc/com/android/tools/profilers/cpu/analysis/PowerRangeUtilsTest.kt b/profilers-ui/testSrc/com/android/tools/profilers/cpu/analysis/PowerRangeUtilsTest.kt
index 821b87f..a271924 100644
--- a/profilers-ui/testSrc/com/android/tools/profilers/cpu/analysis/PowerRangeUtilsTest.kt
+++ b/profilers-ui/testSrc/com/android/tools/profilers/cpu/analysis/PowerRangeUtilsTest.kt
@@ -17,7 +17,9 @@
 
 import com.android.tools.adtui.model.Range
 import com.android.tools.adtui.model.SeriesData
-import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computeCumulativeEnergyUsageInRange
+import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computeAveragePowerInRange
+import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computeCumulativeEnergyInRange
+import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.computePowerUsageRange
 import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.getLowerBoundDataInRange
 import com.android.tools.profilers.cpu.analysis.PowerRailTableUtils.getUpperBoundDataInRange
 import org.junit.Assert
@@ -26,39 +28,51 @@
 
 class PowerRangeUtilsTest {
   @Test
-  fun testComputeCumulativePowerUsageInRangeEntireRange() {
-    // Test case: Range encapsulates all data, expect first elements value subtracted from last elements value
-    val dataList = listOf(SeriesData(10L, 10L), SeriesData(12L, 20L), SeriesData(15L, 30L))
+  fun testComputedPowerUsageInRangeEntireRange() {
+    // Test case: Range encapsulates all data, expect first element and last element used to compute energy and avg power
+    val dataList = listOf(SeriesData(1000L, 10000L), SeriesData(12000L, 20000L), SeriesData(26000L, 35000L))
 
-    val powerUsedInRange = computeCumulativeEnergyUsageInRange(dataList, Range(9.0, 16.0))
-    Assert.assertEquals(powerUsedInRange, 20L)
+    val powerUsageRange = computePowerUsageRange(dataList, Range(0.0, 27000.0))
+    val energyUsedInRange = computeCumulativeEnergyInRange(powerUsageRange)
+    val avgPowerInRange = computeAveragePowerInRange(powerUsageRange)
+    Assert.assertEquals(energyUsedInRange, 25000L)
+    Assert.assertEquals(1000.0, avgPowerInRange, 0.000000000001)
   }
 
   @Test
-  fun testComputeCumulativePowerUsageInRangeSubsetRange() {
-    // Test case: Range encapsulates last two data points, expect second to last's elements value subtracted from last elements value
-    val dataList = listOf(SeriesData(10L, 10L), SeriesData(12L, 20L), SeriesData(15L, 30L))
+  fun testComputedPowerUsageInRangeSubsetRange() {
+    // Test case: Range encapsulates last two data points, expect second element and last element used to compute energy and avg power
+    val dataList = listOf(SeriesData(10000L, 10000L), SeriesData(12000L, 20000L), SeriesData(26000L, 35000L))
 
-    val powerUsedInRange = computeCumulativeEnergyUsageInRange(dataList, Range(11.0, 16.0))
-    Assert.assertEquals(powerUsedInRange, 10L)
+    val powerUsageRange = computePowerUsageRange(dataList, Range(11000.0, 27000.0))
+    val energyUsedInRange = computeCumulativeEnergyInRange(powerUsageRange)
+    val avgPowerInRange = computeAveragePowerInRange(powerUsageRange)
+    Assert.assertEquals(energyUsedInRange, 15000L)
+    Assert.assertEquals(15000 / (14000.0 / 1000.0), avgPowerInRange, 0.000000000001)
   }
 
   @Test
-  fun testComputeCumulativePowerUsageInRangeUpperBoundLessThanLower() {
-    // Test case: Range's upper bound timestamp is less than lower bound's timestamp, expect power used to be 0
-    val dataList = listOf(SeriesData(10L, 10L), SeriesData(12L, 20L), SeriesData(15L, 30L))
+  fun testComputedPowerUsageInRangeUpperBoundLessThanLower() {
+    // Test case: Range's upper bound timestamp is less than lower bound's timestamp, expect total energy and avg power used to be 0
+    val dataList = listOf(SeriesData(10000L, 10000L), SeriesData(12000L, 20000L), SeriesData(26000L, 35000L))
 
-    val powerUsedInRange = computeCumulativeEnergyUsageInRange(dataList, Range(12.0, 11.0))
-    Assert.assertEquals(powerUsedInRange, 0L)
+    val powerUsageRange = computePowerUsageRange(dataList, Range(12000.0, 11000.0))
+    val energyUsedInRange = computeCumulativeEnergyInRange(powerUsageRange)
+    val avgPowerInRange = computeAveragePowerInRange(powerUsageRange)
+    Assert.assertEquals(energyUsedInRange, 0)
+    Assert.assertEquals(avgPowerInRange, 0.0, 0.000000000001)
   }
 
   @Test
-  fun testComputeCumulativePowerUsageInRangeUpperBoundEqualToLower() {
-    // Test case: Range's upper bound timestamp is equal to lower bound's timestamp, expect power used to be 0
-    val dataList = listOf(SeriesData(10L, 10L), SeriesData(12L, 20L), SeriesData(15L, 30L))
+  fun testComputedPowerUsageInRangeUpperBoundEqualToLower() {
+    // Test case: Range's upper bound timestamp is equal to lower bound's timestamp, expect total energy and avg power used to be 0
+    val dataList = listOf(SeriesData(10000L, 10000L), SeriesData(12000L, 20000L), SeriesData(26000L, 35000L))
 
-    val powerUsedInRange = computeCumulativeEnergyUsageInRange(dataList, Range(11.0, 11.0))
-    Assert.assertEquals(powerUsedInRange, 0L)
+    val powerUsageRange = computePowerUsageRange(dataList, Range(11000.0, 11000.0))
+    val energyUsedInRange = computeCumulativeEnergyInRange(powerUsageRange)
+    val avgPowerInRange = computeAveragePowerInRange(powerUsageRange)
+    Assert.assertEquals(energyUsedInRange, 0)
+    Assert.assertEquals(avgPowerInRange, 0.0, 0.000000000001)
   }
 
   @Test
diff --git a/project-system-gradle/src/com/android/tools/idea/projectsystem/gradle/GradleProjectPath.kt b/project-system-gradle/src/com/android/tools/idea/projectsystem/gradle/GradleProjectPath.kt
index f82e944..f6689a5 100644
--- a/project-system-gradle/src/com/android/tools/idea/projectsystem/gradle/GradleProjectPath.kt
+++ b/project-system-gradle/src/com/android/tools/idea/projectsystem/gradle/GradleProjectPath.kt
@@ -19,7 +19,6 @@
 import com.android.tools.idea.gradle.model.impl.IdeModuleSourceSetImpl
 import com.android.tools.idea.projectsystem.ProjectSyncModificationTracker
 import com.android.utils.FileUtils
-import com.intellij.openapi.externalSystem.model.project.ModuleData
 import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
 import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil.getExternalModuleType
 import com.intellij.openapi.module.Module
@@ -33,9 +32,7 @@
 import org.jetbrains.annotations.VisibleForTesting
 import org.jetbrains.plugins.gradle.execution.build.CachedModuleDataFinder
 import org.jetbrains.plugins.gradle.util.GradleConstants
-import org.jetbrains.plugins.gradle.util.gradleIdentityPath
 import org.jetbrains.plugins.gradle.util.gradleIdentityPathOrNull
-import org.jetbrains.plugins.gradle.util.gradlePath
 import org.jetbrains.plugins.gradle.util.gradlePathOrNull
 import org.jetbrains.plugins.gradle.util.isIncludedBuild
 import java.io.File
@@ -152,7 +149,7 @@
 
 fun Module.getGradleIdentityPath(): String? {
   @Suppress("UnstableApiUsage")
-  return CachedModuleDataFinder.getGradleModuleData(this)?.gradleIdentityPath
+  return CachedModuleDataFinder.getGradleModuleData(this)?.moduleData?.gradleIdentityPathOrNull
 }
 
 fun Module.getGradleProjectPath(): GradleProjectPath? {
diff --git a/rendering/src/com/android/tools/rendering/RenderExecutor.kt b/rendering/src/com/android/tools/rendering/RenderExecutor.kt
index c0c187e..645ac33 100644
--- a/rendering/src/com/android/tools/rendering/RenderExecutor.kt
+++ b/rendering/src/com/android/tools/rendering/RenderExecutor.kt
@@ -275,6 +275,11 @@
   /** Returns true if the render thread is busy running some code, false otherwise. */
   fun isBusy() = renderingExecutorService.isBusy
 
+  /**
+   * Returns true if called from the render thread.
+   */
+  fun isRenderThread(): Boolean = renderingExecutorService.hasSpawnedCurrentThread()
+
   companion object {
     @JvmStatic
     fun create(): RenderExecutor {
diff --git a/rendering/src/com/android/tools/rendering/RenderService.java b/rendering/src/com/android/tools/rendering/RenderService.java
index aec0bf3..0f90463 100644
--- a/rendering/src/com/android/tools/rendering/RenderService.java
+++ b/rendering/src/com/android/tools/rendering/RenderService.java
@@ -60,21 +60,36 @@
  * The {@link RenderService} provides rendering and layout information for Android layouts. This is a wrapper around the layout library.
  */
 final public class RenderService implements Disposable {
-  private static RenderExecutor ourExecutor;
+  private static final Object ourExecutorLock = new Object();
+  private static RenderExecutor ourExecutor = getOrCreateExecutor();
 
-  static {
-    ourExecutor = RenderExecutor.create();
+  @NotNull
+  private static RenderExecutor getOrCreateExecutor() {
+    synchronized (ourExecutorLock) {
+      if (ourExecutor == null) ourExecutor = RenderExecutor.create();
+      return ourExecutor;
+    }
+  }
+
+  @Nullable
+  private static RenderExecutor getExistingExecutor() {
+    synchronized (ourExecutorLock) {
+      return ourExecutor;
+    }
   }
 
   @TestOnly
   public static void initializeRenderExecutor() {
     assert ApplicationManager.getApplication().isUnitTestMode(); // Only to be called from unit testszs
 
-    ourExecutor = RenderExecutor.create();
+    synchronized (ourExecutorLock) {
+      ourExecutor = RenderExecutor.create();
+    }
   }
 
   public static void shutdownRenderExecutor() {
-    ourExecutor.shutdown();
+    RenderExecutor currentExecutor = getExistingExecutor();
+    if (currentExecutor != null) currentExecutor.shutdown();
   }
 
   /**
@@ -85,7 +100,9 @@
   public static void shutdownRenderExecutor(@SuppressWarnings("SameParameterValue") long timeoutSeconds) {
     assert ApplicationManager.getApplication().isUnitTestMode(); // Only to be called from unit tests
 
-    ourExecutor.shutdown(timeoutSeconds);
+    // We avoid using getExecutor here since we do not want to create a new one if it doesn't exist
+    RenderExecutor currentExecutor = getExistingExecutor();
+    if (currentExecutor != null) currentExecutor.shutdown(timeoutSeconds);
   }
 
   private static final String JDK_INSTALL_URL = "https://developer.android.com/preview/setup-sdk.html#java8";
@@ -98,7 +115,7 @@
 
   @NotNull
   public static RenderAsyncActionExecutor getRenderAsyncActionExecutor() {
-    return ourExecutor;
+    return getOrCreateExecutor();
   }
 
   /**
@@ -106,7 +123,7 @@
    */
   @TestOnly
   public static StackTraceElement[] getCurrentExecutionStackTrace() {
-    return ourExecutor.currentStackTrace();
+    return getOrCreateExecutor().currentStackTrace();
   }
 
   public RenderService(@NotNull Consumer<RenderTaskBuilder> configureBuilder) {
@@ -148,7 +165,16 @@
    * @return true if the underlying {@link RenderExecutor} is busy, false otherwise.
    */
   public static boolean isBusy() {
-    return ourExecutor.isBusy();
+    RenderExecutor currentExecutor = getExistingExecutor();
+    return currentExecutor != null && currentExecutor.isBusy();
+  }
+
+  /**
+   * @return true if called from the render thread.
+   */
+  public static boolean isRenderThread() {
+    RenderExecutor currentExecutor = getExistingExecutor();
+    return currentExecutor != null && currentExecutor.isRenderThread();
   }
 
   /**
diff --git a/rendering/src/com/android/tools/rendering/SingleThreadExecutorService.kt b/rendering/src/com/android/tools/rendering/SingleThreadExecutorService.kt
index 4646b1e..4c2c791 100644
--- a/rendering/src/com/android/tools/rendering/SingleThreadExecutorService.kt
+++ b/rendering/src/com/android/tools/rendering/SingleThreadExecutorService.kt
@@ -32,9 +32,7 @@
   Integer.getInteger("layoutlib.thread.profile.timeoutms", 4000)
 private val PROFILE_INTERVAL_MS = Integer.getInteger("layoutlib.thread.profile.intervalms", 2500)
 
-/**
- * Settings for profiling a thread.
- */
+/** Settings for profiling a thread. */
 data class ThreadProfileSettings(
   /** Ms to wait before considering a running task as slow and start profiling. */
   val profileSlowTasksTimeoutMs: Long = PROFILE_SLOW_TASKS_TIMEOUT_MS.toLong(),
@@ -48,42 +46,45 @@
   val onSlowThread: (Throwable) -> Unit
 ) {
   companion object {
-    val disabled = ThreadProfileSettings(
-      maxSamples = 0,
-      scheduledExecutorService = ScheduledThreadPoolExecutor(0),
-      onSlowThread = {}
-    )
+    val disabled =
+      ThreadProfileSettings(
+        maxSamples = 0,
+        scheduledExecutorService = ScheduledThreadPoolExecutor(0),
+        onSlowThread = {}
+      )
   }
 }
 
 /**
- * Interface for a single threaded executor service with additional functionality to allow
- * for interrupting the thread and checking whether is active or not.
+ * Interface for a single threaded executor service with additional functionality to allow for
+ * interrupting the thread and checking whether is active or not.
  */
-interface SingleThreadExecutorService: ExecutorService {
+interface SingleThreadExecutorService : ExecutorService {
   /** True if busy and a new command will not execute immediately. */
   val isBusy: Boolean
 
+  /** Returns true if called from the thread that is handled by this [ExecutorService]. */
+  fun hasSpawnedCurrentThread(): Boolean
+
   /**
-   * Returns the current stack thread for the thread executing commands.
-   * The stack trace might be empty if the thread is not currently running.
+   * Returns the current stack thread for the thread executing commands. The stack trace might be
+   * empty if the thread is not currently running.
    */
   fun stackTrace(): Array<StackTraceElement>
 
-  /**
-   * Sends an interrupt to the thread and returns immediately.
-   */
+  /** Sends an interrupt to the thread and returns immediately. */
   fun interrupt()
 
   companion object {
     /**
-     * Creates a new single thread [ExecutorService] with optional profiling. The [threadName] will name
-     * the thread used by this executor.
+     * Creates a new single thread [ExecutorService] with optional profiling. The [threadName] will
+     * name the thread used by this executor.
      */
     fun create(
       threadName: String,
       threadProfileSettings: ThreadProfileSettings
-    ): SingleThreadExecutorService = ProfilingSingleThreadExecutorImpl(threadName, threadProfileSettings)
+    ): SingleThreadExecutorService =
+      ProfilingSingleThreadExecutorImpl(threadName, threadProfileSettings)
   }
 }
 
@@ -97,30 +98,28 @@
   override val isBusy: Boolean
     get() = _isBusy.get()
 
+  private val renderThreadGroup = ThreadGroup("Render Thread group")
+
   /**
    * The thread factory allows us controlling when the new thread is created to we can keep track of
    * it. This allows us to capture the stack trace later.
    */
   private val threadFactory = ThreadFactory {
-    val newThread = Thread(null, it, threadName).apply { isDaemon = true }
+    val newThread = Thread(renderThreadGroup, it, threadName).apply { isDaemon = true }
     theThread.set(newThread)
     newThread
   }
 
-  private fun profileThread(
-    remainingSamples: Int,
-    profileThreadFuture: CompletableFuture<Unit>
-  ) {
+  private fun profileThread(remainingSamples: Int, profileThreadFuture: CompletableFuture<Unit>) {
     if (remainingSamples <= 0) return
     if (!isBusy) return
     threadProfileSettings.onSlowThread(
       TimeoutException(
-        "Slow render action ${threadProfileSettings.maxSamples-remainingSamples}/${threadProfileSettings.maxSamples}"
-      ).also { slowException ->
-        theThread.get()?.also {
-          slowException.stackTrace = it.stackTrace
+          "Slow render action ${threadProfileSettings.maxSamples-remainingSamples}/${threadProfileSettings.maxSamples}"
+        )
+        .also { slowException ->
+          theThread.get()?.also { slowException.stackTrace = it.stackTrace }
         }
-      }
     )
     threadProfileSettings.scheduledExecutorService
       .schedule(
@@ -133,34 +132,37 @@
       }
   }
   private var profileThreadFuture: CompletableFuture<Unit>? = null
-  private val threadPoolExecutor = object:
-    ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, PriorityBlockingQueue(), threadFactory) {
-    override fun beforeExecute(t: Thread?, r: Runnable?) {
-      _isBusy.set(true)
-      profileThreadFuture = CompletableFuture()
-      threadProfileSettings.scheduledExecutorService
-        .schedule(
-          { profileThread(threadProfileSettings.maxSamples, profileThreadFuture!!) },
-          threadProfileSettings.profileSlowTasksTimeoutMs,
-          TimeUnit.MILLISECONDS
-        )
-        .also { scheduledFuture ->
-          profileThreadFuture!!.whenComplete { _, _ -> scheduledFuture.cancel(true) }
-        }
-      super.beforeExecute(t, r)
+  private val threadPoolExecutor =
+    object :
+      ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, PriorityBlockingQueue(), threadFactory) {
+      override fun beforeExecute(t: Thread?, r: Runnable?) {
+        _isBusy.set(true)
+        profileThreadFuture = CompletableFuture()
+        threadProfileSettings.scheduledExecutorService
+          .schedule(
+            { profileThread(threadProfileSettings.maxSamples, profileThreadFuture!!) },
+            threadProfileSettings.profileSlowTasksTimeoutMs,
+            TimeUnit.MILLISECONDS
+          )
+          .also { scheduledFuture ->
+            profileThreadFuture!!.whenComplete { _, _ -> scheduledFuture.cancel(true) }
+          }
+        super.beforeExecute(t, r)
+      }
+
+      override fun afterExecute(r: Runnable?, t: Throwable?) {
+        val pendingProfiling = profileThreadFuture
+        profileThreadFuture = null
+        pendingProfiling?.cancel(true)
+        _isBusy.set(false)
+        super.afterExecute(r, t)
+      }
     }
 
-    override fun afterExecute(r: Runnable?, t: Throwable?) {
-      val pendingProfiling = profileThreadFuture
-      profileThreadFuture = null
-      pendingProfiling?.cancel(true)
-      _isBusy.set(false)
-      super.afterExecute(r, t)
-    }
-  }
+  override fun hasSpawnedCurrentThread(): Boolean =
+    renderThreadGroup.parentOf(Thread.currentThread().threadGroup)
 
-  override fun stackTrace(): Array<StackTraceElement> =
-    theThread.get()?.stackTrace ?: emptyArray()
+  override fun stackTrace(): Array<StackTraceElement> = theThread.get()?.stackTrace ?: emptyArray()
 
   override fun interrupt() {
     theThread.get()?.interrupt()
diff --git a/rendering/src/com/android/tools/rendering/security/RenderSecurityManager.java b/rendering/src/com/android/tools/rendering/security/RenderSecurityManager.java
index 5465dc5..1a57dd0 100644
--- a/rendering/src/com/android/tools/rendering/security/RenderSecurityManager.java
+++ b/rendering/src/com/android/tools/rendering/security/RenderSecurityManager.java
@@ -16,6 +16,7 @@
 package com.android.tools.rendering.security;
 
 import com.android.annotations.Nullable;
+import com.android.tools.rendering.RenderService;
 import com.android.utils.ILogger;
 
 import com.intellij.openapi.application.PathManager;
@@ -31,6 +32,7 @@
 import java.util.Arrays;
 import java.util.PropertyPermission;
 import java.util.concurrent.Callable;
+import java.util.function.Supplier;
 import org.jetbrains.annotations.NotNull;
 
 import static com.android.SdkConstants.*;
@@ -59,25 +61,6 @@
   public static boolean sEnabled = !VALUE_FALSE.equals(System.getProperty(ENABLED_PROPERTY));
 
   /**
-   * Thread local data which indicates whether the current thread is relevant for
-   * this security manager. This is an inheritable thread local such that any threads
-   * spawned from this thread will also apply the security manager; otherwise code
-   * could just create new threads and execute code separate from the security manager
-   * there.
-   */
-  private static ThreadLocal<Boolean> sIsRenderThread = new InheritableThreadLocal<Boolean>() {
-    @Override
-    protected synchronized Boolean initialValue() {
-      return Boolean.FALSE;
-    }
-
-    @Override
-    protected synchronized Boolean childValue(Boolean parentValue) {
-      return parentValue;
-    }
-  };
-
-  /**
    * Secret which must be provided by callers wishing to deactivate the security manager
    */
   private static Object sCredential;
@@ -100,15 +83,7 @@
 
   private boolean isRestrictReads;
 
-  @NotNull
-  private static String normalizeDirectoryPath(@NotNull Path originalPath) {
-    return originalPath.normalize() + originalPath.getFileSystem().getSeparator();
-  }
-
-  @NotNull
-  private static String normalizeDirectoryPath(@NotNull String stringPath) {
-    return normalizeDirectoryPath(Paths.get(stringPath));
-  }
+  private final Supplier<Boolean> isRenderThread;
 
   /**
    * Returns the current render security manager, if any. This will only return
@@ -117,12 +92,10 @@
    */
   @Nullable
   public static RenderSecurityManager getCurrent() {
-    if (sIsRenderThread.get()) {
-      SecurityManager securityManager = System.getSecurityManager();
-      if (securityManager instanceof RenderSecurityManager) {
-        RenderSecurityManager manager = (RenderSecurityManager)securityManager;
-        return manager.isRelevant() ? manager : null;
-      }
+    SecurityManager securityManager = System.getSecurityManager();
+    if (securityManager instanceof RenderSecurityManager) {
+      RenderSecurityManager manager = (RenderSecurityManager)securityManager;
+      return manager.isRelevant() ? manager : null;
     }
 
     return null;
@@ -147,11 +120,12 @@
    * @param restrictReads when true, reads will be restricted to only a set of directories including temp directory and the given sdkPath
    *                      and projectPath directories.
    */
-  public RenderSecurityManager(
+  protected RenderSecurityManager(
     @Nullable String sdkPath,
     @Nullable String projectPath,
     boolean restrictReads,
-    @NotNull String[] allowedPaths) {
+    @NotNull String[] allowedPaths,
+    @NotNull Supplier<Boolean> isRenderThread) {
     mSdkPath = sdkPath;
     mProjectPath = projectPath;
     mTempDir = System.getProperty("java.io.tmpdir");
@@ -160,23 +134,24 @@
     //noinspection AssignmentToStaticFieldFromInstanceMethod
     sLastFailedPath = null;
     isRestrictReads = restrictReads;
+    this.isRenderThread = isRenderThread;
   }
 
-  public RenderSecurityManager(
+  @NotNull
+  static RenderSecurityManager createForTests(
+    @Nullable String sdkPath,
+    @Nullable String projectPath,
+    boolean restrictReads,
+    @NotNull Supplier<Boolean> isRenderThread) {
+    return new RenderSecurityManager(sdkPath, projectPath, restrictReads, RenderSecurityManagerDefaults.getDefaultAllowedPaths(), isRenderThread);
+  }
+
+  @NotNull
+  public static RenderSecurityManager create(
     @Nullable String sdkPath,
     @Nullable String projectPath,
     boolean restrictReads) {
-    this(sdkPath, projectPath, restrictReads, new String[]{
-      // When loading classes, IntelliJ might sometimes drop a corruption marker
-      normalizeDirectoryPath(PathManager.getIndexRoot()),
-      /*
-        Root of the path where IntelliJ stores the logs. When loading classes,
-        IntelliJ might try to update cache hashes for the loaded files
-      */
-      normalizeDirectoryPath(PathManager.getLogPath()),
-      // When loading classes, IntelliJ might try to update cache hashes for the loaded files
-      normalizeDirectoryPath(Paths.get(PathManager.getSystemPath(), "caches"))
-    });
+    return new RenderSecurityManager(sdkPath, projectPath, restrictReads, RenderSecurityManagerDefaults.getDefaultAllowedPaths(), RenderService::isRenderThread);
   }
 
   /**
@@ -216,7 +191,6 @@
       // Enable
       assert !(current instanceof RenderSecurityManager);
       myPreviousSecurityManager = current;
-      sIsRenderThread.set(true);
       mDisabled = false;
       System.setSecurityManager(this);
       //noinspection AssignmentToStaticFieldFromInstanceMethod
@@ -246,20 +220,18 @@
           System.setSecurityManager(myPreviousSecurityManager);
         }
         else if (mLogger != null) {
-          sIsRenderThread.set(false);
           mLogger.warning("Security manager was changed behind the scenes: ", current);
         }
       }
       finally {
         mDisabled = true;
         mAllowSetSecurityManager = false;
-        sIsRenderThread.set(false);
       }
     }
   }
 
   protected boolean isRelevant() {
-    return sEnabled && !mDisabled && sIsRenderThread.get();
+    return sEnabled && !mDisabled && isRenderThread.get();
   }
 
   /**
diff --git a/rendering/src/com/android/tools/rendering/security/RenderSecurityManagerDefaults.kt b/rendering/src/com/android/tools/rendering/security/RenderSecurityManagerDefaults.kt
new file mode 100644
index 0000000..29f907f
--- /dev/null
+++ b/rendering/src/com/android/tools/rendering/security/RenderSecurityManagerDefaults.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.tools.rendering.security
+
+import com.intellij.openapi.application.PathManager
+import java.nio.file.Path
+import java.nio.file.Paths
+
+private fun normalizeDirectoryPath(originalPath: Path): String =
+  originalPath.normalize().toString() + originalPath.fileSystem.separator
+
+private fun normalizeDirectoryPath(stringPath: String): String =
+  normalizeDirectoryPath(Paths.get(stringPath))
+
+object RenderSecurityManagerDefaults {
+  @JvmStatic
+  fun getDefaultAllowedPaths(): Array<String> =
+    arrayOf(
+      // When loading classes, IntelliJ might sometimes drop a corruption marker
+      normalizeDirectoryPath(PathManager.getIndexRoot()),
+      /*
+        Root of the path where IntelliJ stores the logs. When loading classes,
+        IntelliJ might try to update cache hashes for the loaded files
+      */
+      normalizeDirectoryPath(PathManager.getLogPath()),
+      // When loading classes, IntelliJ might try to update cache hashes for the loaded files
+      normalizeDirectoryPath(Paths.get(PathManager.getSystemPath(), "caches"))
+    )
+}
\ No newline at end of file
diff --git a/rendering/testSrc/com/android/tools/rendering/RenderExecutorTest.kt b/rendering/testSrc/com/android/tools/rendering/RenderExecutorTest.kt
index b1b9d66..f96e68f 100644
--- a/rendering/testSrc/com/android/tools/rendering/RenderExecutorTest.kt
+++ b/rendering/testSrc/com/android/tools/rendering/RenderExecutorTest.kt
@@ -48,6 +48,7 @@
 
 private class TestSingleThreadExecutorService(private val delegate: ExecutorService): AbstractExecutorService(), SingleThreadExecutorService {
   override val isBusy: Boolean = false
+  override fun hasSpawnedCurrentThread(): Boolean = false
   override fun stackTrace(): Array<StackTraceElement> = emptyArray()
   override fun interrupt() {}
   override fun execute(command: Runnable) = delegate.execute(command)
diff --git a/rendering/testSrc/com/android/tools/rendering/SingleThreadExecutorServiceTest.kt b/rendering/testSrc/com/android/tools/rendering/SingleThreadExecutorServiceTest.kt
index 475f7f3..e69a320 100644
--- a/rendering/testSrc/com/android/tools/rendering/SingleThreadExecutorServiceTest.kt
+++ b/rendering/testSrc/com/android/tools/rendering/SingleThreadExecutorServiceTest.kt
@@ -17,10 +17,12 @@
 
 import com.android.testutils.VirtualTimeScheduler
 import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import org.junit.Assert
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.util.concurrent.CountDownLatch
 
 class SingleThreadExecutorServiceTest {
   @Test
@@ -66,4 +68,29 @@
 
     executor.shutdown()
   }
+
+  @Test
+  fun testHasSpawnedCurrentThread() {
+    val executor =
+      SingleThreadExecutorService.create(
+        "Test thread",
+        threadProfileSettings = ThreadProfileSettings.disabled
+      )
+
+    var exception: Throwable? = null
+    executor.submit {
+      assertTrue(executor.hasSpawnedCurrentThread())
+
+      Thread {
+        // Child threads must also return hasSpawnedCurrentThread
+        assertTrue(executor.hasSpawnedCurrentThread())
+      }.also {
+        it.setUncaughtExceptionHandler { t, e -> exception = e  }
+        it.start()
+        it.join()
+      }
+      exception?.let { throw it }
+    }.get()
+    assertFalse(executor.hasSpawnedCurrentThread())
+  }
 }
diff --git a/rendering/testSrc/com/android/tools/rendering/security/RenderSecurityManagerTest.java b/rendering/testSrc/com/android/tools/rendering/security/RenderSecurityManagerTest.java
index a7d1285..21c14b3 100644
--- a/rendering/testSrc/com/android/tools/rendering/security/RenderSecurityManagerTest.java
+++ b/rendering/testSrc/com/android/tools/rendering/security/RenderSecurityManagerTest.java
@@ -18,6 +18,7 @@
 import com.android.ide.common.resources.RecordingLogger;
 import com.android.testutils.TestUtils;
 import com.android.utils.SdkUtils;
+import com.google.common.base.Predicates;
 import com.google.common.io.Files;
 import com.intellij.mock.MockApplication;
 import com.intellij.openapi.Disposable;
@@ -30,6 +31,7 @@
 import java.nio.charset.Charset;
 import java.nio.file.Path;
 import java.util.UUID;
+import java.util.function.Supplier;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
@@ -80,7 +82,7 @@
   @Test
   public void testExec() throws IOException {
     assertNull(RenderSecurityManager.getCurrent());
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     RecordingLogger logger = new RecordingLogger();
     manager.setLogger(logger);
     try {
@@ -109,7 +111,7 @@
 
   @Test
   public void testSetSecurityManager() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
       System.setSecurityManager(null);
@@ -126,10 +128,11 @@
 
   @Test
   public void testRead() {
-    RenderSecurityManager manager = new RenderSecurityManager(
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(
       "/Users/userHome/Sdk",
       "/Users/userHome/Projects/project1",
-      true);
+      true,
+      () -> true);
 
     // Not allowed paths
     String[] notAllowedPaths = new String[] {
@@ -179,10 +182,11 @@
 
   @Test
   public void testWrite() {
-    RenderSecurityManager manager = new RenderSecurityManager(
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(
       "/Users/userHome/Sdk",
       "/Users/userHome/Projects/project1",
-      false);
+      false,
+      () -> true);
 
     String cachePath = PathManager.getSystemPath() + "/caches/";
     String indexPath = PathManager.getIndexRoot() + "/";
@@ -230,7 +234,7 @@
 
   @Test
   public void testExecute() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
       manager.checkPermission(new FilePermission("/foo", "execute"));
@@ -247,7 +251,7 @@
 
   @Test
   public void testDelete() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
       manager.checkPermission(new FilePermission("/foo", "delete"));
@@ -264,7 +268,7 @@
 
   @Test
   public void testLoadLibrary() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -285,7 +289,7 @@
 
   @Test
   public void testAllowedLoadLibrary() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -302,7 +306,7 @@
   @SuppressWarnings("CheckReturnValue")
   @Test
   public void testInvalidRead() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -322,7 +326,7 @@
 
   @Test
   public void testInvalidPropertyWrite() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -344,7 +348,7 @@
   @SuppressWarnings("CheckReturnValue")
   @Test
   public void testReadOk() throws IOException {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -367,7 +371,7 @@
 
   @Test
   public void testProperties() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -387,7 +391,7 @@
 
   @Test
   public void testExit() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -420,7 +424,7 @@
         }
       }
     };
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -452,7 +456,7 @@
 
   @Test
   public void testActive() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -493,6 +497,8 @@
 
   @Test
   public void testThread2() throws InterruptedException {
+    ThreadGroup renderThreadGroup = new ThreadGroup("Render thread group");
+    Supplier<Boolean> isRenderThread = () -> renderThreadGroup.parentOf(Thread.currentThread().getThreadGroup());
     final List<Thread> threads = Collections.synchronizedList(new ArrayList<>());
     // Check that when a new thread is created simultaneously from an unrelated
     // thread during rendering, that new thread does not pick up the security manager.
@@ -514,14 +520,14 @@
     // uninstalled, and at barrier5 all threads will check that there are no more
     // restrictions. At barrier6 all threads are done.
 
-    final Thread thread1 = new Thread("render") {
+    final Thread thread1 = new Thread(renderThreadGroup, "render") {
       @Override
       public void run() {
         try {
           barrier1.await();
           assertNull(RenderSecurityManager.getCurrent());
 
-          RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+          RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, isRenderThread);
           manager.setActive(true, myCredential);
 
           barrier2.await();
@@ -649,7 +655,7 @@
   public void testDisabled() {
     assertNull(RenderSecurityManager.getCurrent());
 
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     RenderSecurityManager.sEnabled = false;
     try {
       assertNull(RenderSecurityManager.getCurrent());
@@ -708,7 +714,8 @@
     });
     thread.start();
 
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    Thread thisThread = Thread.currentThread();
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> thisThread == Thread.currentThread());
     RecordingLogger logger = new RecordingLogger();
     manager.setLogger(logger);
     try {
@@ -740,7 +747,7 @@
 
   @Test
   public void testEnterExitSafeRegion() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     Object credential = new Object();
     try {
       manager.setActive(true, credential);
@@ -815,7 +822,7 @@
 
   @Test
   public void testRunSafeRegion() throws Exception {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     Object credential = new Object();
     try {
       manager.setActive(true, credential);
@@ -863,7 +870,7 @@
   public void testImageIo() throws IOException, InterruptedException {
     // Warm up ImageIO static state that calls write actions forbidden by RenderSecurityManager
     ImageIO.getCacheDirectory();
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -907,7 +914,7 @@
 
   @Test
   public void testTempDir() throws IOException {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -928,7 +935,7 @@
 
   @Test
   public void testAppTempDir() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setAppTempDir("/random/path/");
       manager.setActive(true, myCredential);
@@ -943,7 +950,7 @@
   public void testSetTimeZone() {
     // Warm up TimeZone.defaultTimeZone initialization that accesses properties forbidden by RenderSecurityManager
     TimeZone.getDefault();
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -974,7 +981,7 @@
    */
   @Test
   public void testLogDir() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -990,7 +997,7 @@
   }
   @Test
   public void testLogException() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
@@ -1017,7 +1024,7 @@
 
   @Test
   public void testNoLinkCreationAllowed() throws IOException {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     Path testTemp = java.nio.file.Files.createTempDirectory("linkTest");
     Path attackLink = testTemp.resolve("attack");
     File victimFile = new File(PathManager.getConfigPath() + "/victim-" + UUID.randomUUID().toString());
@@ -1037,7 +1044,7 @@
 
   @Test
   public void testCheckWriteToNonExistingLink() throws IOException {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
 
     Path testTemp = java.nio.file.Files.createTempDirectory("linkTest");
     Path attackLink = testTemp.resolve("attack");
@@ -1060,7 +1067,7 @@
 
   @Test
   public void testCheckWriteToExistingLink() throws IOException {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
 
     Path testTemp = java.nio.file.Files.createTempDirectory("linkTest");
     Path attackLink = testTemp.resolve("attack");
@@ -1086,7 +1093,7 @@
    */
   @Test
   public void testPathTraversal() throws IOException {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
       manager.checkPermission(new FilePermission("/tmp/../dev/null", "read,write"));
@@ -1102,7 +1109,7 @@
 
   @Test
   public void testSystemPropertiesAccess() {
-    RenderSecurityManager manager = new RenderSecurityManager(null, null, false);
+    RenderSecurityManager manager = RenderSecurityManager.createForTests(null, null, false, () -> true);
     try {
       manager.setActive(true, myCredential);
 
diff --git a/streaming/screen-sharing-agent/.idea/gradle.xml b/streaming/screen-sharing-agent/.idea/gradle.xml
index ae388c2..0897082 100644
--- a/streaming/screen-sharing-agent/.idea/gradle.xml
+++ b/streaming/screen-sharing-agent/.idea/gradle.xml
@@ -4,16 +4,15 @@
   <component name="GradleSettings">
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>
-        <option name="testRunner" value="GRADLE" />
-        <option name="distributionType" value="DEFAULT_WRAPPED" />
         <option name="externalProjectPath" value="$PROJECT_DIR$" />
-        <option name="gradleJvm" value="jbr-17" />
+        <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
         <option name="modules">
           <set>
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$/app" />
           </set>
         </option>
+        <option name="resolveExternalAnnotations" value="false" />
       </GradleProjectSettings>
     </option>
   </component>
diff --git a/streaming/screen-sharing-agent/.idea/migrations.xml b/streaming/screen-sharing-agent/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/streaming/screen-sharing-agent/.idea/migrations.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectMigrations">
+    <option name="MigrateToGradleLocalJavaHome">
+      <set>
+        <option value="$PROJECT_DIR$" />
+      </set>
+    </option>
+  </component>
+</project>
\ No newline at end of file
diff --git a/streaming/screen-sharing-agent/.idea/misc.xml b/streaming/screen-sharing-agent/.idea/misc.xml
index 0f86676..8978d23 100644
--- a/streaming/screen-sharing-agent/.idea/misc.xml
+++ b/streaming/screen-sharing-agent/.idea/misc.xml
@@ -1,4 +1,5 @@
 <project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
   <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
diff --git a/streaming/screen-sharing-agent/app/build.gradle b/streaming/screen-sharing-agent/app/build.gradle
index 2ead813..5fa9314 100644
--- a/streaming/screen-sharing-agent/app/build.gradle
+++ b/streaming/screen-sharing-agent/app/build.gradle
@@ -39,4 +39,8 @@
     lintOptions {
         checkReleaseBuilds false
     }
+    buildFeatures {
+        aidl true
+    }
+    namespace 'com.android.tools.screensharing'
 }
diff --git a/streaming/screen-sharing-agent/app/src/main/cpp/accessors/surface_control.cc b/streaming/screen-sharing-agent/app/src/main/cpp/accessors/surface_control.cc
index 25ecec8..62d8c90 100644
--- a/streaming/screen-sharing-agent/app/src/main/cpp/accessors/surface_control.cc
+++ b/streaming/screen-sharing-agent/app/src/main/cpp/accessors/surface_control.cc
@@ -114,7 +114,7 @@
 }
 
 void SurfaceControl::ConfigureProjection(
-    Jni jni, jobject display_token, ANativeWindow* surface, const DisplayInfo& display_info, Size projected_size) {
+    Jni jni, jobject display_token, ANativeWindow* surface, const DisplayInfo& display_info, ARect projection_rect) {
   struct Transaction {
     explicit Transaction(Jni jni)
         : jni_(jni) {
@@ -129,7 +129,7 @@
   InitializeStatics(jni);
   Transaction transaction(jni);
   SetDisplaySurface(jni, display_token, surface);
-  SetDisplayProjection(jni, display_token, 0, display_info.logical_size.toRect(), projected_size.toRect());
+  SetDisplayProjection(jni, display_token, 0, display_info.logical_size.toRect(), projection_rect);
   SetDisplayLayerStack(jni, display_token, display_info.layer_stack);
 }
 
diff --git a/streaming/screen-sharing-agent/app/src/main/cpp/accessors/surface_control.h b/streaming/screen-sharing-agent/app/src/main/cpp/accessors/surface_control.h
index 6d17ce7..e862159 100644
--- a/streaming/screen-sharing-agent/app/src/main/cpp/accessors/surface_control.h
+++ b/streaming/screen-sharing-agent/app/src/main/cpp/accessors/surface_control.h
@@ -44,7 +44,8 @@
   static void CloseTransaction(Jni jni);
   static void SetDisplaySurface(Jni jni, jobject display_token, ANativeWindow* surface);
   static void SetDisplayLayerStack(Jni jni, jobject display_token, int32_t layer_stack);
-  static void SetDisplayProjection(Jni jni, jobject display_token, int32_t orientation, const ARect& layer_stack_rect, const ARect& display_rect);
+  static void SetDisplayProjection(
+      Jni jni, jobject display_token, int32_t orientation, const ARect& layer_stack_rect, const ARect& display_rect);
 
   static JObject ToJava(Jni jni, const ARect& rect);
 
@@ -54,9 +55,9 @@
 
   static JObject CreateDisplay(Jni jni, const char* name, bool secure);
   static void DestroyDisplay(Jni jni, jobject display_token);
-  // The display area defined by display_info.logical_size is mapped to projected size.
+  // The display area defined by display_info.logical_size is mapped to projection rectangle.
   static void ConfigureProjection(
-      Jni jni, jobject display_token, ANativeWindow* surface, const DisplayInfo& display_info, Size projected_size);
+      Jni jni, jobject display_token, ANativeWindow* surface, const DisplayInfo& display_info, ARect projection_rect);
 
 private:
   // SurfaceControl class.
diff --git a/streaming/screen-sharing-agent/app/src/main/cpp/display_streamer.cc b/streaming/screen-sharing-agent/app/src/main/cpp/display_streamer.cc
index 898ee9b..1cea876 100644
--- a/streaming/screen-sharing-agent/app/src/main/cpp/display_streamer.cc
+++ b/streaming/screen-sharing-agent/app/src/main/cpp/display_streamer.cc
@@ -53,7 +53,7 @@
 namespace {
 
 constexpr int MAX_SUBSEQUENT_ERRORS = 10;
-constexpr double MIN_VIDEO_RESOLUTION = 128;
+constexpr int MIN_VIDEO_RESOLUTION = 128;
 constexpr int COLOR_FormatSurface = 0x7F000789;  // See android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface.
 constexpr int BIT_RATE = 8000000;
 constexpr int BIT_RATE_REDUCED = 1000000;
@@ -185,12 +185,19 @@
 }
 
 Size ComputeVideoSize(Size rotated_display_size, Size max_resolution, Size size_alignment) {
-  auto width = rotated_display_size.width;
-  auto height = rotated_display_size.height;
-  double scale = max(min(1.0, min(static_cast<double>(max_resolution.width) / width, static_cast<double>(max_resolution.height) / height)),
-                     max(MIN_VIDEO_RESOLUTION / width, MIN_VIDEO_RESOLUTION / height));
-  return Size { RoundUpToMultipleOf(lround(width * scale), max(size_alignment.width, 8)),
-                RoundUpToMultipleOf(lround(height * scale), max(size_alignment.height, 8)) };
+  double display_width = rotated_display_size.width;
+  double display_height = rotated_display_size.height;
+  if (size_alignment.width < 8) {
+    size_alignment.width = 8;  // Increase horizontal size alignment to accommodate FFmpeg video decoder.
+  }
+  double scale = max(min(1.0, min(max_resolution.width / display_width, max_resolution.height / display_height)),
+                     max(MIN_VIDEO_RESOLUTION / display_width, MIN_VIDEO_RESOLUTION / display_height));
+  // We are computing width of the frame first and the height based on the width to make sure that,
+  // if the video frame has a sightly different aspect ration than the display, it is taller rather
+  // than wider.
+  int32_t width = RoundUpToMultipleOf(lround(display_width * scale), size_alignment.width);
+  int32_t height = RoundUpToMultipleOf(lround(width * display_height / display_width), size_alignment.height);
+  return Size { width, height };
 }
 
 Size ConfigureCodec(AMediaCodec* codec, const CodecInfo& codec_info, Size max_video_resolution, AMediaFormat* media_format,
@@ -329,7 +336,9 @@
         virtual_display.Resize(video_size.width, video_size.height, display_info_.logical_density_dpi);
         virtual_display.SetSurface(surface);
       } else {
-        SurfaceControl::ConfigureProjection(jni, display_token, surface, display_info, video_size);
+        int32_t height = lround(static_cast<double>(video_size.width) * display_info.logical_size.height / display_info.logical_size.width);
+        int32_t y = (video_size.height - height) / 2;
+        SurfaceControl::ConfigureProjection(jni, display_token, surface, display_info, { 0, y, video_size.width, height });
       }
       AMediaCodec_start(codec);
       running_codec_ = codec;
diff --git a/streaming/screen-sharing-agent/app/src/main/java/com/android/tools/screensharing/ClipboardAdapter.java b/streaming/screen-sharing-agent/app/src/main/java/com/android/tools/screensharing/ClipboardAdapter.java
index 86f022d..b294a82 100644
--- a/streaming/screen-sharing-agent/app/src/main/java/com/android/tools/screensharing/ClipboardAdapter.java
+++ b/streaming/screen-sharing-agent/app/src/main/java/com/android/tools/screensharing/ClipboardAdapter.java
@@ -30,7 +30,7 @@
 @SuppressWarnings("unused") // Called through JNI.
 public class ClipboardAdapter {
   private static final String PACKAGE_NAME = "com.android.shell";
-  private static final String ATTRIBUTION_TAG = "ScreenSharing";
+  private static final String ATTRIBUTION_TAG = "studio.screen.sharing";
   private static final int USER_ID = 0;
   private static final int DEVICE_ID_DEFAULT = 0; // From android.companion.virtual.VirtualDeviceManager
 
@@ -40,7 +40,6 @@
   private static Method addPrimaryClipChangedListenerMethod;
   private static Method removePrimaryClipChangedListenerMethod;
   private static ClipboardListener clipboardListener;
-  private static int numberOfExtraParameters;
   private static PersistableBundle overlaySuppressor;
 
   static {
@@ -54,8 +53,10 @@
         setPrimaryClipMethod = findMethodAndMakeAccessible(methods, "setPrimaryClip");
         addPrimaryClipChangedListenerMethod = findMethodAndMakeAccessible(methods, "addPrimaryClipChangedListener");
         removePrimaryClipChangedListenerMethod = findMethodAndMakeAccessible(methods, "removePrimaryClipChangedListener");
-        numberOfExtraParameters = getPrimaryClipMethod.getParameterCount() - 1;
-        if (numberOfExtraParameters <= 3) {
+        if (checkNumberOfParameters(getPrimaryClipMethod, 1, 4) &&
+            checkNumberOfParameters(setPrimaryClipMethod, 2, 5) &&
+            checkNumberOfParameters(addPrimaryClipChangedListenerMethod, 2, 5) &&
+            checkNumberOfParameters(removePrimaryClipChangedListenerMethod, 2, 5)) {
           clipboardListener = new ClipboardListener();
           if (SDK_INT >= 33) {
             overlaySuppressor = new PersistableBundle(1);
@@ -63,12 +64,12 @@
           }
         }
         else {
-          Log.e("ScreenSharing", "Unexpected number of getPrimaryClip parameters: " + (numberOfExtraParameters + 1));
+          clipboard = null;
         }
       }
     }
     catch (NoSuchMethodException e) {
-      Log.e("ScreenSharing", e.getMessage());
+      Log.e(ATTRIBUTION_TAG, e.getMessage());
       clipboard = null;
     }
   }
@@ -78,13 +79,14 @@
       return "";
     }
 
-    ClipData clipData = numberOfExtraParameters == 0 ?
+    int numberOfParameters = getPrimaryClipMethod.getParameterCount();
+    ClipData clipData = numberOfParameters == 1 ?
                         (ClipData)getPrimaryClipMethod.invoke(clipboard, PACKAGE_NAME) :
-                        numberOfExtraParameters == 1 ?
+                        numberOfParameters == 2 ?
                         (ClipData)getPrimaryClipMethod.invoke(clipboard, PACKAGE_NAME, USER_ID) :
-                        numberOfExtraParameters == 2 ?
+                        numberOfParameters == 3 ?
                         (ClipData)getPrimaryClipMethod.invoke(clipboard, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID) :
-                        numberOfExtraParameters == 3 ?
+                        numberOfParameters == 4 ?
                         (ClipData)getPrimaryClipMethod.invoke(clipboard, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID, DEVICE_ID_DEFAULT) :
                         null;
     if (clipData == null || clipData.getItemCount() == 0) {
@@ -104,16 +106,17 @@
       clipData.getDescription().setExtras(overlaySuppressor);
     }
 
-    if (numberOfExtraParameters == 0) {
+    int numberOfParameters = setPrimaryClipMethod.getParameterCount();
+    if (numberOfParameters == 2) {
       setPrimaryClipMethod.invoke(clipboard, clipData, PACKAGE_NAME);
     }
-    else if (numberOfExtraParameters == 1) {
+    else if (numberOfParameters == 3) {
       setPrimaryClipMethod.invoke(clipboard, clipData, PACKAGE_NAME, USER_ID);
     }
-    else if (numberOfExtraParameters == 2) {
+    else if (numberOfParameters == 4) {
       setPrimaryClipMethod.invoke(clipboard, clipData, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID);
     }
-    else if (numberOfExtraParameters == 3) {
+    else if (numberOfParameters == 5) {
       setPrimaryClipMethod.invoke(clipboard, clipData, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID, DEVICE_ID_DEFAULT);
     }
   }
@@ -123,16 +126,17 @@
       return;
     }
 
-    if (numberOfExtraParameters == 0) {
+    int numberOfParameters = addPrimaryClipChangedListenerMethod.getParameterCount();
+    if (numberOfParameters == 2) {
       addPrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME);
     }
-    else if (numberOfExtraParameters == 1) {
+    else if (numberOfParameters == 3) {
       addPrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, USER_ID);
     }
-    else if (numberOfExtraParameters == 2) {
+    else if (numberOfParameters == 4) {
       addPrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID);
     }
-    else if (numberOfExtraParameters == 3) {
+    else if (numberOfParameters == 5) {
       addPrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID, DEVICE_ID_DEFAULT);
     }
   }
@@ -142,16 +146,17 @@
       return;
     }
 
-    if (numberOfExtraParameters == 0) {
+    int numberOfParameters = removePrimaryClipChangedListenerMethod.getParameterCount();
+    if (numberOfParameters == 2) {
       removePrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME);
     }
-    else if (numberOfExtraParameters == 1) {
+    else if (numberOfParameters == 3) {
       removePrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, USER_ID);
     }
-    else if (numberOfExtraParameters == 2) {
+    else if (numberOfParameters == 4) {
       removePrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID);
     }
-    else if (numberOfExtraParameters == 3) {
+    else if (numberOfParameters == 5) {
       removePrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID,
                                                     DEVICE_ID_DEFAULT);
     }
@@ -166,4 +171,13 @@
     }
     throw new NoSuchMethodException(name);
   }
+
+  private static boolean checkNumberOfParameters(Method method, int minParam, int maxParam) {
+    int parameterCount = method.getParameterCount();
+    if (minParam <= parameterCount && parameterCount <= maxParam) {
+      return true;
+    }
+    Log.e(ATTRIBUTION_TAG, "Unexpected number of " + method.getName() + " parameters: " + parameterCount);
+    return false;
+  }
 }
diff --git a/streaming/screen-sharing-agent/build.gradle b/streaming/screen-sharing-agent/build.gradle
index 04bc67d..1656642 100644
--- a/streaming/screen-sharing-agent/build.gradle
+++ b/streaming/screen-sharing-agent/build.gradle
@@ -5,7 +5,7 @@
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:7.4.1'
+        classpath 'com.android.tools.build:gradle:8.0.2'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
diff --git a/streaming/screen-sharing-agent/gradle.properties b/streaming/screen-sharing-agent/gradle.properties
index 8890a43..56f70c0 100644
--- a/streaming/screen-sharing-agent/gradle.properties
+++ b/streaming/screen-sharing-agent/gradle.properties
@@ -16,3 +16,6 @@
 # https://developer.android.com/topic/libraries/support-library/androidx-rn
 android.useAndroidX=true
 org.gradle.unsafe.configuration-cache=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonTransitiveRClass=true
+android.nonFinalResIds=true
diff --git a/streaming/screen-sharing-agent/gradle/wrapper/gradle-wrapper.properties b/streaming/screen-sharing-agent/gradle/wrapper/gradle-wrapper.properties
index 9367b98..4f122b4 100644
--- a/streaming/screen-sharing-agent/gradle/wrapper/gradle-wrapper.properties
+++ b/streaming/screen-sharing-agent/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 #Tue May 10 11:42:43 PDT 2022
 distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
diff --git a/streaming/src/com/android/tools/idea/streaming/core/AbstractDisplayView.kt b/streaming/src/com/android/tools/idea/streaming/core/AbstractDisplayView.kt
index bbd7403..199f9f4 100644
--- a/streaming/src/com/android/tools/idea/streaming/core/AbstractDisplayView.kt
+++ b/streaming/src/com/android/tools/idea/streaming/core/AbstractDisplayView.kt
@@ -57,6 +57,7 @@
  * Common base class for [com.android.tools.idea.streaming.emulator.EmulatorView] and
  * [com.android.tools.idea.streaming.device.DeviceView].
  */
+@Suppress("UseJBColor")
 abstract class AbstractDisplayView(val displayId: Int) : ZoomablePanel(), Disposable {
 
   /** Serial number of the device sown in the view. */
@@ -289,7 +290,7 @@
   }
 
  /** Attempts to restore a lost device connection. */
-  protected inner class Reconnector(val reconnectLabel: String, val progressMessage: String, val reconnect: suspend () -> Unit) {
+  protected inner class Reconnector(val reconnectLabel: String, private val progressMessage: String, val reconnect: suspend () -> Unit) {
 
     /** Starts the reconnection attempt. */
     internal fun start() {
diff --git a/streaming/src/com/android/tools/idea/streaming/core/StreamingToolWindowManager.kt b/streaming/src/com/android/tools/idea/streaming/core/StreamingToolWindowManager.kt
index 2d9edc5..93bd7cb 100644
--- a/streaming/src/com/android/tools/idea/streaming/core/StreamingToolWindowManager.kt
+++ b/streaming/src/com/android/tools/idea/streaming/core/StreamingToolWindowManager.kt
@@ -1028,7 +1028,7 @@
 
   private inner class StartDeviceMirroringAction(
     private val device: DeviceDescription,
-  ) : DumbAwareAction(device.deviceName, null, device.config.deviceProperties.deviceType?.icon) {
+  ) : DumbAwareAction(device.deviceName, null, device.config.deviceProperties.icon) {
 
     override fun actionPerformed(event: AnActionEvent) {
       activateMirroring(device)
@@ -1039,7 +1039,7 @@
 
   private inner class StartRemoteDeviceAction(
     private val device: DeviceHandle,
-  ) : DumbAwareAction(device.sourceTemplate?.properties?.composeDeviceName(), null, device.sourceTemplate?.properties?.deviceType?.icon) {
+  ) : DumbAwareAction(device.sourceTemplate?.properties?.composeDeviceName(), null, device.sourceTemplate?.properties?.icon) {
 
     override fun actionPerformed(event: AnActionEvent) {
       device.scope.launch { device.activationAction?.activate() }
diff --git a/streaming/src/com/android/tools/idea/streaming/core/StreamingUtils.kt b/streaming/src/com/android/tools/idea/streaming/core/StreamingUtils.kt
index ae717c1..156d90f 100644
--- a/streaming/src/com/android/tools/idea/streaming/core/StreamingUtils.kt
+++ b/streaming/src/com/android/tools/idea/streaming/core/StreamingUtils.kt
@@ -118,7 +118,7 @@
 private fun Component.isComponentForAction(action: AnAction): Boolean =
     this is AnActionHolder && this.action === action
 
-
+// TODO(b/289230363): use DeviceHandle.state.properties.icon, since it is the source of truth for device icons.
 internal val AvdInfo.icon: Icon
   get() {
     return when (tag) {
@@ -129,16 +129,6 @@
     }
   }
 
-internal val DeviceType.icon: Icon
-  get() {
-    return when (this) {
-      DeviceType.TV -> StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_TV
-      DeviceType.AUTOMOTIVE -> StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_CAR
-      DeviceType.WEAR -> StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_WEAR
-      DeviceType.HANDHELD -> StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_PHONE
-    }
-  }
-
 /**
  * If the device is connected by WiFi, extracts the device's own serial number from the serial
  * number returned by ADB, otherwise returns the original serial number. For example, converts
diff --git a/streaming/src/com/android/tools/idea/streaming/device/DeviceToolWindowPanel.kt b/streaming/src/com/android/tools/idea/streaming/device/DeviceToolWindowPanel.kt
index c370e2b..7e9ac29 100644
--- a/streaming/src/com/android/tools/idea/streaming/device/DeviceToolWindowPanel.kt
+++ b/streaming/src/com/android/tools/idea/streaming/device/DeviceToolWindowPanel.kt
@@ -24,7 +24,6 @@
 import com.android.tools.idea.streaming.core.DeviceId
 import com.android.tools.idea.streaming.core.RunningDevicePanel
 import com.android.tools.idea.streaming.core.STREAMING_SECONDARY_TOOLBAR_ID
-import com.android.tools.idea.streaming.core.icon
 import com.android.tools.idea.streaming.core.installFileDropHandler
 import com.android.tools.idea.streaming.device.DeviceView.ConnectionState
 import com.android.tools.idea.streaming.device.DeviceView.ConnectionStateListener
@@ -37,7 +36,6 @@
 import com.intellij.openapi.Disposable
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.util.Disposer
-import icons.StudioIcons
 import java.awt.EventQueue
 import javax.swing.Icon
 import javax.swing.JComponent
@@ -57,10 +55,7 @@
     get() = deviceClient.deviceName
 
   override val icon: Icon
-    get() {
-      val icon = deviceConfig.deviceProperties.deviceType?.icon ?: StudioIcons.DeviceExplorer.PHYSICAL_DEVICE_PHONE
-      return ExecutionUtil.getLiveIndicator(icon)
-    }
+    get() = ExecutionUtil.getLiveIndicator(deviceClient.deviceHandle.state.properties.icon)
 
   override val isClosable: Boolean = StudioFlags.DEVICE_MIRRORING_ADVANCED_TAB_CONTROL.get()
 
diff --git a/streaming/src/com/android/tools/idea/streaming/device/DeviceView.kt b/streaming/src/com/android/tools/idea/streaming/device/DeviceView.kt
index 77d22ce..3c01587 100644
--- a/streaming/src/com/android/tools/idea/streaming/device/DeviceView.kt
+++ b/streaming/src/com/android/tools/idea/streaming/device/DeviceView.kt
@@ -264,6 +264,7 @@
   }
 
   private fun connected() {
+    hideLongRunningOperationIndicatorInstantly()
     if (connectionState == ConnectionState.CONNECTING) {
       hideDisconnectedStateMessage()
       connectionState = ConnectionState.CONNECTED
@@ -276,6 +277,7 @@
       if (disposed) {
         return@invokeLaterIfNeeded
       }
+      hideLongRunningOperationIndicatorInstantly()
       stopClipboardSynchronization()
       val message: String
       val reconnector: Reconnector
diff --git a/streaming/src/com/android/tools/idea/streaming/device/VideoDecoder.kt b/streaming/src/com/android/tools/idea/streaming/device/VideoDecoder.kt
index dc52643..e15612c 100644
--- a/streaming/src/com/android/tools/idea/streaming/device/VideoDecoder.kt
+++ b/streaming/src/com/android/tools/idea/streaming/device/VideoDecoder.kt
@@ -19,7 +19,8 @@
 import com.android.annotations.concurrency.GuardedBy
 import com.android.tools.adtui.ImageUtils
 import com.android.tools.adtui.ImageUtils.ellipticalClip
-import com.android.tools.idea.streaming.core.coerceAtMost
+import com.android.tools.idea.streaming.core.rotatedByQuadrants
+import com.android.tools.idea.streaming.core.scaled
 import com.intellij.openapi.diagnostic.debug
 import com.intellij.openapi.diagnostic.thisLogger
 import com.intellij.util.containers.ContainerUtil
@@ -173,16 +174,6 @@
     private val pendingPacket: AVPacket = av_packet_alloc()
     private var hasPendingPacket = false
 
-    private val renderingSize: Dimension
-      get() {
-        val videoSize = Dimension(decodingFrame.width(), decodingFrame.height())
-        val maximumSize = maxOutputSize
-        if (maximumSize.width == 0 || maximumSize.height == 0) {
-          return videoSize
-        }
-        return videoSize.coerceAtMost(maximumSize)
-      }
-
     init {
       thisLogger().debug { "Receiving $codecName video stream" }
       val ffmpegCodecName = when (codecName) {
@@ -320,11 +311,12 @@
         throw VideoDecoderException("Could not receive video frame")
       }
 
-      val size = renderingSize
+      val frameWidth = decodingFrame.width()
+      val frameHeight = decodingFrame.height()
       var renderingFrame = renderingFrame
-      if (renderingFrame == null || renderingFrame.width() != size.width || renderingFrame.height() != size.height) {
+      if (renderingFrame == null || renderingFrame.width() != frameWidth || renderingFrame.height() != frameHeight) {
         renderingFrame?.let { av_frame_free(it) }
-        renderingFrame = createRenderingFrame(size).also { this.renderingFrame = it }
+        renderingFrame = createRenderingFrame(frameWidth, frameHeight).also { this.renderingFrame = it }
         if (av_frame_get_buffer(renderingFrame, 4) < 0) {
           throw RuntimeException("av_frame_get_buffer failed")
         }
@@ -333,25 +325,30 @@
         throw RuntimeException("av_frame_make_writable failed")
       }
 
-      sws_scale(getSwsContext(renderingFrame), decodingFrame.data(), decodingFrame.linesize(), 0, decodingFrame.height(),
+      sws_scale(getSwsContext(renderingFrame), decodingFrame.data(), decodingFrame.linesize(), 0, frameHeight,
                 renderingFrame.data(), renderingFrame.linesize())
 
-      val numBytes = av_image_get_buffer_size(renderingFrame.format(), renderingFrame.width(), renderingFrame.height(), 1)
+      val numBytes = av_image_get_buffer_size(renderingFrame.format(), frameWidth, frameHeight, 1)
       val framePixels = renderingFrame.data().get().asByteBufferOfSize(numBytes).asIntBuffer()
-      val imageSize = Dimension(renderingFrame.width(), renderingFrame.height())
+      // Due to video size alignment requirements, the video frame may contain black strips at the top and at the bottom.
+      // These black strips have to be excluded from the rendered image.
+      val rotatedDisplaySize = header.displaySize.rotatedByQuadrants(header.displayOrientation - header.displayOrientationCorrection)
+      val imageHeight = frameWidth.scaled(rotatedDisplaySize.height.toDouble() / rotatedDisplaySize.width).coerceAtMost(frameHeight)
+      val startY = (frameHeight - imageHeight) / 2
+      framePixels.position(startY * frameWidth) // Skip the potential black strip at the top of the frame.
 
       synchronized(imageLock) {
         var image = displayFrame?.image
-        if (image?.width == imageSize.width && image.height == imageSize.height &&
+        if (image?.width == frameWidth && image.height == imageHeight &&
             displayFrame?.orientationCorrection == 0 && header.displayOrientationCorrection == 0 && !header.displayRound) {
           val imagePixels = (image.raster.dataBuffer as DataBufferInt).data
-          framePixels.get(imagePixels)
+          framePixels.get(imagePixels, 0, imageHeight * frameWidth)
         }
         else {
-          val imagePixels = IntArray(imageSize.width * imageSize.height)
-          framePixels.get(imagePixels)
+          val imagePixels = IntArray(frameWidth * imageHeight)
+          framePixels.get(imagePixels, 0, imageHeight * frameWidth)
           val buffer = DataBufferInt(imagePixels, imagePixels.size)
-          val sampleModel = SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, imageSize.width, imageSize.height, SAMPLE_MODEL_BIT_MASKS)
+          val sampleModel = SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, frameWidth, imageHeight, SAMPLE_MODEL_BIT_MASKS)
           val raster = Raster.createWritableRaster(sampleModel, buffer, ZERO_POINT)
           image = ImageUtils.rotateByQuadrants(BufferedImage(COLOR_MODEL, raster, false, null), header.displayOrientationCorrection)
           if (header.displayRound) {
@@ -370,15 +367,15 @@
       val context = sws_getCachedContext(swsContext, decodingFrame.width(), decodingFrame.height(), decodingFrame.format(),
                                          renderingFrame.width(), renderingFrame.height(), renderingFrame.format(),
                                          SWS_BILINEAR, null, null, null as DoublePointer?) ?:
-             throw VideoDecoderException("Could not allocate SwsContext")
+          throw VideoDecoderException("Could not allocate SwsContext")
       swsContext = context
       return context
     }
 
-    private fun createRenderingFrame(size: Dimension): AVFrame {
+    private fun createRenderingFrame(width: Int, height: Int): AVFrame {
       return av_frame_alloc().apply {
-        width(size.width)
-        height(size.height)
+        width(width)
+        height(height)
         format(AV_PIX_FMT_BGRA)
       }
     }
diff --git a/streaming/testData/DeviceToolWindowPanelTest/golden/AppearanceAndToolbarActions1.png b/streaming/testData/DeviceToolWindowPanelTest/golden/AppearanceAndToolbarActions1.png
index 0254bf7..9e01200 100644
--- a/streaming/testData/DeviceToolWindowPanelTest/golden/AppearanceAndToolbarActions1.png
+++ b/streaming/testData/DeviceToolWindowPanelTest/golden/AppearanceAndToolbarActions1.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/MultiTouch1.png b/streaming/testData/DeviceViewTest/golden/MultiTouch1.png
index 2df0903..61ec020 100644
--- a/streaming/testData/DeviceViewTest/golden/MultiTouch1.png
+++ b/streaming/testData/DeviceViewTest/golden/MultiTouch1.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/MultiTouch2.png b/streaming/testData/DeviceViewTest/golden/MultiTouch2.png
index 9509cfc..bbe7e06 100644
--- a/streaming/testData/DeviceViewTest/golden/MultiTouch2.png
+++ b/streaming/testData/DeviceViewTest/golden/MultiTouch2.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/MultiTouch3.png b/streaming/testData/DeviceViewTest/golden/MultiTouch3.png
index 1b1fc68..7250e44 100644
--- a/streaming/testData/DeviceViewTest/golden/MultiTouch3.png
+++ b/streaming/testData/DeviceViewTest/golden/MultiTouch3.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/MultiTouch4.png b/streaming/testData/DeviceViewTest/golden/MultiTouch4.png
index 10e5da1..5c808fc 100644
--- a/streaming/testData/DeviceViewTest/golden/MultiTouch4.png
+++ b/streaming/testData/DeviceViewTest/golden/MultiTouch4.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/Rotation0.png b/streaming/testData/DeviceViewTest/golden/Rotation0.png
index 5afe285..7c1f743 100644
--- a/streaming/testData/DeviceViewTest/golden/Rotation0.png
+++ b/streaming/testData/DeviceViewTest/golden/Rotation0.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/Rotation180.png b/streaming/testData/DeviceViewTest/golden/Rotation180.png
index 1160d2f..6398d25 100644
--- a/streaming/testData/DeviceViewTest/golden/Rotation180.png
+++ b/streaming/testData/DeviceViewTest/golden/Rotation180.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/Rotation270.png b/streaming/testData/DeviceViewTest/golden/Rotation270.png
index 7862be9..1e613de 100644
--- a/streaming/testData/DeviceViewTest/golden/Rotation270.png
+++ b/streaming/testData/DeviceViewTest/golden/Rotation270.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/Rotation90.png b/streaming/testData/DeviceViewTest/golden/Rotation90.png
index 082e208..c067939 100644
--- a/streaming/testData/DeviceViewTest/golden/Rotation90.png
+++ b/streaming/testData/DeviceViewTest/golden/Rotation90.png
Binary files differ
diff --git a/streaming/testData/DeviceViewTest/golden/Zoom1.png b/streaming/testData/DeviceViewTest/golden/Zoom1.png
index ed7390f..5350f18 100644
--- a/streaming/testData/DeviceViewTest/golden/Zoom1.png
+++ b/streaming/testData/DeviceViewTest/golden/Zoom1.png
Binary files differ
diff --git a/streaming/testSrc/com/android/tools/idea/streaming/core/StreamingToolWindowManagerTest.kt b/streaming/testSrc/com/android/tools/idea/streaming/core/StreamingToolWindowManagerTest.kt
index 0fc58e6..d28c13d 100644
--- a/streaming/testSrc/com/android/tools/idea/streaming/core/StreamingToolWindowManagerTest.kt
+++ b/streaming/testSrc/com/android/tools/idea/streaming/core/StreamingToolWindowManagerTest.kt
@@ -19,6 +19,13 @@
 import com.android.emulator.control.KeyboardEvent
 import com.android.emulator.control.PaneEntry
 import com.android.emulator.control.PaneEntry.PaneIndex
+import com.android.sdklib.deviceprovisioner.DeviceProperties
+import com.android.sdklib.deviceprovisioner.DeviceState
+import com.android.sdklib.deviceprovisioner.DeviceTemplate
+import com.android.sdklib.deviceprovisioner.Reservation
+import com.android.sdklib.deviceprovisioner.ReservationState
+import com.android.sdklib.deviceprovisioner.TemplateActivationAction
+import com.android.sdklib.deviceprovisioner.testing.DeviceProvisionerRule
 import com.android.sdklib.internal.avd.AvdInfo
 import com.android.testutils.MockitoKt.mock
 import com.android.testutils.MockitoKt.whenever
@@ -71,10 +78,8 @@
 import com.intellij.openapi.actionSystem.PlatformDataKeys
 import com.intellij.openapi.application.ApplicationManager
 import com.intellij.openapi.components.service
-import com.intellij.openapi.project.Project
 import com.intellij.openapi.util.Disposer
 import com.intellij.openapi.wm.ToolWindow
-import com.intellij.openapi.wm.ToolWindowFactory
 import com.intellij.openapi.wm.ToolWindowManager
 import com.intellij.openapi.wm.ToolWindowType
 import com.intellij.openapi.wm.ex.ToolWindowManagerListener
@@ -113,9 +118,10 @@
   private val emulatorRule = FakeEmulatorRule()
   private val androidExecutorsRule = AndroidExecutorsRule(workerThreadExecutor = Executors.newCachedThreadPool())
   private val popupRule = JBPopupRule()
+  private val provisionerRule = DeviceProvisionerRule()
   @get:Rule
-  val ruleChain = RuleChain(agentRule, emulatorRule, ClipboardSynchronizationDisablementRule(), androidExecutorsRule, EdtRule(),
-                            PortableUiFontRule(), HeadlessDialogRule(), popupRule)
+  val ruleChain = RuleChain(agentRule, provisionerRule, emulatorRule, ClipboardSynchronizationDisablementRule(), androidExecutorsRule,
+                            EdtRule(), PortableUiFontRule(), HeadlessDialogRule(), popupRule)
 
   private val windowFactory: StreamingToolWindowFactory by lazy { StreamingToolWindowFactory() }
   private var nullableToolWindow: TestToolWindow? = null
@@ -375,6 +381,38 @@
   }
 
   @Test
+  fun testRemoteDevice() {
+    val properties = DeviceProperties.build {
+      icon = StudioIcons.DeviceExplorer.FIREBASE_DEVICE_CAR
+      model = "Pixel 9000"
+    }
+    val device = provisionerRule.deviceProvisionerPlugin.newDevice(properties = properties)
+    device.sourceTemplate = object: DeviceTemplate {
+      override val properties = properties
+      override val activationAction: TemplateActivationAction = mock()
+      override val editAction = null
+    }
+    device.stateFlow.value = DeviceState.Disconnected(
+      properties, false, "offline", Reservation(ReservationState.ACTIVE, "active", null, null))
+    provisionerRule.deviceProvisionerPlugin.addDevice(device)
+
+    val provisionerService: DeviceProvisionerService = mock()
+    whenever(provisionerService.deviceProvisioner).thenReturn(provisionerRule.deviceProvisioner)
+    project.replaceService(DeviceProvisionerService::class.java, provisionerService, agentRule.disposable)
+
+    toolWindow.show()
+    waitForCondition(2, TimeUnit.SECONDS) { toolWindow.tabActions.isNotEmpty() }
+    val newTabAction = toolWindow.tabActions[0]
+    newTabAction.actionPerformed(createTestEvent(toolWindow.component, project))
+    val popup: FakeListPopup<Any> = popupRule.fakePopupFactory.getNextPopup(2, TimeUnit.SECONDS)
+
+    assertThat(popup.actions.toString()).isEqualTo(
+      "[Separator (Remote Devices), Pixel 9000 (null), Separator (null), " +
+        "Pair Devices Using Wi-Fi (Open the Device Pairing dialog which allows connecting devices over Wi-Fi)]")
+    assertThat(popup.actions[1].templatePresentation.icon).isEqualTo(StudioIcons.DeviceExplorer.FIREBASE_DEVICE_CAR)
+  }
+
+  @Test
   fun testPhysicalDeviceActivateOnConnection() {
     if (!isFFmpegAvailableToTest()) {
       return
diff --git a/streaming/testUtil/com/android/tools/idea/streaming/device/FakeScreenSharingAgent.kt b/streaming/testUtil/com/android/tools/idea/streaming/device/FakeScreenSharingAgent.kt
index dde0e38..ab6f069 100644
--- a/streaming/testUtil/com/android/tools/idea/streaming/device/FakeScreenSharingAgent.kt
+++ b/streaming/testUtil/com/android/tools/idea/streaming/device/FakeScreenSharingAgent.kt
@@ -482,7 +482,8 @@
         return
       }
 
-      val size = getScaledAndRotatedDisplaySize()
+      val size = computeDisplayImageSize()
+      val videoSize = Dimension(size.width, size.height.roundUpToMultipleOf8())
       val encoderContext = avcodec_alloc_context3(encoder)?.apply {
         bit_rate(8000000L)
         time_base(av_make_q(1, 1000))
@@ -490,8 +491,8 @@
         gop_size(2)
         max_b_frames(1)
         pix_fmt(encoder.pix_fmts().get())
-        width(size.width)
-        height(size.height)
+        width(videoSize.width)
+        height(videoSize.height)
       } ?: throw RuntimeException("Could not allocate encoder context")
 
       if (avcodec_open2(encoderContext, encoder, null as AVDictionary?) < 0) {
@@ -499,8 +500,8 @@
       }
       val encodingFrame = av_frame_alloc().apply {
         format(encoderContext.pix_fmt())
-        width(size.width)
-        height(size.height)
+        width(videoSize.width)
+        height(videoSize.height)
       }
       if (av_frame_get_buffer(encodingFrame, 0) < 0) {
         throw RuntimeException("av_frame_get_buffer failed")
@@ -510,12 +511,12 @@
       }
 
       val image = drawDisplayImage(size.rotatedByQuadrants(-displayOrientation), imageFlavor, displayId)
-        .rotatedByQuadrants(displayOrientation)
+          .rotatedByQuadrants(displayOrientation)
 
       val rgbFrame = av_frame_alloc().apply {
         format(AV_PIX_FMT_BGR24)
-        width(size.width)
-        height(size.height)
+        width(videoSize.width)
+        height(videoSize.height)
       }
       if (av_frame_get_buffer(rgbFrame, 1) < 0) {
         throw RuntimeException("Could not allocate the video frame data")
@@ -524,7 +525,13 @@
       // Copy the image to the frame with conversion to the destination format.
       val dataBufferByte = image.raster.dataBuffer as DataBufferByte
       val numBytes = av_image_get_buffer_size(rgbFrame.format(), rgbFrame.width(), rgbFrame.height(), 1)
-      rgbFrame.data(0).asByteBufferOfSize(numBytes).put(dataBufferByte.data)
+      val byteBuffer = rgbFrame.data(0).asByteBufferOfSize(numBytes)
+      val y = (videoSize.height - size.height) / 2
+      // Fill the extra strip at the top with black three bytes per pixel.
+      byteBuffer.fill(0.toByte(), y * rgbFrame.width() * 3)
+      byteBuffer.put(dataBufferByte.data)
+      // Fill the extra strip at the bottom with black three bytes per pixel.
+      byteBuffer.fill(0.toByte(), (videoSize.height - y - size.height) * rgbFrame.width() * 3)
       val swsContext = sws_getContext(rgbFrame.width(), rgbFrame.height(), rgbFrame.format(),
                                       encodingFrame.width(), encodingFrame.height(), encodingFrame.format(),
                                       SWS_BICUBIC, null, null, null as DoublePointer?)!!
@@ -611,15 +618,18 @@
       }
     }
 
-    private fun getScaledAndRotatedDisplaySize(): Dimension {
+    private fun computeDisplayImageSize(): Dimension {
+      // The same logic as in ComputeVideoSize in display_streamer.cc except for rounding of height.
       val rotatedDisplaySize = getFoldedDisplaySize().rotatedByQuadrants(displayOrientation)
-      val width = rotatedDisplaySize.width
-      val height = rotatedDisplaySize.height
+      val displayWidth = rotatedDisplaySize.width.toDouble()
+      val displayHeight = rotatedDisplaySize.height.toDouble()
       val maxResolutionWidth = maxVideoResolution.width.coerceAtMost(maxVideoEncoderResolution)
       val maxResolutionHeight = maxVideoResolution.height.coerceAtMost(maxVideoEncoderResolution)
-      val scale = max(min(1.0, min(maxResolutionWidth.toDouble() / width, maxResolutionHeight.toDouble() / height)),
-                      max(MIN_VIDEO_RESOLUTION / width, MIN_VIDEO_RESOLUTION / height))
-      return Dimension((width * scale).roundToInt().roundUpToMultipleOf8(), (height * scale).roundToInt().roundUpToMultipleOf8())
+      val scale = max(min(1.0, min(maxResolutionWidth / displayWidth, maxResolutionHeight / displayHeight)),
+                      max(MIN_VIDEO_RESOLUTION / displayWidth, MIN_VIDEO_RESOLUTION / displayHeight))
+      val width = (displayWidth * scale).roundToInt().roundUpToMultipleOf8()
+      val height = (width * displayHeight / displayWidth).roundToInt()
+      return Dimension(width, height)
     }
 
     private fun getFoldedDisplaySize(): Dimension {
@@ -802,6 +812,12 @@
   return false
 }
 
+private fun ByteBuffer.fill(b: Byte, count: Int) {
+  for (i in 0 until count) {
+    put(b)
+  }
+}
+
 private class ColorScheme(val start1: Color, val end1: Color, val start2: Color, val end2: Color)
 
 private val COLOR_SCHEMES = listOf(ColorScheme(Color(236, 112, 99), Color(250, 219, 216), Color(212, 230, 241), Color(84, 153, 199)),
diff --git a/studio/version.bzl b/studio/version.bzl
index e46945e..831f1cb 100644
--- a/studio/version.bzl
+++ b/studio/version.bzl
@@ -3,5 +3,5 @@
 
 STUDIO_CODENAME = "Hedgehog"
 STUDIO_VERSION = "Canary"
-STUDIO_MICRO_PATCH = "1.10"
-STUDIO_RELEASE_NUMBER = 10
+STUDIO_MICRO_PATCH = "1.11"
+STUDIO_RELEASE_NUMBER = 11