Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6233caf0 authored by Nick Chameyev's avatar Nick Chameyev
Browse files

Defer display switch update if transition is running

WindowManager receives updates from DisplayManager through
onDisplayChanged() callback by reading the DisplayInfo
objects after receiving this callback.  This CL makes
WindowManager to defer the updates if there is a collecting
Shell transition. This is needed to allow starting physical
display change transition if there is another transition
running.

Previously this was silently failing without starting
a display switch transition.  It also changes the behavior
of PhysicalDisplayTransitionLauncher: now it starts the
display change transition even if it's not an 'unfold' transition.
UnfoldTransitionHandler will decide if it wants to handle
'unfold' display change and default transition handler will be used otherwise.

Bug: 259220649
Bug: 277866717
Test: atest PhysicalDisplaySwitchTransitionLauncherTest
Test: atest DisplayContentTests
Test: manual fold/unfold with apps/split screen/
  split screen + PIP
Change-Id: Ib0d0624bf141ff16578d7902bec98272d17ee36f
parent 9dfb6430
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -169,6 +169,12 @@
      "group": "WM_DEBUG_WINDOW_ORGANIZER",
      "at": "com\/android\/server\/wm\/TaskOrganizerController.java"
    },
    "-1961637874": {
      "message": "DeferredDisplayUpdater: applying DisplayInfo immediately",
      "level": "DEBUG",
      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
      "at": "com\/android\/server\/wm\/DeferredDisplayUpdater.java"
    },
    "-1949279037": {
      "message": "Attempted to add input method window with bad token %s.  Aborting.",
      "level": "WARN",
@@ -313,6 +319,12 @@
      "group": "WM_DEBUG_RESIZE",
      "at": "com\/android\/server\/wm\/WindowState.java"
    },
    "-1818910559": {
      "message": "DeferredDisplayUpdater: applied DisplayInfo after deferring",
      "level": "DEBUG",
      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
      "at": "com\/android\/server\/wm\/DeferredDisplayUpdater.java"
    },
    "-1814361639": {
      "message": "Set IME snapshot position: (%d, %d)",
      "level": "INFO",
@@ -1909,6 +1921,12 @@
      "group": "WM_DEBUG_FOCUS_LIGHT",
      "at": "com\/android\/server\/wm\/DisplayContent.java"
    },
    "-415346336": {
      "message": "DeferredDisplayUpdater: partially applying DisplayInfo immediately",
      "level": "DEBUG",
      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
      "at": "com\/android\/server\/wm\/DeferredDisplayUpdater.java"
    },
    "-401282500": {
      "message": "destroyIfPossible: r=%s destroy returned removed=%s",
      "level": "DEBUG",
@@ -1957,6 +1975,12 @@
      "group": "WM_DEBUG_APP_TRANSITIONS",
      "at": "com\/android\/server\/wm\/AppTransitionController.java"
    },
    "-376950429": {
      "message": "DeferredDisplayUpdater: deferring DisplayInfo update",
      "level": "DEBUG",
      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
      "at": "com\/android\/server\/wm\/DeferredDisplayUpdater.java"
    },
    "-374767836": {
      "message": "setAppVisibility(%s, visible=%b): %s visible=%b mVisibleRequested=%b Callers=%s",
      "level": "VERBOSE",
+1 −2
Original line number Diff line number Diff line
@@ -50,7 +50,6 @@ import android.window.WindowContainerTransaction;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
import com.android.wm.shell.common.split.SplitScreenUtils;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.pip.PipTransitionController;
@@ -298,7 +297,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
            mixed.mLeftoversHandler = handler.first;
            mActiveTransitions.add(mixed);
            return handler.second;
        } else if (mUnfoldHandler != null && mUnfoldHandler.hasUnfold(request)) {
        } else if (mUnfoldHandler != null && mUnfoldHandler.shouldPlayUnfoldAnimation(request)) {
            final WindowContainerTransaction wct =
                    mUnfoldHandler.handleRequest(transition, request);
            if (wct != null) {
+27 −5
Original line number Diff line number Diff line
@@ -106,7 +106,7 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
            @NonNull SurfaceControl.Transaction startTransaction,
            @NonNull SurfaceControl.Transaction finishTransaction,
            @NonNull TransitionFinishCallback finishCallback) {
        if (hasUnfold(info) && transition != mTransition) {
        if (shouldPlayUnfoldAnimation(info) && transition != mTransition) {
            // Take over transition that has unfold, we might receive it if no other handler
            // accepted request in handleRequest, e.g. for rotation + unfold or
            // TRANSIT_NONE + unfold transitions
@@ -213,14 +213,36 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
    }

    /** Whether `request` contains an unfold action. */
    public boolean hasUnfold(@NonNull TransitionRequestInfo request) {
    public boolean shouldPlayUnfoldAnimation(@NonNull TransitionRequestInfo request) {
        // Unfold animation won't play when animations are disabled
        if (!ValueAnimator.areAnimatorsEnabled()) return false;

        return (request.getType() == TRANSIT_CHANGE
                && request.getDisplayChange() != null
                && request.getDisplayChange().isPhysicalDisplayChanged());
                && isUnfoldDisplayChange(request.getDisplayChange()));
    }

    private boolean isUnfoldDisplayChange(
            @NonNull TransitionRequestInfo.DisplayChange displayChange) {
        if (!displayChange.isPhysicalDisplayChanged()) {
            return false;
        }

        if (displayChange.getStartAbsBounds() == null || displayChange.getEndAbsBounds() == null) {
            return false;
        }

        // Handle only unfolding, currently we don't have an animation when folding
        final int endArea =
                displayChange.getEndAbsBounds().width() * displayChange.getEndAbsBounds().height();
        final int startArea = displayChange.getStartAbsBounds().width()
                * displayChange.getStartAbsBounds().height();

        return endArea > startArea;
    }

    /** Whether `transitionInfo` contains an unfold action. */
    public boolean hasUnfold(@NonNull TransitionInfo transitionInfo) {
    public boolean shouldPlayUnfoldAnimation(@NonNull TransitionInfo transitionInfo) {
        // Unfold animation won't play when animations are disabled
        if (!ValueAnimator.areAnimatorsEnabled()) return false;

@@ -250,7 +272,7 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
    @Override
    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
            @NonNull TransitionRequestInfo request) {
        if (hasUnfold(request)) {
        if (shouldPlayUnfoldAnimation(request)) {
            mTransition = transition;
            return new WindowContainerTransaction();
        }
+26 −3
Original line number Diff line number Diff line
@@ -98,10 +98,13 @@ public class UnfoldTransitionHandlerTest {
    }

    @Test
    public void handleRequest_physicalDisplayChange_handlesTransition() {
    public void handleRequest_physicalDisplayChangeUnfold_handlesTransition() {
        ActivityManager.RunningTaskInfo triggerTaskInfo = new ActivityManager.RunningTaskInfo();
        TransitionRequestInfo.DisplayChange displayChange = new TransitionRequestInfo.DisplayChange(
                Display.DEFAULT_DISPLAY).setPhysicalDisplayChanged(true);
                Display.DEFAULT_DISPLAY)
                .setPhysicalDisplayChanged(true)
                .setStartAbsBounds(new Rect(0, 0, 100, 100))
                .setEndAbsBounds(new Rect(0, 0, 200, 200));
        TransitionRequestInfo requestInfo = new TransitionRequestInfo(TRANSIT_CHANGE,
                triggerTaskInfo, /* remoteTransition= */ null, displayChange, 0 /* flags */);

@@ -111,6 +114,23 @@ public class UnfoldTransitionHandlerTest {
        assertThat(result).isNotNull();
    }

    @Test
    public void handleRequest_physicalDisplayChangeFold_doesNotHandleTransition() {
        ActivityManager.RunningTaskInfo triggerTaskInfo = new ActivityManager.RunningTaskInfo();
        TransitionRequestInfo.DisplayChange displayChange = new TransitionRequestInfo.DisplayChange(
                Display.DEFAULT_DISPLAY)
                .setPhysicalDisplayChanged(true)
                .setStartAbsBounds(new Rect(0, 0, 200, 200))
                .setEndAbsBounds(new Rect(0, 0, 100, 100));
        TransitionRequestInfo requestInfo = new TransitionRequestInfo(TRANSIT_CHANGE,
                triggerTaskInfo, /* remoteTransition= */ null, displayChange, 0 /* flags */);

        WindowContainerTransaction result = mUnfoldTransitionHandler.handleRequest(mTransition,
                requestInfo);

        assertThat(result).isNull();
    }

    @Test
    public void handleRequest_noPhysicalDisplayChange_doesNotHandleTransition() {
        ActivityManager.RunningTaskInfo triggerTaskInfo = new ActivityManager.RunningTaskInfo();
@@ -306,7 +326,10 @@ public class UnfoldTransitionHandlerTest {
    private TransitionRequestInfo createUnfoldTransitionRequestInfo() {
        ActivityManager.RunningTaskInfo triggerTaskInfo = new ActivityManager.RunningTaskInfo();
        TransitionRequestInfo.DisplayChange displayChange = new TransitionRequestInfo.DisplayChange(
                Display.DEFAULT_DISPLAY).setPhysicalDisplayChanged(true);
                Display.DEFAULT_DISPLAY)
                .setPhysicalDisplayChanged(true)
                .setStartAbsBounds(new Rect(0, 0, 100, 100))
                .setEndAbsBounds(new Rect(0, 0, 200, 200));
        return new TransitionRequestInfo(TRANSIT_CHANGE,
                triggerTaskInfo, /* remoteTransition= */ null, displayChange, 0 /* flags */);
    }
+345 −0
Original line number Diff line number Diff line
/*
 * 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.server.wm;

import static android.view.WindowManager.TRANSIT_CHANGE;

import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS;
import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.view.DisplayInfo;
import android.window.DisplayAreaInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.display.BrightnessSynchronizer;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.wm.utils.DisplayInfoOverrides.DisplayInfoFieldsUpdater;

import java.util.Arrays;
import java.util.Objects;

/**
 * A DisplayUpdater that could defer and queue display updates coming from DisplayManager to
 * WindowManager. It allows to defer pending display updates if WindowManager is currently not
 * ready to apply them.
 * For example, this might happen if there is a Shell transition running and physical display
 * changed. We can't immediately apply the display updates because we want to start a separate
 * display change transition. In this case, we will queue all display updates until the current
 * transition's collection finishes and then apply them afterwards.
 */
public class DeferredDisplayUpdater implements DisplayUpdater {

    /**
     * List of fields that could be deferred before applying to DisplayContent.
     * This should be kept in sync with {@link DeferredDisplayUpdater#calculateDisplayInfoDiff}
     */
    @VisibleForTesting
    static final DisplayInfoFieldsUpdater DEFERRABLE_FIELDS = (out, override) -> {
        // Treat unique id and address change as WM-specific display change as we re-query display
        // settings and parameters based on it which could cause window changes
        out.uniqueId = override.uniqueId;
        out.address = override.address;

        // Also apply WM-override fields, since they might produce differences in window hierarchy
        WM_OVERRIDE_FIELDS.setFields(out, override);
    };

    private final DisplayContent mDisplayContent;

    @NonNull
    private final DisplayInfo mNonOverrideDisplayInfo = new DisplayInfo();

    /**
     * The last known display parameters from DisplayManager, some WM-specific fields in this object
     * might not be applied to the DisplayContent yet
     */
    @Nullable
    private DisplayInfo mLastDisplayInfo;

    /**
     * The last DisplayInfo that was applied to DisplayContent, only WM-specific parameters must be
     * used from this object. This object is used to store old values of DisplayInfo while these
     * fields are pending to be applied to DisplayContent.
     */
    @Nullable
    private DisplayInfo mLastWmDisplayInfo;

    @NonNull
    private final DisplayInfo mOutputDisplayInfo = new DisplayInfo();

    public DeferredDisplayUpdater(@NonNull DisplayContent displayContent) {
        mDisplayContent = displayContent;
        mNonOverrideDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
    }

    /**
     * Reads the latest display parameters from the display manager and returns them in a callback.
     * If there are pending display updates, it will wait for them to finish first and only then it
     * will call the callback with the latest display parameters.
     *
     * @param finishCallback is called when all pending display updates are finished
     */
    @Override
    public void updateDisplayInfo(@NonNull Runnable finishCallback) {
        // Get the latest display parameters from the DisplayManager
        final DisplayInfo displayInfo = getCurrentDisplayInfo();

        final int displayInfoDiff = calculateDisplayInfoDiff(mLastDisplayInfo, displayInfo);
        final boolean physicalDisplayUpdated = isPhysicalDisplayUpdated(mLastDisplayInfo,
                displayInfo);

        mLastDisplayInfo = displayInfo;

        // Apply whole display info immediately as is if either:
        // * it is the first display update
        // * shell transitions are disabled or temporary unavailable
        if (displayInfoDiff == DIFF_EVERYTHING
                || !mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
            ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
                    "DeferredDisplayUpdater: applying DisplayInfo immediately");

            mLastWmDisplayInfo = displayInfo;
            applyLatestDisplayInfo();
            finishCallback.run();
            return;
        }

        // If there are non WM-specific display info changes, apply only these fields immediately
        if ((displayInfoDiff & DIFF_NOT_WM_DEFERRABLE) > 0) {
            ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
                    "DeferredDisplayUpdater: partially applying DisplayInfo immediately");
            applyLatestDisplayInfo();
        }

        // If there are WM-specific display info changes, apply them through a Shell transition
        if ((displayInfoDiff & DIFF_WM_DEFERRABLE) > 0) {
            ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
                    "DeferredDisplayUpdater: deferring DisplayInfo update");

            requestDisplayChangeTransition(physicalDisplayUpdated, () -> {
                // Apply deferrable fields to DisplayContent only when the transition
                // starts collecting, non-deferrable fields are ignored in mLastWmDisplayInfo
                mLastWmDisplayInfo = displayInfo;
                applyLatestDisplayInfo();
                finishCallback.run();
            });
        } else {
            // There are no WM-specific updates, so we can immediately notify that all display
            // info changes are applied
            finishCallback.run();
        }
    }

    /**
     * Requests a display change Shell transition
     *
     * @param physicalDisplayUpdated if true also starts remote display change
     * @param onStartCollect         called when the Shell transition starts collecting
     */
    private void requestDisplayChangeTransition(boolean physicalDisplayUpdated,
            @NonNull Runnable onStartCollect) {

        final Transition transition = new Transition(TRANSIT_CHANGE, /* flags= */ 0,
                mDisplayContent.mTransitionController,
                mDisplayContent.mTransitionController.mSyncEngine);

        mDisplayContent.mAtmService.startPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY);

        mDisplayContent.mTransitionController.startCollectOrQueue(transition, deferred -> {
            final Rect startBounds = new Rect(0, 0, mDisplayContent.mInitialDisplayWidth,
                    mDisplayContent.mInitialDisplayHeight);
            final int fromRotation = mDisplayContent.getRotation();

            onStartCollect.run();

            ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
                    "DeferredDisplayUpdater: applied DisplayInfo after deferring");

            if (physicalDisplayUpdated) {
                onDisplayUpdated(transition, fromRotation, startBounds);
            } else {
                transition.setAllReady();
            }
        });
    }

    /**
     * Applies current DisplayInfo to DisplayContent, DisplayContent is merged from two parts:
     * - non-deferrable fields are set from the most recent values received from DisplayManager
     * (uses {@link mLastDisplayInfo} field)
     * - deferrable fields are set from the latest values that we could apply to WM
     * (uses {@link mLastWmDisplayInfo} field)
     */
    private void applyLatestDisplayInfo() {
        copyDisplayInfoFields(mOutputDisplayInfo, /* base= */ mLastDisplayInfo,
                /* override= */ mLastWmDisplayInfo, /* fields= */ DEFERRABLE_FIELDS);
        mDisplayContent.onDisplayInfoUpdated(mOutputDisplayInfo);
    }

    @NonNull
    private DisplayInfo getCurrentDisplayInfo() {
        mDisplayContent.mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo(
                mDisplayContent.mDisplayId, mNonOverrideDisplayInfo);
        return new DisplayInfo(mNonOverrideDisplayInfo);
    }

    /**
     * Called when physical display is updated, this could happen e.g. on foldable
     * devices when the physical underlying display is replaced. This method should be called
     * when the new display info is already applied to the WM hierarchy.
     *
     * @param fromRotation rotation before the display change
     * @param startBounds  display bounds before the display change
     */
    private void onDisplayUpdated(@NonNull Transition transition, int fromRotation,
            @NonNull Rect startBounds) {
        final Rect endBounds = new Rect(0, 0, mDisplayContent.mInitialDisplayWidth,
                mDisplayContent.mInitialDisplayHeight);
        final int toRotation = mDisplayContent.getRotation();

        final TransitionRequestInfo.DisplayChange displayChange =
                new TransitionRequestInfo.DisplayChange(mDisplayContent.getDisplayId());
        displayChange.setStartAbsBounds(startBounds);
        displayChange.setEndAbsBounds(endBounds);
        displayChange.setStartRotation(fromRotation);
        displayChange.setEndRotation(toRotation);
        displayChange.setPhysicalDisplayChanged(true);

        mDisplayContent.mTransitionController.requestStartTransition(transition,
                /* startTask= */ null, /* remoteTransition= */ null, displayChange);

        final DisplayAreaInfo newDisplayAreaInfo = mDisplayContent.getDisplayAreaInfo();

        final boolean startedRemoteChange = mDisplayContent.mRemoteDisplayChangeController
                .performRemoteDisplayChange(fromRotation, toRotation, newDisplayAreaInfo,
                        transaction -> finishDisplayUpdate(transaction, transition));

        if (!startedRemoteChange) {
            finishDisplayUpdate(/* wct= */ null, transition);
        }
    }

    private void finishDisplayUpdate(@Nullable WindowContainerTransaction wct,
            @NonNull Transition transition) {
        if (wct != null) {
            mDisplayContent.mAtmService.mWindowOrganizerController.applyTransaction(
                    wct);
        }
        transition.setAllReady();
    }

    private boolean isPhysicalDisplayUpdated(@Nullable DisplayInfo first,
            @Nullable DisplayInfo second) {
        if (first == null || second == null) return true;
        return !Objects.equals(first.uniqueId, second.uniqueId);
    }

    /**
     * Diff result: fields are the same
     */
    static final int DIFF_NONE = 0;

    /**
     * Diff result: fields that could be deferred in WM are different
     */
    static final int DIFF_WM_DEFERRABLE = 1 << 0;

    /**
     * Diff result: fields that could not be deferred in WM are different
     */
    static final int DIFF_NOT_WM_DEFERRABLE = 1 << 1;

    /**
     * Diff result: everything is different
     */
    static final int DIFF_EVERYTHING = 0XFFFFFFFF;

    @VisibleForTesting
    static int calculateDisplayInfoDiff(@Nullable DisplayInfo first, @Nullable DisplayInfo second) {
        int diff = DIFF_NONE;

        if (Objects.equals(first, second)) return diff;
        if (first == null || second == null) return DIFF_EVERYTHING;

        if (first.layerStack != second.layerStack
                || first.flags != second.flags
                || first.type != second.type
                || first.displayId != second.displayId
                || first.displayGroupId != second.displayGroupId
                || !Objects.equals(first.deviceProductInfo, second.deviceProductInfo)
                || first.modeId != second.modeId
                || first.renderFrameRate != second.renderFrameRate
                || first.defaultModeId != second.defaultModeId
                || first.userPreferredModeId != second.userPreferredModeId
                || !Arrays.equals(first.supportedModes, second.supportedModes)
                || first.colorMode != second.colorMode
                || !Arrays.equals(first.supportedColorModes, second.supportedColorModes)
                || !Objects.equals(first.hdrCapabilities, second.hdrCapabilities)
                || !Arrays.equals(first.userDisabledHdrTypes, second.userDisabledHdrTypes)
                || first.minimalPostProcessingSupported != second.minimalPostProcessingSupported
                || first.appVsyncOffsetNanos != second.appVsyncOffsetNanos
                || first.presentationDeadlineNanos != second.presentationDeadlineNanos
                || first.state != second.state
                || first.committedState != second.committedState
                || first.ownerUid != second.ownerUid
                || !Objects.equals(first.ownerPackageName, second.ownerPackageName)
                || first.removeMode != second.removeMode
                || first.getRefreshRate() != second.getRefreshRate()
                || first.brightnessMinimum != second.brightnessMinimum
                || first.brightnessMaximum != second.brightnessMaximum
                || first.brightnessDefault != second.brightnessDefault
                || first.installOrientation != second.installOrientation
                || !Objects.equals(first.layoutLimitedRefreshRate, second.layoutLimitedRefreshRate)
                || !BrightnessSynchronizer.floatEquals(first.hdrSdrRatio, second.hdrSdrRatio)
                || !first.thermalRefreshRateThrottling.contentEquals(
                second.thermalRefreshRateThrottling)
                || !Objects.equals(first.thermalBrightnessThrottlingDataId,
                second.thermalBrightnessThrottlingDataId)) {
            diff |= DIFF_NOT_WM_DEFERRABLE;
        }

        if (first.appWidth != second.appWidth
                || first.appHeight != second.appHeight
                || first.smallestNominalAppWidth != second.smallestNominalAppWidth
                || first.smallestNominalAppHeight != second.smallestNominalAppHeight
                || first.largestNominalAppWidth != second.largestNominalAppWidth
                || first.largestNominalAppHeight != second.largestNominalAppHeight
                || first.logicalWidth != second.logicalWidth
                || first.logicalHeight != second.logicalHeight
                || first.physicalXDpi != second.physicalXDpi
                || first.physicalYDpi != second.physicalYDpi
                || first.rotation != second.rotation
                || !Objects.equals(first.displayCutout, second.displayCutout)
                || first.logicalDensityDpi != second.logicalDensityDpi
                || !Objects.equals(first.roundedCorners, second.roundedCorners)
                || !Objects.equals(first.displayShape, second.displayShape)
                || !Objects.equals(first.uniqueId, second.uniqueId)
                || !Objects.equals(first.address, second.address)
        ) {
            diff |= DIFF_WM_DEFERRABLE;
        }

        return diff;
    }
}
Loading