Snap for 13264465 from 5d53e4c17d94c8b173d872587e7334b7b5529308 to 25Q2-release

Change-Id: I4607d199827681af2ea577294e6ed32f077f91ea
diff --git a/samples/VirtualDeviceManager/host/AndroidManifest.xml b/samples/VirtualDeviceManager/host/AndroidManifest.xml
index 33b881f..80a9da8 100644
--- a/samples/VirtualDeviceManager/host/AndroidManifest.xml
+++ b/samples/VirtualDeviceManager/host/AndroidManifest.xml
@@ -80,6 +80,7 @@
         <activity
             android:name=".DisplayActivity"
             android:label="Virtual Display"
+            android:supportsPictureInPicture="true"
             android:exported="false" />
         <activity
             android:name=".CustomLauncherActivity"
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/CustomLauncherActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/CustomLauncherActivity.java
index 9b32785..ab36ec1 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/CustomLauncherActivity.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/CustomLauncherActivity.java
@@ -19,9 +19,13 @@
 import android.app.WallpaperManager;
 import android.content.Intent;
 import android.os.Bundle;
+import android.widget.FrameLayout;
 import android.widget.GridView;
 
 import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
 
 import dagger.hilt.android.AndroidEntryPoint;
 
@@ -49,6 +53,15 @@
                         startActivity(intent);
                     }
                 });
+
+        ViewCompat.setOnApplyWindowInsetsListener(launcher, (v, windowInsets) -> {
+            Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+            lp.topMargin = insets.top;
+            lp.bottomMargin = insets.bottom;
+            v.setLayoutParams(lp);
+            return WindowInsetsCompat.CONSUMED;
+        });
     }
 
     @Override
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayActivity.java
index 42537f4..00fa96e 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayActivity.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayActivity.java
@@ -16,15 +16,23 @@
 
 package com.example.android.vdmdemo.host;
 
+import android.app.PictureInPictureParams;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
 import android.hardware.display.DisplayManager;
+import android.media.Image;
+import android.media.ImageReader;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.util.Log;
+import android.util.Rational;
 import android.view.Display;
 import android.view.InputDevice;
 import android.view.KeyEvent;
@@ -33,8 +41,10 @@
 import android.view.MenuItem;
 import android.view.Surface;
 import android.view.TextureView;
+import android.view.View;
 
 import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
@@ -44,6 +54,8 @@
 
 import dagger.hilt.android.AndroidEntryPoint;
 
+import java.nio.ByteBuffer;
+
 import javax.inject.Inject;
 
 /**
@@ -59,8 +71,13 @@
     // https://developer.android.com/reference/android/util/DisplayMetrics#density
     private static final float DIP_TO_DPI = 160f;
 
+    /** @see android.app.PictureInPictureParams.Builder#setAspectRatio(android.util.Rational) */
+    private static final Rational MAX_PIP_RATIO = new Rational(239, 100);
+    private static final Rational MIN_PIP_RATIO = new Rational(100, 239);
+
     static final String EXTRA_DISPLAY_ID = "displayId";
 
+
     @Inject
     InputController mInputController;
 
@@ -68,6 +85,9 @@
 
     private VdmService mVdmService = null;
     private int mDisplayId;
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
     private Surface mSurface;
     private int mSurfaceWidth;
     private int mSurfaceHeight;
@@ -75,13 +95,40 @@
 
     private RemoteDisplay mDisplay;
     private boolean mPoweredOn = true;
