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;