+    private ImageReader mImageReader;
 
     private final ServiceConnection mServiceConnection = new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName className, IBinder binder) {
-            Log.d(TAG, "Connected to VDM Service");
-            mVdmService = ((VdmService.LocalBinder) binder).getService();
-            createDisplay();
+            synchronized (mLock) {
+                Log.d(TAG, "Connected to VDM Service");
+                mVdmService = ((VdmService.LocalBinder) binder).getService();
+                mDisplay = mVdmService.getRemoteDisplay(mDisplayId).orElseGet(() ->
+                        mVdmService.createRemoteDisplay(
+                                DisplayActivity.this, mDisplayId, 200, 200, mDpi, null));
+            }
+            if (isInPictureInPictureMode()) {
+                Log.v(TAG, "Initializing copy from display " + mDisplayId + " to PIP window");
+                mImageReader = ImageReader.newInstance(
+                        mDisplay.getWidth(), mDisplay.getHeight(), PixelFormat.RGBA_8888, 2);
+                mDisplay.setSurface(mImageReader.getSurface());
+                mImageReader.setOnImageAvailableListener((reader) -> {
+                    Image image = reader.acquireLatestImage();
+                    synchronized (mLock) {
+                        if (image != null && mSurface != null) {
+                            copyImageToSurfaceLocked(image);
+                            image.close();
+                        }
+                    }
+                }, null);
+            } else {
+                synchronized (mLock) {
+                    if (mSurface != null) {
+                        resetDisplayLocked();
+                        setPictureInPictureParams(buildPictureInPictureParams());
+                    }
+                }
+            }
         }
 
         @Override
@@ -101,13 +148,16 @@
         mDpi = (int) (getResources().getDisplayMetrics().density * DIP_TO_DPI);
 
         setContentView(R.layout.activity_display);
-        Toolbar toolbar = requireViewById(R.id.main_tool_bar);
-        setSupportActionBar(toolbar);
-        setTitle(getTitle() + " " + mDisplayId);
-        EdgeToEdgeUtils.applyTopInsets(toolbar);
-
         TextureView textureView = requireViewById(R.id.display_surface_view);
-        EdgeToEdgeUtils.applyBottomInsets(textureView);
+        Toolbar toolbar = requireViewById(R.id.main_tool_bar);
+        if (isInPictureInPictureMode()) {
+            toolbar.setVisibility(View.GONE);
+        } else {
+            setSupportActionBar(toolbar);
+            setTitle(getTitle() + " " + mDisplayId);
+            EdgeToEdgeUtils.applyTopInsets(toolbar);
+            EdgeToEdgeUtils.applyBottomInsets(textureView);
+        }
 
         textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
             @Override
@@ -116,11 +166,15 @@
             @Override
             public void onSurfaceTextureAvailable(
                     @NonNull SurfaceTexture texture, int width, int height) {
-                Log.v(TAG, "Setting surface for local display " + mDisplayId);
-                mSurface = new Surface(texture);
-                mSurfaceWidth = width;
-                mSurfaceHeight = height;
-                createDisplay();
+                synchronized (mLock) {
+                    Log.d(TAG, "onSurfaceTextureAvailable for local display " + mDisplayId);
+                    mSurfaceWidth = width;
+                    mSurfaceHeight = height;
+                    mSurface = new Surface(texture);
+                    if (!isInPictureInPictureMode() && mDisplay != null) {
+                        resetDisplayLocked();
+                    }
+                }
             }
 
             @Override
@@ -134,8 +188,12 @@
             public void onSurfaceTextureSizeChanged(
                     @NonNull SurfaceTexture texture, int width, int height) {
                 Log.v(TAG, "onSurfaceTextureSizeChanged for local display " + mDisplayId);
-                if (mDisplay != null) {
-                    mDisplay.reset(width, height, mDpi);
+                synchronized (mLock) {
+                    mSurfaceWidth = width;
+                    mSurfaceHeight = height;
+                    if (!isInPictureInPictureMode() && mDisplay != null) {
+                        resetDisplayLocked();
+                    }
                 }
             }
         });
@@ -193,6 +251,10 @@
     protected void onDestroy() {
         super.onDestroy();
         mDisplayManager.unregisterDisplayListener(this);
+        if (mImageReader != null) {
+            mImageReader.close();
+            mImageReader = null;
+        }
     }
 
     @Override
@@ -209,10 +271,9 @@
                 if (mVdmService != null) {
                     mVdmService.closeRemoteDisplay(mDisplayId);
                 }
-                finish();
                 return true;
             case R.id.pip:
-                // TODO(b/404803361): enter PiP
+                enterPictureInPictureMode(buildPictureInPictureParams());
                 return true;
             case R.id.power:
                 if (mDisplay != null) {
@@ -241,13 +302,64 @@
         return true;
     }
 
-    private synchronized void createDisplay() {
-        if (mVdmService == null || mSurface == null || mDisplay != null) {
-            return;
+    private PictureInPictureParams buildPictureInPictureParams() {
+        Rational ratio = new Rational(mDisplay.getWidth(), mDisplay.getHeight());
+        if (ratio.compareTo(MAX_PIP_RATIO) > 0) {
+            ratio = MAX_PIP_RATIO;
+        } else if (ratio.compareTo(MIN_PIP_RATIO) < 0) {
+            ratio = MIN_PIP_RATIO;
         }
+        Rect rect = new Rect();
+        View textureView = requireViewById(R.id.display_surface_view);
+        textureView.getGlobalVisibleRect(rect);
+        return new PictureInPictureParams.Builder()
+                .setAutoEnterEnabled(true)
+                .setAspectRatio(ratio)
+                .setExpandedAspectRatio(ratio)
+                .setSourceRectHint(rect)
+                .setSeamlessResizeEnabled(false)
+                .build();
+    }
 
-        mDisplay = mVdmService.createRemoteDisplay(
-                this, mDisplayId, mSurfaceWidth, mSurfaceHeight, mDpi, mSurface, null);
+    @GuardedBy("mLock")
+    private void resetDisplayLocked() {
+        if (mDisplay.getWidth() != mSurfaceWidth || mDisplay.getHeight() != mSurfaceHeight) {
+            Log.v(TAG, "Resizing display " + mDisplayId + " to " + mSurfaceWidth
+                    + "/" + mSurfaceHeight);
+            mDisplay.reset(mSurfaceWidth, mSurfaceHeight, mDpi);
+        }
+        mDisplay.setSurface(mSurface);
+    }
+
+    @GuardedBy("mLock")
+    private void copyImageToSurfaceLocked(Image image) {
+        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
+        int pixelStride = image.getPlanes()[0].getPixelStride();
+        int rowStride = image.getPlanes()[0].getRowStride();
+        int pixelBytesPerRow = pixelStride * image.getWidth();
+        int rowPadding = rowStride - pixelBytesPerRow;
+
+        // Remove the row padding bytes from the buffer before converting to a Bitmap
+        ByteBuffer trimmedBuffer = ByteBuffer.allocate(buffer.remaining());
+        buffer.rewind();
+        while (buffer.hasRemaining()) {
+            for (int i = 0; i < pixelBytesPerRow; ++i) {
+                trimmedBuffer.put(buffer.get());
+            }
+            buffer.position(buffer.position() + rowPadding); // Skip the padding bytes
+        }
+        trimmedBuffer.flip(); // Prepare the trimmed buffer for reading
+
+        Canvas canvas = mSurface.lockCanvas(null);
+        Bitmap bitmap =
+                Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
+        bitmap.copyPixelsFromBuffer(trimmedBuffer);
+        Bitmap scaled = Bitmap.createScaledBitmap(bitmap, mSurfaceWidth, mSurfaceHeight, false);
+        // Draw the Bitmap onto the Canvas
+        canvas.drawBitmap(scaled, 0f, 0f, null);
+
+        bitmap.recycle();
+        mSurface.unlockCanvasAndPost(canvas);
     }
 
     @Override
@@ -256,7 +368,7 @@
     @Override
     public void onDisplayRemoved(int displayId) {
         if (mDisplay != null && displayId == mDisplay.getDisplayId()) {
-            finish();
+            finishAndRemoveTask();
         }
     }
 
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
index 56646a6..f060d54 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
@@ -104,7 +104,6 @@
 
     private final Context mContext;
     private final RemoteIo mRemoteIo;
-    private Surface mSurface;
     private final PreferenceController mPreferenceController;
     private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
     private final VirtualDisplay mVirtualDisplay;
@@ -162,12 +161,10 @@
             int height,
             int dpi,
             VirtualDevice virtualDevice,
-            Surface surface,
             RemoteIo remoteIo,
             @DisplayType int displayType,
             PreferenceController preferenceController) {
         mContext = context;
-        mSurface = surface;
         mRemoteIo = remoteIo;
         mRemoteDisplayId = displayId;
         mVirtualDevice = virtualDevice;
@@ -250,7 +247,7 @@
     }
 
     void setSurface(Surface surface) {
-        mSurface = surface;
+        mVirtualDisplay.setSurface(surface);
     }
 
     void reset(int width, int height, int dpi) {
@@ -263,13 +260,11 @@
         if (mVideoManager != null) {
             mVideoManager.stop();
         }
-        if (mSurface == null) {
+        if (mRemoteIo != null) {
             mVideoManager = VideoManager.createDisplayEncoder(mRemoteDisplayId, mRemoteIo,
                     mPreferenceController.getBoolean(R.string.pref_record_encoder_output));
             Surface surface = mVideoManager.createInputSurface(mWidth, mHeight, DISPLAY_FPS);
             mVirtualDisplay.setSurface(surface);
-        } else {
-            mVirtualDisplay.setSurface(mSurface);
         }
 
         mRotation = mVirtualDisplay.getDisplay().getRotation();
@@ -311,9 +306,11 @@
         mHeight = height;
         mDpi = dpi;
 
-        // Video encoder needs round dimensions...
-        mHeight -= mHeight % 10;
-        mWidth -= mWidth % 10;
+        if (mRemoteIo != null) {
+            // Video encoder needs round dimensions...
+            mHeight -= mHeight % 10;
+            mWidth -= mWidth % 10;
+        }
     }
 
     void launchIntent(Intent intent) {
@@ -333,6 +330,14 @@
         return new PointF(mWidth, mHeight);
     }
 
+    int getWidth() {
+        return mWidth;
+    }
+
+    int getHeight() {
+        return mHeight;
+    }
+
     void onDisplayChanged() {
         if (mRotation != mVirtualDisplay.getDisplay().getRotation()) {
             mRotation = mVirtualDisplay.getDisplay().getRotation();
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java
index 52670a8..234a4cd 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java
@@ -62,7 +62,6 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.Display;
-import android.view.Surface;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
@@ -393,7 +392,7 @@
             createRemoteDisplay(this, event.getDisplayId(),
                     event.getDisplayCapabilities().getViewportWidth(),
                     event.getDisplayCapabilities().getViewportHeight(),
-                    event.getDisplayCapabilities().getDensityDpi(), null, mRemoteIo);
+                    event.getDisplayCapabilities().getDensityDpi(), mRemoteIo);
         } else if (event.hasStopStreaming() && !event.getStopStreaming().getPause()) {
             closeRemoteDisplay(event.getDisplayId());
         } else if (event.hasDisplayChangeEvent() && event.getDisplayChangeEvent().getFocused()) {
@@ -615,12 +614,11 @@
 
             if (mDeviceCapabilities.getSensorCapabilitiesCount() > 0) {
                 mRemoteSensorManager = new RemoteSensorManager(mRemoteIo);
-                virtualDeviceBuilder
-                        .setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM)
-                        .setVirtualSensorCallback(
-                                MoreExecutors.directExecutor(),
-                                mRemoteSensorManager.getVirtualSensorCallback());
+                virtualDeviceBuilder.setVirtualSensorCallback(
+                        MoreExecutors.directExecutor(),
+                        mRemoteSensorManager.getVirtualSensorCallback());
             }
+            virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM);
         }
 
         if (mPreferenceController.getBoolean(R.string.pref_enable_client_camera)) {
@@ -735,19 +733,10 @@
     }
 
     RemoteDisplay createRemoteDisplay(
-            Context context, int displayId, int width, int height, int dpi, Surface surface,
+            Context context, int remoteDisplayId, int width, int height, int dpi,
             RemoteIo remoteIo) {
-        Optional<RemoteDisplay> existingDisplay =
-                mDisplayRepository.getDisplayByRemoteId(displayId);
-        if (existingDisplay.isPresent()) {
-            existingDisplay.get().setSurface(surface);
-            existingDisplay.get().reset(width, height, dpi);
-            return existingDisplay.get();
-        }
-
-        RemoteDisplay remoteDisplay = new RemoteDisplay(context, displayId, width, height, dpi,
-                mVirtualDevice, surface, remoteIo, mPendingDisplayType, mPreferenceController);
-        remoteDisplay.setSurface(surface);
+        RemoteDisplay remoteDisplay = new RemoteDisplay(context, remoteDisplayId, width, height,
+                dpi, mVirtualDevice, remoteIo, mPendingDisplayType, mPreferenceController);
         mDisplayRepository.addDisplay(remoteDisplay);
         if (mPendingRemoteIntent != null) {
             remoteDisplay.launchIntent(mPendingRemoteIntent);
@@ -756,6 +745,10 @@
         return remoteDisplay;
     }
 
+    Optional<RemoteDisplay> getRemoteDisplay(int remoteDisplayId) {
+        return mDisplayRepository.getDisplayByRemoteId(remoteDisplayId);
+    }
+
     void closeRemoteDisplay(int remoteDisplayId) {
         mDisplayRepository.removeDisplayByRemoteId(remoteDisplayId);
     }
diff --git a/tools/winscope/src/viewers/components/tree_component.ts b/tools/winscope/src/viewers/components/tree_component.ts
index 1b0950b..9753941 100644
--- a/tools/winscope/src/viewers/components/tree_component.ts
+++ b/tools/winscope/src/viewers/components/tree_component.ts
@@ -15,6 +15,7 @@
  */
 import {
   ChangeDetectionStrategy,
+  ChangeDetectorRef,
   Component,
   ElementRef,
   EventEmitter,
@@ -88,7 +89,8 @@
         (highlightedChange)="propagateNewHighlightedItem($event)"
         (pinnedItemChange)="propagateNewPinnedItem($event)"
         (hoverStart)="childHover = true"
-        (hoverEnd)="childHover = false"></tree-view>
+        (hoverEnd)="childHover = false"
+        (expandParent)="expandTree()"></tree-view>
     </div>
   `,
   styles: [nodeStyles, treeNodeDataViewStyles, nodeInnerItemStyles],
@@ -116,12 +118,13 @@
   @Output() readonly pinnedItemChange = new EventEmitter<UiHierarchyTreeNode>();
   @Output() readonly hoverStart = new EventEmitter<void>();
   @Output() readonly hoverEnd = new EventEmitter<void>();
+  @Output() readonly expandParent = new EventEmitter<void>();
 
-  localExpandedState = true;
   childHover = false;
   readonly levelOffset = 24;
   nodeElement: HTMLElement;
 
+  private localExpandedState = true;
   private storeKeyCollapsedState = '';
 
   childTrackById(
@@ -131,7 +134,10 @@
     return child.id;
   }
 
-  constructor(@Inject(ElementRef) public elementRef: ElementRef) {
+  constructor(
+    @Inject(ElementRef) public elementRef: ElementRef,
+    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
+  ) {
     this.nodeElement = elementRef.nativeElement.querySelector('.node');
     this.nodeElement?.addEventListener(
       'mousedown',
@@ -237,6 +243,8 @@
 
   expandTree() {
     this.setExpandedValue(true);
+    this.changeDetectorRef.detectChanges();
+    this.expandParent.emit();
   }
 
   isExpanded() {
diff --git a/tools/winscope/src/viewers/components/tree_component_test.ts b/tools/winscope/src/viewers/components/tree_component_test.ts
index 80728c7..ae12116 100644
--- a/tools/winscope/src/viewers/components/tree_component_test.ts
+++ b/tools/winscope/src/viewers/components/tree_component_test.ts
@@ -15,7 +15,7 @@
  */
 
 import {Clipboard, ClipboardModule} from '@angular/cdk/clipboard';
-import {Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild} from '@angular/core';
+import {Component, ViewChild} from '@angular/core';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {MatIconModule} from '@angular/material/icon';
 import {MatTooltipModule} from '@angular/material/tooltip';
@@ -49,7 +49,6 @@
         PropertyTreeNodeDataViewComponent,
       ],
       imports: [MatTooltipModule, MatIconModule, ClipboardModule],
-      schemas: [CUSTOM_ELEMENTS_SCHEMA],
     }).compileComponents();
     fixture = TestBed.createComponent(TestHostComponent);
     component = fixture.componentInstance;
@@ -63,17 +62,18 @@
 
   it('shows node', () => {
     fixture.detectChanges();
-    const treeNode = htmlElement.querySelector('tree-node');
-    expect(treeNode).toBeTruthy();
+    expect(htmlElement.querySelector('tree-node')).toBeTruthy();
   });
 
   it('can identify if a parent node has a selected child', () => {
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
-    expect(treeComponent.hasSelectedChild()).toBeFalse();
+    const treeNode = assertDefined(
+      htmlElement.querySelector<HTMLElement>('tree-node'),
+    );
+    expect(treeNode.className.includes('child-selected')).toBeFalse();
     component.highlightedItem = '3 Child3';
     fixture.detectChanges();
-    expect(treeComponent.hasSelectedChild()).toBeTrue();
+    expect(treeNode.className.includes('child-selected')).toBeTrue();
   });
 
   it('highlights node and inner node upon click', () => {
@@ -97,29 +97,23 @@
 
   it('toggles tree upon node double click', () => {
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
-    const treeNode = assertDefined(
-      htmlElement.querySelector<HTMLElement>('tree-node'),
+    const toggleButton = assertDefined(
+      htmlElement.querySelector('.toggle-tree-btn'),
     );
-    const currLocalExpandedState = treeComponent.localExpandedState;
-    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
-    fixture.detectChanges();
-    expect(!currLocalExpandedState).toEqual(treeComponent.localExpandedState);
+    expect(toggleButton.textContent?.trim()).toEqual('arrow_drop_down');
+    checkIsExpanded(true);
+
+    doubleClickFirstNode();
+    expect(toggleButton.textContent?.trim()).toEqual('chevron_right');
+    checkIsExpanded(false);
   });
 
   it('does not toggle tree in flat mode on double click', () => {
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
     component.isFlattened = true;
     fixture.detectChanges();
-    const treeNode = assertDefined(
-      htmlElement.querySelector<HTMLElement>('tree-node'),
-    );
-
-    const currLocalExpandedState = treeComponent.localExpandedState;
-    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
-    fixture.detectChanges();
-    expect(currLocalExpandedState).toEqual(treeComponent.localExpandedState);
+    doubleClickFirstNode();
+    checkIsExpanded(true);
   });
 
   it('pins node on click', () => {
@@ -138,66 +132,73 @@
 
   it('expands tree on expand tree button click', () => {
     fixture.detectChanges();
-    const treeNode = assertDefined(
-      htmlElement.querySelector<HTMLElement>('tree-node'),
-    );
-    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
-    fixture.detectChanges();
-    expect(component.treeComponent?.localExpandedState).toEqual(false);
+    doubleClickFirstNode();
+    checkIsExpanded(false);
+
     assertDefined(
       htmlElement.querySelector<HTMLElement>('.expand-tree-btn'),
     ).click();
     fixture.detectChanges();
-    expect(component.treeComponent?.localExpandedState).toEqual(true);
+    checkIsExpanded(true);
+  });
+
+  it('expands tree recursively on node selection', () => {
+    fixture.detectChanges();
+    doubleClickFirstNode();
+    checkIsExpanded(false);
+    component.highlightedItem = '79 Child79';
+    fixture.detectChanges();
+    checkIsExpanded(true);
   });
 
   it('scrolls selected node only if not in view', () => {
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
-    const treeNode = assertDefined(
-      treeComponent.elementRef.nativeElement.querySelector(`#nodeChild79`),
-    );
+    checkNodeScrolling();
+  });
 
-    component.highlightedItem = 'Root node';
+  it('scrolls selected node if not in view even if pinned', () => {
+    component.pinnedItems = [
+      assertDefined(component.tree.getChildByName('Child78')),
+      assertDefined(component.tree.getChildByName('Child79')),
+    ];
     fixture.detectChanges();
-
-    const spy = spyOn(treeNode, 'scrollIntoView').and.callThrough();
-    component.highlightedItem = '79 Child79';
-    fixture.detectChanges();
-    expect(spy).toHaveBeenCalledTimes(1);
-
-    component.highlightedItem = '78 Child78';
-    fixture.detectChanges();
-    expect(spy).toHaveBeenCalledTimes(1);
+    checkNodeScrolling();
   });
 
   it('sets initial expanded state to true by default for leaf', () => {
     fixture.detectChanges();
-    expect(assertDefined(component.treeComponent).isExpanded()).toBeTrue();
+    checkIsExpanded(true);
   });
 
   it('sets initial expanded state to true by default for non root', () => {
-    component.tree = component.tree.getAllChildren()[0];
+    const child = component.tree.getAllChildren()[0] as UiHierarchyTreeNode;
+    const innerChild = UiHierarchyTreeNode.from(
+      new HierarchyTreeBuilder()
+        .setId('InnerChild')
+        .setName('child')
+        .setChildren([])
+        .build(),
+    );
+    child.addOrReplaceChild(innerChild);
+    component.tree = child;
     fixture.detectChanges();
-    expect(assertDefined(component.treeComponent).isExpanded()).toBeTrue();
+    checkIsExpanded(true);
   });
 
   it('sets initial expanded state to false if collapse state exists in store', () => {
     component.useStoredExpandedState = true;
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
     // tree expanded by default
-    expect(treeComponent.isExpanded()).toBeTrue();
+    checkIsExpanded(true);
 
     // tree collapsed
-    treeComponent.toggleTree();
-    fixture.detectChanges();
-    expect(treeComponent.isExpanded()).toBeFalse();
+    doubleClickFirstNode();
+    checkIsExpanded(false);
 
     // tree collapsed state retained
     component.tree = makeTree();
     fixture.detectChanges();
-    expect(treeComponent.isExpanded()).toBeFalse();
+    checkIsExpanded(false);
   });
 
   it('renders show state button if applicable', () => {
@@ -313,6 +314,35 @@
     );
   }
 
+  function doubleClickFirstNode() {
+    assertDefined(
+      htmlElement.querySelector<HTMLElement>('tree-node'),
+    ).dispatchEvent(new MouseEvent('click', {detail: 2}));
+    fixture.detectChanges();
+  }
+
+  function checkIsExpanded(isExpanded: boolean) {
+    expect(htmlElement.querySelector<HTMLElement>('.children')?.hidden).toEqual(
+      !isExpanded,
+    );
+  }
+
+  function checkNodeScrolling() {
+    const treeNode = assertDefined(htmlElement.querySelector(`#nodeChild79`));
+    const spy = spyOn(treeNode, 'scrollIntoView').and.callThrough();
+
+    component.highlightedItem = 'Root node';
+    fixture.detectChanges();
+
+    component.highlightedItem = '79 Child79';
+    fixture.detectChanges();
+    expect(spy).toHaveBeenCalledTimes(1);
+
+    component.highlightedItem = '78 Child78';
+    fixture.detectChanges();
+    expect(spy).toHaveBeenCalledTimes(1);
+  }
+
   @Component({
     selector: 'host-component',
     template: `
@@ -320,7 +350,7 @@
       <tree-view
         [node]="tree"
         [isFlattened]="isFlattened"
-        [isPinned]="false"
+        [pinnedItems]="pinnedItems"
         [highlightedItem]="highlightedItem"
         [useStoredExpandedState]="useStoredExpandedState"
         [itemsClickable]="true"
@@ -342,6 +372,7 @@
     isFlattened = false;
     useStoredExpandedState = false;
     rectIdToShowState: Map<string, RectShowState> | undefined;
+    pinnedItems: Array<UiHierarchyTreeNode | UiPropertyTreeNode> = [];
 
     constructor() {
       this.tree = makeTree();
diff --git a/tools/winscope/src/viewers/components/tree_node_component.ts b/tools/winscope/src/viewers/components/tree_node_component.ts
index 9de6728..820f4ac 100644
--- a/tools/winscope/src/viewers/components/tree_node_component.ts
+++ b/tools/winscope/src/viewers/components/tree_node_component.ts
@@ -106,7 +106,7 @@
 
   @Output() readonly toggleTreeChange = new EventEmitter<void>();
   @Output() readonly rectShowStateChange = new EventEmitter<void>();
-  @Output() readonly expandTreeChange = new EventEmitter<boolean>();
+  @Output() readonly expandTreeChange = new EventEmitter<void>();
   @Output() readonly pinNodeChange = new EventEmitter<UiHierarchyTreeNode>();
 
   collapseDiffClass = '';
@@ -123,8 +123,11 @@
   }
 
   ngOnChanges() {
+    if (!this.isInPinnedSection && this.isSelected) {
+      this.expandTreeChange.emit();
+    }
     this.collapseDiffClass = this.updateCollapseDiffClass();
-    if (!this.isPinned && this.isSelected && !this.isNodeInView()) {
+    if (!this.isInPinnedSection && this.isSelected && !this.isNodeInView()) {
       this.el.scrollIntoView({block: 'center', inline: 'nearest'});
     }
   }
@@ -184,7 +187,7 @@
     this.pinNodeChange.emit(assertDefined(this.node) as UiHierarchyTreeNode);
   }
 
-  updateCollapseDiffClass() {
+  updateCollapseDiffClass(): string {
     if (this.isExpanded) {
       return '';
     }
@@ -199,7 +202,7 @@
       return '';
     }
     if (childrenDiffClasses.size === 1) {
-      const diffType = childrenDiffClasses.values().next().value;
+      const diffType = assertDefined(childrenDiffClasses.values().next().value);
       return diffType;
     }
     return DiffType.MODIFIED;
diff --git a/tools/winscope/src/viewers/components/tree_node_component_test.ts b/tools/winscope/src/viewers/components/tree_node_component_test.ts
index b5cf8ae..9df330b 100644
--- a/tools/winscope/src/viewers/components/tree_node_component_test.ts
+++ b/tools/winscope/src/viewers/components/tree_node_component_test.ts
@@ -117,6 +117,24 @@
     expect(spy).toHaveBeenCalled();
   });
 
+  it('can trigger tree expansion if node is selected and not in pinned section', () => {
+    const spy = spyOn(
+      assertDefined(component.treeNodeComponent).expandTreeChange,
+      'emit',
+    );
+    component.isInPinnedSection = true;
+    component.isSelected = true;
+    fixture.detectChanges();
+    expect(spy).not.toHaveBeenCalled();
+
+    component.isSelected = false;
+    component.isInPinnedSection = false;
+    fixture.detectChanges();
+    component.isSelected = true;
+    fixture.detectChanges();
+    expect(spy).toHaveBeenCalledTimes(1);
+  });
+
   it('assigns diff css classes to expand tree button', () => {
     const expandButton = assertDefined(
       htmlElement.querySelector<HTMLElement>('.expand-tree-btn'),
@@ -221,7 +239,7 @@
         [node]="node"
         [isExpanded]="isExpanded"
         [isPinned]="false"
-        [isInPinnedSection]="false"
+        [isInPinnedSection]="isInPinnedSection"
         [isSelected]="isSelected"
         [isLeaf]="isLeaf"></tree-node>
     `,
@@ -238,6 +256,7 @@
     isSelected = false;
     isLeaf = false;
     isExpanded = false;
+    isInPinnedSection = false;
 
     @ViewChild(TreeNodeComponent)
     treeNodeComponent: TreeNodeComponent | undefined;